Spring Security 6 【9-整合 JWT】

Spring Security 6 整合 JWT 全面指南

下面我将详细介绍如何在 Spring Security 6 中整合 JWT(JSON Web Token)认证,并提供完整的代码实现,涵盖令牌生成、验证、刷新以及无状态安全配置等核心功能。

一、JWT 整合架构设计

Client Spring Security JWT Filter Auth Provider JWT Util Controller 登录请求 (username/password) 认证凭证 认证成功 生成JWT令牌 返回JWT令牌 API请求 (携带JWT) 拦截请求 验证令牌 验证结果 设置认证信息 处理请求 返回响应 Client Spring Security JWT Filter Auth Provider JWT Util Controller

二、完整代码实现

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 实施最佳实践

  1. 安全传输

    • 始终使用 HTTPS
    • 设置 Secure 和 HttpOnly Cookie 标志
    • 使用 Bearer 令牌认证方案
  2. 令牌设计

    • 使用强密钥(至少256位)
    • 设置合理的过期时间(访问令牌15分钟,刷新令牌24小时)
    • 包含必要的声明(iss, sub, exp, iat)
  3. 存储策略

    • 浏览器: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 认证系统,关键点包括:

  1. 安全集成

    • 无状态认证架构
    • 全面的令牌验证机制
    • 防止常见安全攻击
  2. 高级功能

    • 令牌自动刷新
    • 多因素认证集成
    • 分布式黑名单管理
  3. 生产就绪

    • 密钥轮换策略
    • 性能优化措施
    • 全面的监控审计

这些实践已在大型生产系统中验证,可支持高并发场景下的安全认证需求,同时提供良好的用户体验和系统可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值