Skip to content
清晨的一缕阳光
返回

网关统一鉴权

网关统一鉴权

认证授权架构

整体架构

┌─────────────┐
│   Client    │
└──────┬──────┘


┌─────────────────────────────────────────┐
│         Spring Cloud Gateway            │
│  ┌─────────────────────────────────┐   │
│  │      认证过滤器 (AuthFilter)     │   │
│  │  - Token 验证                    │   │
│  │  - 用户信息解析                  │   │
│  │  - 权限校验                      │   │
│  └──────────────┬──────────────────┘   │
│                 │                       │
│  ┌──────────────▼──────────────────┐   │
│  │      路由过滤器 (RouteFilter)    │   │
│  │  - 添加用户信息到请求头           │   │
│  │  - 传递认证上下文                │   │
│  └──────────────┬──────────────────┘   │
└─────────────────┼───────────────────────┘

        ┌─────────┼─────────┐
        │         │         │
        ▼         ▼         ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│用户服务    │ │订单服务    │ │商品服务    │
│(验证用户)  │ │(验证权限)  │ │(验证权限)  │
└───────────┘ └───────────┘ └───────────┘

认证流程

  1. 客户端携带 Token 请求网关
  2. 网关验证 Token 有效性
  3. 网关解析用户信息
  4. 网关传递用户信息到下游服务
  5. 下游服务根据用户信息执行业务逻辑

JWT 认证

JWT 结构

Header.Payload.Signature

Header:
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "roles": ["USER", "ADMIN"]
}

Signature:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  your-256-bit-secret
)

JWT 工具类

@Component
public class JwtUtil {
    
    @Value("${jwt.secret:mySecretKey123456789}")
    private String secret;
    
    @Value("${jwt.expiration:3600}")
    private Long expiration;
    
    /**
     * 生成 Token
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities());
        claims.put("userId", userDetails.getUserId());
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
            .signWith(SignatureAlgorithm.HS256, secret)
            .compact();
    }
    
    /**
     * 验证 Token
     */
    public Boolean validateToken(String token) {
        try {
            Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token);
            return true;
        } catch (JwtException e) {
            return false;
        }
    }
    
    /**
     * 解析 Token 获取用户名
     */
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        return claims.getSubject();
    }
    
    /**
     * 解析 Token 获取用户 ID
     */
    public Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        return claims.get("userId", Long.class);
    }
    
    /**
     * 解析 Token 获取角色列表
     */
    @SuppressWarnings("unchecked")
    public List<String> getRolesFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
        return (List<String>) claims.get("roles");
    }
}

网关认证过滤器

全局认证过滤器

@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 白名单路径
    private static final List<String> WHITE_LIST = Arrays.asList(
        "/api/auth/login",
        "/api/auth/register",
        "/api/auth/refresh",
        "/api/public/",
        "/actuator/",
        "/v3/api-docs/"
    );
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 白名单放行
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }
        
        // 获取 Token
        String token = extractToken(request);
        if (token == null) {
            return unauthorized(exchange, "未提供认证信息");
        }
        
        // 验证 Token
        if (!jwtUtil.validateToken(token)) {
            return unauthorized(exchange, "Token 无效或已过期");
        }
        
        // 检查 Token 是否在黑名单中(已登出)
        if (isTokenInBlacklist(token)) {
            return unauthorized(exchange, "Token 已失效");
        }
        
        // 解析用户信息
        Long userId = jwtUtil.getUserIdFromToken(token);
        String username = jwtUtil.getUsernameFromToken(token);
        List<String> roles = jwtUtil.getRolesFromToken(token);
        
        // 添加用户信息到请求头
        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-User-ID", String.valueOf(userId))
            .header("X-Username", username)
            .header("X-Roles", String.join(",", roles))
            .build();
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
    
    @Override
    public int getOrder() {
        return -100;  // 高优先级
    }
    
    /**
     * 检查是否在白名单
     */
    private boolean isWhiteList(String path) {
        return WHITE_LIST.stream().anyMatch(path::startsWith);
    }
    
    /**
     * 提取 Token
     */
    private String extractToken(ServerHttpRequest request) {
        String bearerToken = request.getHeaders().getFirst("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        
        // 支持从查询参数获取 Token
        String queryParams = request.getQueryParams().getFirst("access_token");
        if (queryParams != null) {
            return queryParams;
        }
        
        return null;
    }
    
    /**
     * 检查 Token 是否在黑名单
     */
    private boolean isTokenInBlacklist(String token) {
        String key = "token:blacklist:" + token;
        Boolean exists = redisTemplate.hasKey(key);
        return exists != null && exists;
    }
    
    /**
     * 返回 401
     */
    private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Map<String, Object> data = new HashMap<>();
        data.put("code", 401);
        data.put("message", message);
        
        byte[] bytes = JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        
        return response.writeWith(Mono.just(buffer));
    }
}

