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;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
@@ -15,6 +19,7 @@ import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
@Component
@@ -29,95 +34,86 @@ public class JwtAuthenticationFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getPath().value();
String method = exchange.getRequest().getMethod().name();
final var request = exchange.getRequest();
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);
log.info("Authorization头: {}", authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "null");
final String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
log.info("未找到Authorization头跳过JWT认证继续处理请求");
// 没有 Authorization不做认证交给后续链匿名访问或由安全链控制
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return chain.filter(exchange);
}
if (!authHeader.startsWith("Bearer ")) {
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);
final String token = authHeader.substring(7);
try {
log.info("开始解析JWT 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 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) {
log.warn("JWT token缺少必要claims信息");
log.warn(" - userId: {}", userId);
log.warn(" - userType: {}", userType);
log.warn(" - username: {}", username);
log.info("跳过JWT认证继续处理请求");
return chain.filter(exchange);
log.warn("JWT token缺少必要claims");
return unauthorized(exchange, "Invalid token payload");
}
// 创建Spring Security的Authentication对象
log.info("开始创建Authentication对象");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, // principal
null, // credentials (不需要密码)
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
);
// 设置认证详情包含JWT claims信息
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
);
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);
log.info("=== JWT token验证成功设置安全上下文 ===");
log.info("用户: {} (ID: {}, 类型: {}) 在路径: {} 上JWT验证通过",
username, userId, userType, path);
log.debug("JWT认证成功 - 用户: {} ({})", username, userType);
// 将安全上下文设置到ReactiveSecurityContextHolder中
// 把认证信息写入反应式安全上下文
return chain.filter(exchange)
.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) {
log.error("=== JWT token验证失败 ===");
log.error("请求路径: {}", path);
log.error("Authorization头: {}", authHeader);
log.error("错误详情: {}", e.getMessage(), e);
log.info("JWT认证失败继续处理请求未认证状态");
log.error("JWT认证异常: {}", e.getMessage(), e);
return unauthorized(exchange, "Authentication error");
}
}
/**
* 返回自定义的 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");
}
log.info("JWT过滤器处理完成继续处理请求");
return chain.filter(exchange);
// 不返回 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) {
log.info("=== 开始解析JWT token ===");
log.info("token长度: {} 字符", token.length());
log.debug("完整token: {}", token);
try {
log.info("开始验证JWT签名");
io.jsonwebtoken.Claims claims = Jwts.parserBuilder()
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
log.info("=== JWT token解析成功 ===");
log.info("解析结果:");
log.info(" - subject: {}", claims.getSubject());
log.info(" - issuedAt: {}", claims.getIssuedAt());
log.info(" - expiration: {}", claims.getExpiration());
log.info(" - 所有claims: {}", claims);
return claims;
} catch (io.jsonwebtoken.ExpiredJwtException e) {
log.debug("JWT token已过期");
throw e;
} catch (io.jsonwebtoken.JwtException e) {
log.debug("JWT token无效: {}", e.getMessage());
throw e;
} catch (Exception e) {
log.error("=== JWT token解析失败 ===");
log.error("token: {}", token);
log.error("错误详情: {}", e.getMessage(), e);
log.error("JWT解析异常: {}", e.getMessage());
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.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
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;
@Configuration
@@ -28,98 +30,62 @@ public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
log.info("=== 开始配置Spring Security安全链 ===");
SecurityWebFilterChain chain = http
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(cors -> {})
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.securityContextRepository(securityContextRepository())
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authenticationManager(authenticationManager())
.authorizeExchange(ex -> ex
.pathMatchers("/actuator/**").permitAll()
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 允许所有CORS预检请求
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll() // 用户端获取链接状态接口,公开访问
.pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll() // 用户端选区接口,公开访问
.pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll() // 用户端轮询登录接口,公开访问
.pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll() // 二维码获取接口,公开访问
.pathMatchers(HttpMethod.HEAD, "/api/link/qr/**").permitAll() // 二维码HEAD请求公开访问
.pathMatchers(HttpMethod.GET, "/api/link/image/**").permitAll() // 图片访问接口,公开访问
.pathMatchers(HttpMethod.HEAD, "/api/link/image/**").permitAll() // 图片HEAD请求公开访问
.pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll() // 游戏界面数据接口,公开访问
.pathMatchers("/api/link/**").authenticated() // 其他链接接口需要认证
.anyExchange().permitAll() // 其他接口后续再收紧
.pathMatchers(HttpMethod.GET, "/api/link/status").permitAll()
.pathMatchers(HttpMethod.POST, "/api/link/select-region").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/poll-login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/qr/**").permitAll()
.pathMatchers(HttpMethod.HEAD, "/api/link/qr/**").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/image/**").permitAll()
.pathMatchers(HttpMethod.HEAD, "/api/link/image/**").permitAll()
.pathMatchers(HttpMethod.GET, "/api/link/*/game-interface").permitAll()
.pathMatchers("/api/link/**").authenticated()
.anyExchange().permitAll()
)
// 关键将JWT过滤器集成到Security过滤链中放在AUTHENTICATION位置
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.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
public ReactiveAuthenticationManager authenticationManager() {
log.info("创建JWT认证管理器");
return authentication -> {
log.info("=== JWT认证管理器开始处理认证 ===");
log.info("认证对象: {}", authentication);
if (authentication instanceof UsernamePasswordAuthenticationToken) {
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()) {
log.info("认证对象已经通过验证,返回成功");
return Mono.just(auth);
}
}
log.info("认证对象未通过验证,返回失败");
return Mono.empty();
};
}
@Bean
public ServerSecurityContextRepository securityContextRepository() {
log.info("创建安全上下文仓库");
return new WebSessionServerSecurityContextRepository();
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
log.info("创建BCrypt密码编码器");
return new BCryptPasswordEncoder();
}
}