feat: 优化JWT认证逻辑,增强错误处理和CORS支持
This commit is contained in:
@@ -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 token,token长度: {}", 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("\"", "\\\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user