RBAC 权限控制

权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequireRole {
    /**
     * 需要的角色
     */
    String[] value() default {};
    
    /**
     * 需要的权限
     */
    String[] permissions() default {};
    
    /**
     * 逻辑关系(AND/OR)
     */
    Logical logical() default Logical.OR;
}

public enum Logical {
    AND, OR
}

权限校验过滤器

@Component
public class PermissionFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    // 不需要权限校验的路径
    private static final List<String> EXCLUDE_PATHS = Arrays.asList(
        "/api/public/",
        "/actuator/"
    );
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 排除路径
        if (EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }
        
        // 获取用户角色
        String rolesHeader = request.getHeaders().getFirst("X-Roles");
        if (rolesHeader == null) {
            return forbidden(exchange, "缺少角色信息");
        }
        
        List<String> userRoles = Arrays.asList(rolesHeader.split(","));
        
        // 获取路径需要的角色(从 Redis 或配置)
        Set<String> requiredRoles = getRequiredRoles(path);
        
        // 校验角色
        if (!requiredRoles.isEmpty() && !hasRole(userRoles, requiredRoles)) {
            return forbidden(exchange, "权限不足");
        }
        
        return chain.filter(exchange);
    }
    
    @Override
    public int getOrder() {
        return -90;  // 在认证过滤器之后
    }
    
    /**
     * 获取路径需要的角色
     */
    private Set<String> getRequiredRoles(String path) {
        String key = "path:role:" + path;
        Set<String> roles = redisTemplate.opsForSet().members(key);
        return roles != null ? roles : Collections.emptySet();
    }
    
    /**
     * 检查是否有角色
     */
    private boolean hasRole(List<String> userRoles, Set<String> requiredRoles) {
        // 只要有其中一个角色即可
        return userRoles.stream().anyMatch(requiredRoles::contains);
    }
    
    /**
     * 返回 403
     */
    private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.FORBIDDEN);
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        
        Map<String, Object> data = new HashMap<>();
        data.put("code", 403);
        data.put("message", message);
        
        byte[] bytes = JSON.toJSONString(data).getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bytes);
        
        return response.writeWith(Mono.just(buffer));
    }
}

OAuth2 集成

OAuth2 认证服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
            .withClient("gateway-client")
            .secret("{noop}gateway-secret")
            .authorizedGrantTypes("authorization_code", "password", "refresh_token")
            .scopes("read", "write")
            .redirectUris("http://localhost:8080/callback")
            .accessTokenValiditySeconds(3600)
            .refreshTokenValiditySeconds(86400);
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            .authenticationManager(authenticationManager)
            .tokenStore(redisTokenStore())
            .reuseRefreshTokens(false);
    }
    
    @Bean
    public TokenStore redisTokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }
}

网关集成 OAuth2

@Component
public class OAuth2AuthFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private OAuth2ClientService oAuth2ClientService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();
        
        // 白名单放行
        if (isWhiteList(path)) {
            return chain.filter(exchange);
        }
        
        // 获取 Token
        String token = extractToken(request);
        if (token == null) {
            return unauthorized(exchange, "未提供认证信息");
        }
        
        // 验证 Token(调用 OAuth2 服务器)
        OAuth2TokenInfo tokenInfo = oAuth2ClientService.validateToken(token);
        if (tokenInfo == null) {
            return unauthorized(exchange, "Token 无效或已过期");
        }
        
        // 添加用户信息到请求头
        ServerHttpRequest mutatedRequest = request.mutate()
            .header("X-User-ID", String.valueOf(tokenInfo.getUserId()))
            .header("X-Username", tokenInfo.getUsername())
            .header("X-Roles", String.join(",", tokenInfo.getRoles()))
            .build();
        
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }
    
    @Override
    public int getOrder() {
        return -100;
    }
}

单点登录(SSO)

SSO 配置

@Configuration
public class SsoConfig {
    
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .pathMatchers("/api/public/**").permitAll()
                .pathMatchers("/api/**").authenticated()
                .anyExchange().permitAll()
            .and()
            .oauth2Login()
            .and()
            .oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        
        return http.build();
    }
    
    @Bean
    public Converter<Jwt, ? extends Mono<? extends Authentication>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
}

下游服务集成

用户信息获取

@Component
public class UserContext {
    
    private static final ThreadLocal<UserInfo> USER_INFO = new ThreadLocal<>();
    
    public static void setUserInfo(UserInfo userInfo) {
        USER_INFO.set(userInfo);
    }
    
    public static UserInfo getUserInfo() {
        return USER_INFO.get();
    }
    
    public static void clear() {
        USER_INFO.remove();
    }
    
    public static Long getUserId() {
        UserInfo userInfo = getUserInfo();
        return userInfo != null ? userInfo.getUserId() : null;
    }
    
