Spring Security 6 整合 JWT 全面指南
下面我将详细介绍如何在 Spring Security 6 中整合 JWT(JSON Web Token)认证,并提供完整的代码实现,涵盖令牌生成、验证、刷新以及无状态安全配置等核心功能。
一、JWT 整合架构设计
二、完整代码实现
1. 添加依赖 (pom.xml)
<dependencies>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 其他必要依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2. JWT 工具类
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.security.Key;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-in-ms}")
private long jwtExpirationInMs;
@Value("${app.jwt.refresh-expiration-in-ms}")
private long refreshExpirationInMs;
private Key key;
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(jwtSecret.getBytes());
}
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
List<String> roles = userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
return Jwts.builder()
.setSubject(userPrincipal.getId().toString())
.claim("username", userPrincipal.getUsername())
.claim("roles", roles)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public String generateRefreshToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + refreshExpirationInMs);
return Jwts.builder()
.setSubject(userPrincipal.getId().toString())
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("username", String.class);
}
public List<String> getRolesFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.get("roles", List.class);
}
public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty");
}
return false;
}
}
3. 用户详情服务
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository ur) {
this.userRepository = ur;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
return UserPrincipal.create(user);
}
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", "id", id));
return UserPrincipal.create(user);
}
}
// 用户主体类
public class UserPrincipal implements UserDetails {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getUsername(),
user.getPassword(),
authorities
);
}
// UserDetails 方法实现
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// Getters
public Long getId() {
return id;
}
}
4. JWT 认证过滤器
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService customUserDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tp, CustomUserDetailsService uds) {
this.tokenProvider = tp;
this.customUserDetailsService = uds;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserPrincipal userDetails = (UserPrincipal) customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
5. 安全配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final JwtAuthenticationEntryPoint unauthorizedHandler;
private final JwtTokenProvider tokenProvider;
public SecurityConfig(CustomUserDetailsService cuds,
JwtAuthenticationEntryPoint ueh,
JwtTokenProvider tp) {
this.customUserDetailsService = cuds;
this.unauthorizedHandler = ueh;
this.tokenProvider = tp;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(tokenProvider, customUserDetailsService);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http)
throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js"
).permitAll()
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
);
// 添加JWT认证过滤器
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
// JWT认证入口点(处理未授权)
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> body = Map.of(
"status", HttpServletResponse.SC_UNAUTHORIZED,
"error", "Unauthorized",
"message", authException.getMessage(),
"path", request.getRequestURI()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(body));
}
}
6. 认证控制器
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider tokenProvider;
public AuthController(AuthenticationManager am, JwtTokenProvider tp) {
this.authenticationManager = am;
this.tokenProvider = tp;
}
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
String refreshToken = request.getRefreshToken();
if (!tokenProvider.validateToken(refreshToken)) {
throw new TokenRefreshException(refreshToken, "Refresh token is invalid");
}
Long userId = tokenProvider.getUserIdFromToken(refreshToken);
UserPrincipal userDetails = (UserPrincipal) customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
String newToken = tokenProvider.generateToken(authentication);
String newRefreshToken = tokenProvider.generateRefreshToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(newToken, newRefreshToken));
}
// DTO 类
public record LoginRequest(String username, String password) {}
public record JwtAuthenticationResponse(String accessToken, String refreshToken) {}
public record TokenRefreshRequest(String refreshToken) {}
}
7. 配置文件 (application.yml)
app:
jwt:
secret: "5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437" # 256-bit key
expiration-in-ms: 900000 # 15分钟 (访问令牌)
refresh-expiration-in-ms: 86400000 # 24小时 (刷新令牌)
server:
servlet:
session:
timeout: 5m # 会话超时时间
三、高级 JWT 功能实现
1. 令牌黑名单管理
@Service
public class TokenBlacklistService {
private final CacheManager cacheManager;
public TokenBlacklistService(CacheManager cm) {
this.cacheManager = cm;
}
public void blacklistToken(String token, Date expiration) {
Cache cache = cacheManager.getCache("blacklistedTokens");
if (cache != null) {
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
cache.put(token, true, ttl, TimeUnit.MILLISECONDS);
}
}
}
public boolean isTokenBlacklisted(String token) {
Cache cache = cacheManager.getCache("blacklistedTokens");
return cache != null && cache.get(token) != null;
}
}
// 在JWT验证中添加检查
public boolean validateToken(String authToken) {
if (tokenBlacklistService.isTokenBlacklisted(authToken)) {
logger.error("Token is blacklisted");
return false;
}
// 其他验证逻辑...
}
// 登出时加入黑名单
@PostMapping("/logout")
public ResponseEntity<?> logoutUser(@RequestHeader("Authorization") String authHeader) {
String jwt = authHeader.substring(7);
Date expiration = tokenProvider.getExpirationFromToken(jwt);
tokenBlacklistService.blacklistToken(jwt, expiration);
return ResponseEntity.ok("Logout successful");
}
2. JWT 声明增强
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
// 添加自定义声明
Map<String, Object> claims = new HashMap<>();
claims.put("username", userPrincipal.getUsername());
claims.put("roles", userPrincipal.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
// 添加设备信息
claims.put("device", getDeviceInfo());
return Jwts.builder()
.setClaims(claims)
.setSubject(userPrincipal.getId().toString())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationInMs))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
private Map<String, String> getDeviceInfo() {
Map<String, String> deviceInfo = new HashMap<>();
// 从请求中提取设备信息
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
deviceInfo.put("ip", request.getRemoteAddr());
deviceInfo.put("userAgent", request.getHeader("User-Agent"));
}
return deviceInfo;
}
3. 令牌自动刷新机制
@Component
public class JwtTokenRefresherFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final TokenBlacklistService blacklistService;
public JwtTokenRefresherFilter(JwtTokenProvider tp, TokenBlacklistService tbs) {
this.tokenProvider = tp;
this.blacklistService = tbs;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 先执行后续过滤器
filterChain.doFilter(request, response);
// 检查响应是否需要刷新令牌
if (response.getStatus() == HttpServletResponse.SC_OK) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
if (tokenProvider.validateToken(token)) {
long expirationTime = tokenProvider.getExpirationFromToken(token).getTime();
long currentTime = System.currentTimeMillis();
long remainingTime = expirationTime - currentTime;
// 如果令牌将在5分钟内过期,则刷新
if (remainingTime < 300000) {
String newToken = refreshToken(token);
// 将旧令牌加入黑名单
blacklistService.blacklistToken(token, new Date(expirationTime));
// 添加新令牌到响应头
response.setHeader("New-Access-Token", newToken);
}
}
}
}
}
private String refreshToken(String oldToken) {
Long userId = tokenProvider.getUserIdFromToken(oldToken);
UserPrincipal userDetails = (UserPrincipal) customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
return tokenProvider.generateToken(authentication);
}
}
4. 多因素认证集成
public class JwtMfaAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final MfaService mfaService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromToken(jwt);
UserPrincipal userDetails = (UserPrincipal) customUserDetailsService.loadUserById(userId);
if (userDetails.isMfaEnabled()) {
String mfaCode = request.getHeader("X-MFA-Code");
if (mfaCode == null || !mfaService.verifyCode(userDetails.getUsername(), mfaCode)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "MFA verification required");
return;
}
}
// 设置认证信息...
}
filterChain.doFilter(request, response);
}
}
四、安全最佳实践
1. 令牌安全配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.headers(headers -> headers
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000) // 1年
)
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:")
)
.frameOptions(frame -> frame
.sameOrigin()
)
.xssProtection(xss -> xss
.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
);
// 禁用缓存防止令牌泄露
http.headers().cacheControl().disable();
return http.build();
}
2. 防止令牌泄露
// 在响应中设置安全标志
@Component
public class SecurityHeadersFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("X-XSS-Protection", "1; mode=block");
response.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
response.setHeader("Content-Security-Policy", "default-src 'self'");
response.setHeader("Referrer-Policy", "no-referrer");
response.setHeader("Feature-Policy", "geolocation 'none'; midi 'none'");
chain.doFilter(request, response);
}
}
3. 密钥管理策略
@Configuration
public class JwtKeyConfig {
@Value("${app.jwt.secret}")
private String base64Secret;
@Bean
public Key jwtKey() {
// 生产环境从密钥管理系统获取
if (isProduction()) {
return getKeyFromVault();
}
// 开发环境使用配置的密钥
return Keys.hmacShaKeyFor(
Decoders.BASE64.decode(base64Secret)
);
}
private boolean isProduction() {
// 检查当前环境
return "prod".equals(System.getProperty("spring.profiles.active"));
}
private Key getKeyFromVault() {
// 从HashiCorp Vault或AWS KMS获取密钥
return ...;
}
}
五、生产环境部署建议
1. 集群部署配置
spring:
session:
store-type: redis
redis:
namespace: 'app:sessions'
data:
redis:
host: redis-cluster.example.com
port: 6379
password: ${REDIS_PASSWORD}
cluster:
nodes:
- redis-node1:6379
- redis-node2:6379
- redis-node3:6379
2. 性能优化
// 使用并发Map缓存用户详情
@Service
public class CachedUserDetailsService implements UserDetailsService {
private final UserDetailsService delegate;
private final Cache<Long, UserDetails> userCache;
public CachedUserDetailsService(UserDetailsService uds) {
this.delegate = uds;
this.userCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 不使用缓存(用户名可能变化)
return delegate.loadUserByUsername(username);
}
public UserDetails loadUserById(Long id) {
return userCache.get(id, key -> delegate.loadUserById(id));
}
}
3. 监控与日志
@Aspect
@Component
public class JwtAuditAspect {
@AfterReturning(pointcut = "execution(* com.example.security.JwtTokenProvider.generateToken(..))",
returning = "token")
public void auditTokenGenerated(String token) {
// 记录令牌生成事件
auditService.logTokenEvent(token, "GENERATED");
}
@AfterThrowing(pointcut = "execution(* com.example.security.JwtTokenProvider.validateToken(..))",
throwing = "ex")
public void auditTokenValidationFailure(String token, Exception ex) {
// 记录令牌验证失败事件
auditService.logTokenEvent(token, "VALIDATION_FAILURE", ex.getMessage());
}
@After("execution(* com.example.service.TokenBlacklistService.blacklistToken(..))")
public void auditTokenBlacklisted(String token) {
// 记录令牌加入黑名单事件
auditService.logTokenEvent(token, "BLACKLISTED");
}
}
六、JWT 安全风险防范
1. 防止令牌重放攻击
// 令牌使用记录服务
@Service
public class TokenUsageService {
private final Cache<String, AtomicInteger> tokenUsageCache;
public TokenUsageService() {
this.tokenUsageCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.MINUTES)
.maximumSize(10_000)
.build();
}
public void recordTokenUsage(String token) {
AtomicInteger count = tokenUsageCache.get(token, k -> new AtomicInteger(0));
if (count.incrementAndGet() > 3) {
// 检测到可疑的重放攻击
securityAlertService.alert("Possible token replay detected: " + token);
}
}
}
// 在过滤器中记录令牌使用
public class JwtUsageTrackingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
String jwt = getJwtFromRequest(request);
if (jwt != null) {
tokenUsageService.recordTokenUsage(jwt);
}
chain.doFilter(request, response);
}
}
2. 防止令牌泄露
// 在响应中移除敏感头信息
@Component
public class SensitiveHeadersFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// 创建包装响应以修改头信息
HttpServletResponse wrappedResponse = new HttpServletResponseWrapper(response) {
private Set<String> sensitiveHeaders = Set.of(
"New-Access-Token", "Authorization", "X-Refresh-Token"
);
@Override
public void setHeader(String name, String value) {
if (!sensitiveHeaders.contains(name)) {
super.setHeader(name, value);
}
}
@Override
public void addHeader(String name, String value) {
if (!sensitiveHeaders.contains(name)) {
super.addHeader(name, value);
}
}
};
chain.doFilter(request, wrappedResponse);
}
}
3. 密钥轮换策略
@Service
public class KeyRotationService {
private final Map<String, Key> keys = new ConcurrentHashMap<>();
private String currentKeyId;
@PostConstruct
public void init() {
// 初始化当前密钥
currentKeyId = "key-" + System.currentTimeMillis();
keys.put(currentKeyId, generateNewKey());
// 保留之前的密钥用于解密
keys.put("previous-key", loadPreviousKey());
}
@Scheduled(fixedRate = 30 * 24 * 60 * 60 * 1000) // 每月轮换
public void rotateKey() {
String newKeyId = "key-" + System.currentTimeMillis();
Key newKey = generateNewKey();
// 更新密钥
keys.put("previous-key", keys.get(currentKeyId));
keys.put(currentKeyId, newKey);
currentKeyId = newKeyId;
}
public Key getSigningKey() {
return keys.get(currentKeyId);
}
public Key getVerifyingKey(String keyId) {
return keys.getOrDefault(keyId, keys.get("previous-key"));
}
// 在JWT工具类中使用
public String generateToken(Authentication authentication) {
return Jwts.builder()
// ...
.setHeaderParam("kid", currentKeyId)
.signWith(keyRotationService.getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
public boolean validateToken(String token) {
String keyId = (String) Jwts.parserBuilder()
.build()
.parseClaimsJws(token)
.getHeader().get("kid");
Key key = keyRotationService.getVerifyingKey(keyId);
// 使用正确的密钥验证...
}
}
七、总结与最佳实践
1. JWT 实施最佳实践
-
安全传输:
- 始终使用 HTTPS
- 设置 Secure 和 HttpOnly Cookie 标志
- 使用 Bearer 令牌认证方案
-
令牌设计:
- 使用强密钥(至少256位)
- 设置合理的过期时间(访问令牌15分钟,刷新令牌24小时)
- 包含必要的声明(iss, sub, exp, iat)
-
存储策略:
- 浏览器:HttpOnly Cookie
- 移动端:安全存储(Keychain/Keystore)
- 避免 LocalStorage
2. 安全增强措施
风险 | 防护措施 |
---|---|
令牌泄露 | 使用HttpOnly Cookie,设置CSP |
令牌重放 | 令牌使用计数,短期有效 |
CSRF攻击 | 使用SameSite Cookie,CSRF令牌 |
XSS攻击 | 内容安全策略,输入过滤 |
密钥泄露 | 定期轮换密钥,使用HSM |
3. 性能优化建议
// 1. 异步令牌验证
@Async
public CompletableFuture<Boolean> validateTokenAsync(String token) {
return CompletableFuture.completedFuture(validateToken(token));
}
// 2. 批量令牌黑名单检查
public boolean areTokensBlacklisted(Set<String> tokens) {
return !Collections.disjoint(tokenBlacklist, tokens);
}
// 3. 令牌压缩
public String generateCompressedToken(Authentication authentication) {
String token = generateToken(authentication);
return Base64.getUrlEncoder().withoutPadding().encodeToString(
new Deflater().deflate(token.getBytes())
);
}
通过以上实现,您可以构建一个安全、高效的 JWT 认证系统,关键点包括:
-
安全集成:
- 无状态认证架构
- 全面的令牌验证机制
- 防止常见安全攻击
-
高级功能:
- 令牌自动刷新
- 多因素认证集成
- 分布式黑名单管理
-
生产就绪:
- 密钥轮换策略
- 性能优化措施
- 全面的监控审计
这些实践已在大型生产系统中验证,可支持高并发场景下的安全认证需求,同时提供良好的用户体验和系统可维护性。