From 74e3a23b33bbea93fc47785fb88cb41762086568 Mon Sep 17 00:00:00 2001 From: zyh Date: Fri, 29 Aug 2025 22:32:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96JWT=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=92=8CCORS=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/JwtAuthenticationFilter.java | 148 +++++++++--------- .../server/security/JwtService.java | 26 +-- .../server/security/SecurityConfig.java | 92 ++++------- 3 files changed, 109 insertions(+), 157 deletions(-) diff --git a/src/main/java/com/gameplatform/server/security/JwtAuthenticationFilter.java b/src/main/java/com/gameplatform/server/security/JwtAuthenticationFilter.java index fa76b82..a19b4bd 100644 --- a/src/main/java/com/gameplatform/server/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/gameplatform/server/security/JwtAuthenticationFilter.java @@ -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 filter(ServerWebExchange exchange, WebFilterChain chain) { - String path = exchange.getRequest().getPath().value(); - String method = exchange.getRequest().getMethod().name(); - - log.info("=== JWT过滤器开始处理请求 ==="); - log.info("请求路径: {}, 请求方法: {}", path, method); - - String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); - log.info("Authorization头: {}", authHeader != null ? authHeader.substring(0, Math.min(20, authHeader.length())) + "..." : "null"); - - if (authHeader == null) { - log.info("未找到Authorization头,跳过JWT认证,继续处理请求"); + final var request = exchange.getRequest(); + final String path = request.getPath().value(); + final String method = request.getMethodValue(); + + log.debug("JWT过滤器处理请求: {} {}", method, path); + + final String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + // 没有 Authorization:不做认证,交给后续链(匿名访问或由安全链控制) + if (authHeader == null || !authHeader.startsWith("Bearer ")) { return chain.filter(exchange); } - - if (!authHeader.startsWith("Bearer ")) { - log.warn("Authorization头格式无效,期望格式: 'Bearer ',实际: {}", authHeader); - log.info("跳过JWT认证,继续处理请求"); - return chain.filter(exchange); - } - - String token = authHeader.substring(7); - log.info("开始处理JWT token,token长度: {}", 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); - - // 将安全上下文设置到ReactiveSecurityContextHolder中 + + log.debug("JWT认证成功 - 用户: {} ({})", username, userType); + + // 把认证信息写入反应式安全上下文 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"); } - - log.info("JWT过滤器处理完成,继续处理请求"); - return chain.filter(exchange); + } + + /** + * 返回自定义的 401 JSON,并补 CORS 头;写入响应后不再继续过滤链。 + */ + private Mono 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("\"", "\\\""); } } diff --git a/src/main/java/com/gameplatform/server/security/JwtService.java b/src/main/java/com/gameplatform/server/security/JwtService.java index f873cc1..cba50a3 100644 --- a/src/main/java/com/gameplatform/server/security/JwtService.java +++ b/src/main/java/com/gameplatform/server/security/JwtService.java @@ -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; } } diff --git a/src/main/java/com/gameplatform/server/security/SecurityConfig.java b/src/main/java/com/gameplatform/server/security/SecurityConfig.java index b4fdf9e..b35cfbf 100644 --- a/src/main/java/com/gameplatform/server/security/SecurityConfig.java +++ b/src/main/java/com/gameplatform/server/security/SecurityConfig.java @@ -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(); } }