feat: 优化JWT认证逻辑,增强错误处理和CORS支持

This commit is contained in:
zyh
2025-08-29 22:32:16 +08:00
parent e7d36d5723
commit 74e3a23b33
3 changed files with 109 additions and 157 deletions

View File

@@ -1,9 +1,13 @@
package com.gameplatform.server.security; package com.gameplatform.server.security;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
@@ -15,6 +19,7 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain; import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Collections; import java.util.Collections;
@Component @Component
@@ -29,95 +34,86 @@ public class JwtAuthenticationFilter implements WebFilter {
@Override @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getPath().value(); final var request = exchange.getRequest();
String method = exchange.getRequest().getMethod().name(); final String path = request.getPath().value();
final String method = request.getMethodValue();
log.info("=== JWT过滤器开始处理请求 ===");
log.info("请求路径: {}, 请求方法: {}", path, method); log.debug("JWT过滤器处理请求: {} {}", method, path);
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); final String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
log.info("Authorization头: {}", authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "null");
// 没有 Authorization不做认证交给后续链匿名访问或由安全链控制
if (authHeader == null) { if (authHeader == null || !authHeader.startsWith("Bearer ")) {
log.info("未找到Authorization头跳过JWT认证继续处理请求");
return chain.filter(exchange); return chain.filter(exchange);
} }
if (!authHeader.startsWith("Bearer ")) { final String token = authHeader.substring(7);
log.warn("Authorization头格式无效期望格式: 'Bearer <token>',实际: {}", authHeader);
log.info("跳过JWT认证继续处理请求");
return chain.filter(exchange);
}
String token = authHeader.substring(7);
log.info("开始处理JWT tokentoken长度: {}", token.length());
log.debug("JWT token内容: {}", token);
try { try {
log.info("开始解析JWT token");
Claims claims = jwtService.parse(token); Claims claims = jwtService.parse(token);
log.info("JWT token解析成功");
Long userId = claims.get("userId", Long.class);
Long userId = claims.get("userId", Long.class);
String userType = claims.get("userType", String.class); String userType = claims.get("userType", String.class);
String username = claims.get("username", String.class); String username = claims.get("username", String.class);
String subject = claims.getSubject();
log.info("JWT claims解析结果:");
log.info(" - subject: {}", subject);
log.info(" - userId: {}", userId);
log.info(" - userType: {}", userType);
log.info(" - username: {}", username);
log.info(" - issuedAt: {}", claims.getIssuedAt());
log.info(" - expiration: {}", claims.getExpiration());
log.info(" - 所有claims键: {}", claims.keySet());
if (userId == null || userType == null || username == null) { if (userId == null || userType == null || username == null) {
log.warn("JWT token缺少必要claims信息"); log.warn("JWT token缺少必要claims");
log.warn(" - userId: {}", userId); return unauthorized(exchange, "Invalid token payload");
log.warn(" - userType: {}", userType);
log.warn(" - username: {}", username);
log.info("跳过JWT认证继续处理请求");
return chain.filter(exchange);
} }
// 创建Spring Security的Authentication对象 UsernamePasswordAuthenticationToken authentication =
log.info("开始创建Authentication对象"); new UsernamePasswordAuthenticationToken(
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username,
username, // principal null,
null, // credentials (不需要密码) Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase())) );
);
// 设置认证详情包含JWT claims信息
authentication.setDetails(claims); authentication.setDetails(claims);
log.info("Authentication对象创建成功: {}", authentication);
log.info("Authentication是否已认证: {}", authentication.isAuthenticated());
log.info("Authentication的principal: {}", authentication.getPrincipal());
log.info("Authentication的authorities: {}", authentication.getAuthorities());
log.info("Authentication的details: {}", authentication.getDetails());
// 创建安全上下文并设置认证信息
SecurityContext securityContext = new SecurityContextImpl(authentication); SecurityContext securityContext = new SecurityContextImpl(authentication);
log.info("=== JWT token验证成功设置安全上下文 ==="); log.debug("JWT认证成功 - 用户: {} ({})", username, userType);
log.info("用户: {} (ID: {}, 类型: {}) 在路径: {} 上JWT验证通过",
username, userId, userType, path); // 把认证信息写入反应式安全上下文
// 将安全上下文设置到ReactiveSecurityContextHolder中
return chain.filter(exchange) return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))); .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
} catch (ExpiredJwtException e) {
log.debug("JWT token已过期: {}", e.getMessage());
return unauthorized(exchange, "Token expired");
} catch (JwtException e) {
log.warn("JWT token无效: {}", e.getMessage());
return unauthorized(exchange, "Invalid token");
} catch (Exception e) { } catch (Exception e) {
log.error("=== JWT token验证失败 ==="); log.error("JWT认证异常: {}", e.getMessage(), e);
log.error("请求路径: {}", path); return unauthorized(exchange, "Authentication error");
log.error("Authorization头: {}", authHeader);
log.error("错误详情: {}", e.getMessage(), e);
log.info("JWT认证失败继续处理请求未认证状态");
} }
}
log.info("JWT过滤器处理完成继续处理请求");
return chain.filter(exchange); /**
* 返回自定义的 401 JSON并补 CORS 头;写入响应后不再继续过滤链。
*/
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
var response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 给 401 也补 CORS 头,确保前端能拿到状态码/体,避免浏览器当成 Basic 401 处理
String origin = exchange.getRequest().getHeaders().getFirst(HttpHeaders.ORIGIN);
if (origin != null) {
response.getHeaders().set("Access-Control-Allow-Origin", origin);
response.getHeaders().set("Access-Control-Allow-Credentials", "true");
response.getHeaders().add("Vary", "Origin");
}
// 不返回 WWW-Authenticate避免浏览器 Basic 弹窗
response.getHeaders().remove(HttpHeaders.WWW_AUTHENTICATE);
String body = "{\"code\":401,\"msg\":\"" + escapeJson(message) + "\"}";
var buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
private static String escapeJson(String s) {
return s == null ? "" : s.replace("\\", "\\\\").replace("\"", "\\\"");
} }
} }