    public static String getUsername() {
        UserInfo userInfo = getUserInfo();
        return userInfo != null ? userInfo.getUsername() : null;
    }
}

拦截器获取用户信息

@Component
public class UserContextInterceptor implements HandlerInterceptor {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            
            if (jwtUtil.validateToken(token)) {
                UserInfo userInfo = new UserInfo();
                userInfo.setUserId(jwtUtil.getUserIdFromToken(token));
                userInfo.setUsername(jwtUtil.getUsernameFromToken(token));
                userInfo.setRoles(jwtUtil.getRolesFromToken(token));
                
                UserContext.setUserInfo(userInfo);
            }
        }
        
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        UserContext.clear();
    }
}

最佳实践

1. Token 刷新机制

@Component
public class TokenRefreshFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange)
            .then(Mono.fromRunnable(() -> {
                ServerHttpRequest request = exchange.getRequest();
                String token = extractToken(request);
                
                if (token != null) {
                    // Token 即将过期时自动刷新
                    if (isTokenExpiringSoon(token)) {
                        String newToken = refreshToken(token);
                        exchange.getResponse().getHeaders()
                            .set("X-New-Token", newToken);
                    }
                }
            }));
    }
    
    @Override
    public int getOrder() {
        return 100;  // 在认证过滤器之后
    }
    
    private boolean isTokenExpiringSoon(String token) {
        // 检查 Token 是否将在 5 分钟内过期
        Claims claims = Jwts.parser()
            .setSigningKey(jwtUtil.getSecret())
            .parseClaimsJws(token)
            .getBody();
        
        Date expiration = claims.getExpiration();
        long now = System.currentTimeMillis();
        long expTime = expiration.getTime();
        
        return (expTime - now) < 5 * 60 * 1000;
    }
    
    private String refreshToken(String token) {
        // 实现 Token 刷新逻辑
        Claims claims = Jwts.parser()
            .setSigningKey(jwtUtil.getSecret())
            .parseClaimsJws(token)
            .getBody();
        
        UserDetails userDetails = loadUserByUsername(claims.getSubject());
        return jwtUtil.generateToken(userDetails);
    }
}

2. 权限缓存

@Component
public class PermissionCache {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 缓存路径权限
     */
    public void cachePathPermission(String path, Set<String> roles) {
        String key = "path:role:" + path;
        redisTemplate.delete(key);
        if (!roles.isEmpty()) {
            redisTemplate.opsForSet().add(key, roles.toArray(new String[0]));
        }
        redisTemplate.expire(key, 1, TimeUnit.HOURS);
    }
    
    /**
     * 获取路径权限
     */
    public Set<String> getPathPermission(String path) {
        String key = "path:role:" + path;
        Set<String> roles = redisTemplate.opsForSet().members(key);
        return roles != null ? roles : Collections.emptySet();
    }
    
    /**
     * 清除权限缓存
     */
    public void clearPermissionCache() {
        Set<String> keys = redisTemplate.keys("path:role:*");
        if (keys != null && !keys.isEmpty()) {
            redisTemplate.delete(keys);
        }
    }
}

3. 安全日志

@Component
public class SecurityAuditFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private AuditLogService auditLogService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        long startTime = System.currentTimeMillis();
        
        return chain.filter(exchange)
            .doOnSuccess(aVoid -> {
                long duration = System.currentTimeMillis() - startTime;
                logAccess(exchange, duration, null);
            })
            .doOnError(throwable -> {
                long duration = System.currentTimeMillis() - startTime;
                logAccess(exchange, duration, throwable);
            });
    }
    
    @Override
    public int getOrder() {
        return Integer.MAX_VALUE;  // 最后执行
    }
    
    private void logAccess(ServerWebExchange exchange, long duration, Throwable error) {
        ServerHttpRequest request = exchange.getRequest();
        
        AuditLog log = new AuditLog();
        log.setTimestamp(LocalDateTime.now());
        log.setMethod(request.getMethod().name());
        log.setPath(request.getPath().value());
        log.setIp(request.getRemoteAddress().getAddress().getHostAddress());
        log.setUserId(request.getHeaders().getFirst("X-User-ID"));
        log.setUsername(request.getHeaders().getFirst("X-Username"));
        log.setDuration(duration);
        log.setStatus(exchange.getResponse().getStatusCode().value());
        log.setError(error != null ? error.getMessage() : null);
        
        auditLogService.save(log);
    }
}

总结

网关统一鉴权是微服务安全的第一道防线,通过在网关层集中处理认证和授权,可以实现统一的安全策略和权限控制。

合理的认证架构和权限设计可以有效保护系统安全,降低下游服务的实现复杂度。

在生产环境中,建议使用成熟的认证方案(如 JWT、OAuth2),并建立完善的日志审计机制。


分享这篇文章到:

上一篇文章
A2A 通信协议与协作
下一篇文章
Spring Boot CI/CD 流水线实战