网关统一鉴权
认证授权架构
整体架构
┌─────────────┐
│ Client │
└──────┬──────┘
│
▼
┌─────────────────────────────────────────┐
│ Spring Cloud Gateway │
│ ┌─────────────────────────────────┐ │
│ │ 认证过滤器 (AuthFilter) │ │
│ │ - Token 验证 │ │
│ │ - 用户信息解析 │ │
│ │ - 权限校验 │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ┌──────────────▼──────────────────┐ │
│ │ 路由过滤器 (RouteFilter) │ │
│ │ - 添加用户信息到请求头 │ │
│ │ - 传递认证上下文 │ │
│ └──────────────┬──────────────────┘ │
└─────────────────┼───────────────────────┘
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│用户服务 │ │订单服务 │ │商品服务 │
│(验证用户) │ │(验证权限) │ │(验证权限) │
└───────────┘ └───────────┘ └───────────┘
认证流程
- 客户端携带 Token 请求网关
- 网关验证 Token 有效性
- 网关解析用户信息
- 网关传递用户信息到下游服务
- 下游服务根据用户信息执行业务逻辑
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),并建立完善的日志审计机制。