View File

@@ -155,30 +155,20 @@ public class JwtService {
} }
public io.jsonwebtoken.Claims parse(String token) { public io.jsonwebtoken.Claims parse(String token) {
log.info("=== 开始解析JWT token ===");
log.info("token长度: {} 字符", token.length());
log.debug("完整token: {}", token);
try { try {
log.info("开始验证JWT签名"); return Jwts.parserBuilder()
io.jsonwebtoken.Claims claims = Jwts.parserBuilder()
.setSigningKey(key) .setSigningKey(key)
.build() .build()
.parseClaimsJws(token) .parseClaimsJws(token)
.getBody(); .getBody();
} catch (io.jsonwebtoken.ExpiredJwtException e) {
log.info("=== JWT token解析成功 ==="); log.debug("JWT token已过期");
log.info("解析结果:"); throw e;
log.info(" - subject: {}", claims.getSubject()); } catch (io.jsonwebtoken.JwtException e) {
log.info(" - issuedAt: {}", claims.getIssuedAt()); log.debug("JWT token无效: {}", e.getMessage());
log.info(" - expiration: {}", claims.getExpiration()); throw e;
log.info(" - 所有claims: {}", claims);
return claims;
} catch (Exception e) { } catch (Exception e) {
log.error("=== JWT token解析失败 ==="); log.error("JWT解析异常: {}", e.getMessage());
log.error("token: {}", token);
log.error("错误详情: {}", e.getMessage(), e);
throw e; throw e;
} }
} }

View File

