实现JWT身份认证机制,新增JWT认证过滤器和服务,更新链接生成接口以支持JWT验证,删除旧的用户控制器,添加JWT认证文档,增强错误处理和日志记录。

This commit is contained in:
zyh
2025-08-25 21:26:16 +08:00
parent 3f01d8590a
commit 7317866f98
54 changed files with 551 additions and 163 deletions

View File

@@ -0,0 +1,123 @@
package com.gameplatform.server.security;
import io.jsonwebtoken.Claims;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
@Component
public class JwtAuthenticationFilter implements WebFilter {
private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
private final JwtService jwtService;
public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public Mono<Void> 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认证继续处理请求");
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);
try {
log.info("开始解析JWT token");
Claims claims = jwtService.parse(token);
log.info("JWT token解析成功");
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);
}
// 创建Spring Security的Authentication对象
log.info("开始创建Authentication对象");
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, // principal
null, // credentials (不需要密码)
Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.toUpperCase()))
);
// 设置认证详情包含JWT 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);
log.info("=== JWT token验证成功设置安全上下文 ===");
log.info("用户: {} (ID: {}, 类型: {}) 在路径: {} 上JWT验证通过",
username, userId, userType, path);
// 将安全上下文设置到ReactiveSecurityContextHolder中
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
} catch (Exception e) {
log.error("=== JWT token验证失败 ===");
log.error("请求路径: {}", path);
log.error("Authorization头: {}", authHeader);
log.error("错误详情: {}", e.getMessage(), e);
log.info("JWT认证失败继续处理请求未认证状态");
}
log.info("JWT过滤器处理完成继续处理请求");
return chain.filter(exchange);
}
}

View File

@@ -27,9 +27,14 @@ public class JwtService {
byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes();
this.key = Keys.hmacShaKeyFor(bytes);
this.accessTokenMinutes = accessTokenMinutes;
log.info("JWT服务初始化完成 - 密钥长度: {} bytes, token过期时间: {} 分钟", bytes.length, accessTokenMinutes);
}
public String generateToken(String subject, String userType, Long userId, String username, Map<String, Object> extra) {
log.info("=== 开始生成JWT token ===");
log.info("生成参数: subject={}, userType={}, userId={}, username={}, extra={}",
subject, userType, userId, username, extra);
Instant now = Instant.now();
var builder = Jwts.builder()
.setSubject(subject)
@@ -41,14 +46,44 @@ public class JwtService {
if (extra != null) {
extra.forEach(builder::claim);
}
log.info("JWT builder配置完成开始签名");
String token = builder.signWith(key, SignatureAlgorithm.HS256).compact();
if (log.isDebugEnabled()) {
log.debug("JWT generated subject={}, userType={}, userId={}, username={} expInMin={}", subject, userType, userId, username, accessTokenMinutes);
}
log.info("=== JWT token生成成功 ===");
log.info("token长度: {} 字符", token.length());
log.info("过期时间: {} 分钟后", accessTokenMinutes);
log.info("完整token: {}", token);
return token;
}
public io.jsonwebtoken.Claims parse(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
log.info("=== 开始解析JWT token ===");
log.info("token长度: {} 字符", token.length());
log.debug("完整token: {}", token);
try {
log.info("开始验证JWT签名");
io.jsonwebtoken.Claims claims = 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 (Exception e) {
log.error("=== JWT token解析失败 ===");
log.error("token: {}", token);
log.error("错误详情: {}", e.getMessage(), e);
throw e;
}
}
}

View File

@@ -1,37 +1,107 @@
package com.gameplatform.server.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
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 reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
log.info("=== 开始配置Spring Security安全链 ===");
SecurityWebFilterChain chain = http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(cors -> {})
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.securityContextRepository(securityContextRepository())
.authenticationManager(authenticationManager())
.authorizeExchange(ex -> ex
.pathMatchers("/actuator/**").permitAll()
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
.pathMatchers(HttpMethod.GET, "/api/auth/me").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(" * POST /api/auth/login -> 允许所有");
log.info(" * GET /api/auth/me -> 允许所有");
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();
}
@Bean
public PasswordEncoder passwordEncoder() {
log.info("创建BCrypt密码编码器");
return new BCryptPasswordEncoder();
}
}