@@ -14,8 +14,10 @@ import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsConfigurationSource;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@Configuration @Configuration
@@ -28,98 +30,62 @@ public class SecurityConfig {
@Bean @Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
log.info("=== 开始配置Spring Security安全链 ==="); return http
SecurityWebFilterChain chain = http
.csrf(ServerHttpSecurity.CsrfSpec::disable) .csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(cors -> {}) .cors(cors -> cors.configurationSource(corsConfigurationSource()))
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.securityContextRepository(securityContextRepository()) .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authenticationManager(authenticationManager()) .authenticationManager(authenticationManager())
.authorizeExchange(ex -> ex .authorizeExchange(ex -> ex
.pathMatchers("/actuator/**").permitAll() .pathMatchers("/actuator/**").permitAll()
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允许所有CORS预检请求 .pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll() .pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll() .pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/status").permitAll()
.pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问 .pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll() // 用户端轮询登录接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll() // 二维码获取接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll()
.pathMatchers(HttpMethod.HEAD, "/api/link/qr/**").permitAll() // 二维码HEAD请求公开访问 .pathMatchers(HttpMethod.HEAD, "/api/link/qr/**").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/image/**").permitAll() // 图片访问接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/image/**").permitAll()
.pathMatchers(HttpMethod.HEAD, "/api/link/image/**").permitAll() // 图片HEAD请求公开访问 .pathMatchers(HttpMethod.HEAD, "/api/link/image/**").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll() // 游戏界面数据接口,公开访问 .pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll()
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证 .pathMatchers("/api/link/**").authenticated()
.anyExchange().permitAll() // 其他接口后续再收紧 .anyExchange().permitAll()
) )
// 关键将JWT过滤器集成到Security过滤链中放在AUTHENTICATION位置
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build(); .build();
log.info("=== Spring Security安全链配置完成 ===");
log.info("安全配置详情:");
log.info(" - CSRF: 已禁用");
log.info(" - CORS: 已启用");
log.info(" - HTTP Basic: 已禁用");
log.info(" - Form Login: 已禁用");
log.info(" - JWT过滤器: 已集成到Security链中 (AUTHENTICATION位置)");
log.info(" - 路径权限配置:");
log.info(" * /actuator/** -> 允许所有");
log.info(" * OPTIONS /** -> 允许所有 (CORS预检请求)");
log.info(" * POST /api/auth/login -> 允许所有");
log.info(" * GET /api/auth/me -> 允许所有");
log.info(" * GET /api/link/status -> 允许所有 (用户端公开接口)");
log.info(" * POST /api/link/select-region -> 允许所有 (用户端公开接口)");
log.info(" * GET /api/link/poll-login -> 允许所有 (用户端公开接口)");
log.info(" * GET /api/link/qr/** -> 允许所有 (二维码获取接口)");
log.info(" * HEAD /api/link/qr/** -> 允许所有 (二维码HEAD请求)");
log.info(" * GET /api/link/image/** -> 允许所有 (图片访问接口)");
log.info(" * HEAD /api/link/image/** -> 允许所有 (图片HEAD请求)");
log.info(" * GET /api/link/*/game-interface -> 允许所有 (游戏界面数据接口)");
log.info(" * /api/link/** -> 需要认证");
log.info(" * 其他路径 -> 允许所有");
return chain;
} }
@Bean @Bean
public ReactiveAuthenticationManager authenticationManager() { public ReactiveAuthenticationManager authenticationManager() {
log.info("创建JWT认证管理器");
return authentication -> { return authentication -> {
log.info("=== JWT认证管理器开始处理认证 ===");
log.info("认证对象: {}", authentication);
if (authentication instanceof UsernamePasswordAuthenticationToken) { if (authentication instanceof UsernamePasswordAuthenticationToken) {
UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication; UsernamePasswordAuthenticationToken auth = (UsernamePasswordAuthenticationToken) authentication;
log.info("处理UsernamePasswordAuthenticationToken认证");
log.info("Principal: {}", auth.getPrincipal());
log.info("Credentials: {}", auth.getCredentials());
log.info("Authorities: {}", auth.getAuthorities());
log.info("Details: {}", auth.getDetails());
log.info("是否已认证: {}", auth.isAuthenticated());
// 如果已经通过JWT过滤器认证直接返回
if (auth.isAuthenticated()) { if (auth.isAuthenticated()) {
log.info("认证对象已经通过验证,返回成功");
return Mono.just(auth); return Mono.just(auth);
} }
} }
log.info("认证对象未通过验证,返回失败");
return Mono.empty(); return Mono.empty();
}; };
} }
@Bean @Bean
public ServerSecurityContextRepository securityContextRepository() { public CorsConfigurationSource corsConfigurationSource() {
log.info("创建安全上下文仓库"); CorsConfiguration configuration = new CorsConfiguration();
return new WebSessionServerSecurityContextRepository(); configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
} }
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
log.info("创建BCrypt密码编码器");
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
} }