Spring Security 6.x 与 OAuth2/JWT 安全认证实战

Spring Security 6.x 深度实战:OAuth2 协议、JWT 签发验证、Spring Authorization Server、资源服务器、方法级安全与前后端分离架构。

概述

企业级应用的安全认证从传统的 Session-Cookie 模式逐步演进到无状态 JWT + OAuth2 架构。Spring Security 6.x 基于 Jakarta EE namespace,全面支持 Spring Boot 3.x,内置对 OAuth2 Resource Server、Spring Authorization Server(取代已停止维护的 Spring Security OAuth)的一等支持。本文系统讲解 RBAC/ABAC 设计、Spring Security 过滤器链、OAuth2 四大授权模式与 PKCE、Spring Authorization Server 配置、JWT 签发与验证、资源服务器搭建、方法级安全、前后端分离 CORS 方案以及常见攻击防御和生产调优。


1. 访问控制模型

1.1 RBAC 与 ABAC

RBAC(Role-Based Access Control)基于静态角色进行权限映射,适合组织架构清晰的业务系统。ABAC(Attribute-Based Access Control)基于动态属性(时间、地点、资源标签、用户等级)做细粒度决策,适合零信任架构和复杂政策引擎场景。

特性RBACABAC
决策维度角色-权限映射用户/资源/环境属性组合
复杂度低,易于维护高,需政策引擎
扩展性角色爆炸问题灵活,属性组合即可
典型场景B 端后台管理系统金融风控、零信任网关

在 Spring Security 中,RBAC 通过 authorityrole 表达,前缀 ROLE_ 区分角色与普通权限。ABAC 可通过自定义 PermissionEvaluator 或 SpEL 方法安全实现。


2. Spring Security 6 过滤器链架构

2.1 SecurityFilterChain 的核心设计

Spring Security 的执行完全围绕 SecurityFilterChain 展开。DelegatingFilterProxy(注册到 Servlet 容器)将请求委托给 FilterChainProxy,后者匹配并执行一条或多条 SecurityFilterChain

// SecurityConfig.java — 定义自定义过滤器链
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            // 匹配以 /api 开头的请求
            .securityMatcher("/api/**")
            // 禁用 Session,采用无状态 JWT 方案
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // 配置授权规则
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            )
            // 启用 JWT 资源服务器
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtDecoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            )
            // 禁用 CSRF(无状态 API 无需 CSRF Token)
            .csrf(csrf -> csrf.disable())
            // 配置异常处理
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
            );
        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        // 使用 JWK Set URI 动态获取公钥验证 JWT 签名
        return ReactiveJwtDecoders.fromIssuerLocation("http://auth-server:9000");
    }

    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        // 将 JWT 中的 claims 映射为 Spring Security 的 GrantedAuthority
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        authoritiesConverter.setAuthoritiesClaimName("roles");
        authoritiesConverter.setAuthorityPrefix("ROLE_");

        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        return converter;
    }
}

2.2 关键内置过滤器顺序

过滤器职责
OAuth2AuthorizationRequestRedirectFilter构造并重定向到授权服务器
AuthorizationFilterSpring Security 6 替代 FilterSecurityInterceptor,执行 authorizeHttpRequests
BearerTokenAuthenticationFilter从请求头解析 Bearer Token 并认证
ExceptionTranslationFilter捕获 AccessDeniedException / AuthenticationException 并处理

Spring Security 6 的重大变更是将方法级和请求级安全都统一到 AuthorizationFilter,基于 AuthorizationManager 接口执行决策。


3. OAuth2 授权模式与 PKCE

3.1 四大授权模式对比

模式适用场景Refresh Token安全风险
Authorization Code服务端 Web 应用支持低,配合 PKCE 可防御授权码拦截
PKCE单页应用(SPA)、移动端支持极低,无 Client Secret 也能安全
Client Credentials服务间调用(M2M)不支持低,依赖 Client Secret 安全存储
Device Code输入受限设备(智能电视、IoT)支持中,需轮询并限制用户码有效期

3.2 Authorization Code + PKCE 完整流程

// application.yml — 客户端配置示例(注册到授权服务器)
spring:
  security:
    oauth2:
      client:
        registration:
          my-oidc-client:
            provider: my-provider
            client-id: demo-client
            client-secret: "{noop}secret"  # 生产环境使用加密方式
            client-authentication-method: client_secret_basic
            authorization-grant-type: authorization_code
            redirect-uri: "http://localhost:8080/login/oauth2/code/my-oidc-client"
            scope:
              - openid
              - profile
              - email
              - read
              - write
        provider:
          my-provider:
            issuer-uri: http://auth-server:9000  # 自动发现 .well-known/openid-configuration

3.3 自定义授权服务器端点与 PKCE 校验

Spring Authorization Server 内置支持 PKCE。当客户端注册为 public client(无 secret)时必须使用 PKCE。code_challenge 采用 S256 变换,授权服务器在校换 Token 时验证 code_verifier

// OAuth2AuthorizationServerConfig.java — 授权服务器 Bean 配置
@Configuration
public class AuthorizationServerConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            // 自定义授权确认页面端点
            .authorizationEndpoint(authorizationEndpoint ->
                authorizationEndpoint.consentPage("/oauth2/consent")
            )
            // 启用 OIDC 1.0
            .oidc(Customizer.withDefaults());

        // 配置 CORS 与 CSRF(授权服务器端点通常需要特定处理)
        http.cors(Customizer.withDefaults());
        http.csrf(csrf -> csrf.ignoringRequestMatchers("/oauth2/**", "/oidc/**"));

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        // 使用数据库存储已注册客户端,支持动态注册
        JdbcRegisteredClientRepository repository = new JdbcRegisteredClientRepository(jdbcTemplate);

        // 示例:SPA 应用,public client,强制 PKCE
        RegisteredClient spaClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("spa-client")
            .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)  // 无 secret
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://localhost:3000/callback")
            .scope(OidcScopes.OPENID)
            .scope("read")
            .scope("write")
            .clientSettings(ClientSettings.builder()
                .requireProofKey(true)  // 强制 PKCE
                .requireAuthorizationConsent(true)  // 要求用户显式确认授权
                .build())
            .tokenSettings(TokenSettings.builder()
                .accessTokenTimeToLive(Duration.ofMinutes(15))
                .refreshTokenTimeToLive(Duration.ofDays(7))
                .build())
            .build();

        repository.save(spaClient);
        return repository;
    }
}

4. Spring Authorization Server 完整配置

4.1 JWK 与 Token 签发器

// JwtConfig.java — 配置非对称密钥对用于 JWT 签名
@Configuration
public class JwtConfig {

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        // 生成 RSA 密钥对(实际生产环境应从 HSM 或密钥管理服务加载)
        RSAKey rsaKey = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    private static RSAKey generateRsa() {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey)
            .privateKey(privateKey)
            .keyID(UUID.randomUUID().toString())  // kid 用于匹配密钥
            .build();
    }

    private static KeyPair generateRsaKey() {
        try {
            KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
            keyGen.initialize(2048);  // RSA-2048 是安全与性能的平衡点
            return keyGen.generateKeyPair();
        } catch (NoSuchAlgorithmException ex) {
            throw new IllegalStateException(ex);
        }
    }

    @Bean
    public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
        return new NimbusJwtEncoder(jwkSource);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        // 使用 JWKSource 创建解码器,支持密钥轮换
        Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
        jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
        jwsAlgs.addAll(JWSAlgorithm.Family.EC);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer("http://auth-server:9000");
        NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri("http://auth-server:9000/.well-known/jwks.json")
            .jwsAlgorithms(algs -> algs.addAll(jwsAlgs))
            .build();
        decoder.setJwtValidator(withIssuer);
        return decoder;
    }
}

4.2 授权服务信息端点与 Token 自定义

// OAuth2TokenCustomizerConfig.java — 向 JWT 中注入自定义声明
@Component
public class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    @Override
    public void customize(JwtEncodingContext context) {
        // 根据 Token 类型区分处理
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            Authentication principal = context.getPrincipal();
            if (principal instanceof UsernamePasswordAuthenticationToken auth) {
                // 注入自定义用户属性到 Access Token
                UserDetails user = (UserDetails) auth.getPrincipal();
                context.getClaims().claim("username", user.getUsername());
                context.getClaims().claim("roles", user.getAuthorities().stream()
                    .map(GrantedAuthority::getAuthority)
                    .toList());
                // 注入租户信息(多租户场景)
                context.getClaims().claim("tenant_id", resolveTenant(user));
            }
        }

        // Refresh Token 可注入设备指纹等元数据
        if (OAuth2TokenType.REFRESH_TOKEN.equals(context.getTokenType())) {
            HttpServletRequest request = getCurrentRequest();
            if (request != null) {
                context.getClaims().claim("client_ip", request.getRemoteAddr());
            }
        }
    }

    private String resolveTenant(UserDetails user) {
        // 从用户扩展属性解析租户
        return "T001";
    }

    private HttpServletRequest getCurrentRequest() {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        if (attrs instanceof ServletRequestAttributes servletAttrs) {
            return servletAttrs.getRequest();
        }
        return null;
    }
}

5. JWT 实战:JJWT 与 Spring Security 集成

5.1 JJWT 库签发与验证 Access/Refresh Token

// JwtTokenProvider.java — 使用 JJWT 0.12.x 版本 API
@Component
public class JwtTokenProvider {

    private final SecretKey accessKey;
    private final SecretKey refreshKey;
    private final long accessExpirationMs = 15 * 60 * 1000;   // 15 分钟
    private final long refreshExpirationMs = 7 * 24 * 60 * 60 * 1000; // 7 天

    public JwtTokenProvider(@Value("${jwt.access.secret}") String accessSecret,
                            @Value("${jwt.refresh.secret}") String refreshSecret) {
        // 使用 HMAC-SHA 算法生成安全密钥
        this.accessKey = Keys.hmacShaKeyFor(accessSecret.getBytes(StandardCharsets.UTF_8));
        this.refreshKey = Keys.hmacShaKeyFor(refreshSecret.getBytes(StandardCharsets.UTF_8));
    }

    public String generateAccessToken(String userId, String username, List<String> roles) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + accessExpirationMs);

        return Jwts.builder()
            .subject(userId)
            .claim("username", username)
            .claim("roles", roles)
            .claim("type", "access")
            .issuedAt(now)
            .expiration(expiry)
            .id(UUID.randomUUID().toString())  // jti 用于 Token 黑名单
            .signWith(accessKey, Jwts.SIG.HS256)
            .compact();
    }

    public String generateRefreshToken(String userId) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + refreshExpirationMs);

        return Jwts.builder()
            .subject(userId)
            .claim("type", "refresh")
            .issuedAt(now)
            .expiration(expiry)
            .signWith(refreshKey, Jwts.SIG.HS512)  // Refresh Token 使用更强的算法
            .compact();
    }

    public Claims parseAccessToken(String token) {
        return Jwts.parser()
            .verifyWith(accessKey)
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }

    public boolean validateAccessToken(String token) {
        try {
            Claims claims = parseAccessToken(token);
            return "access".equals(claims.get("type", String.class));
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SecurityException ex) {
            return false;
        }
    }
}

5.2 JWE(加密 Token)示例

在敏感场景中,需要对 JWT payload 加密以防止 claims 被读取。JWE 使用公钥加密,私钥解密。

// JweTokenProvider.java — JWT 加密实现
@Component
public class JweTokenProvider {

    private final RSAKey rsaKey;

    public JweTokenProvider() throws Exception {
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
        gen.initialize(2048);
        KeyPair pair = gen.generateKeyPair();
        this.rsaKey = new RSAKey.Builder((RSAPublicKey) pair.getPublic())
            .privateKey((RSAPrivateKey) pair.getPrivate())
            .build();
    }

    public String generateJweToken(String subject, Map<String, Object> claims) throws Exception {
        JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder()
            .subject(subject)
            .issueTime(new Date())
            .expirationTime(new Date(System.currentTimeMillis() + 900_000))
            .jwtID(UUID.randomUUID().toString())
            .build();

        // 将明文 JWT 作为 JWE payload
        SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), jwtClaims);
        signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey()));

        JWEObject jweObject = new JWEObject(
            new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
                .contentType("JWT")
                .build(),
            new Payload(signedJWT)
        );

        jweObject.encrypt(new RSAEncrypter(rsaKey.toRSAPublicKey()));
        return jweObject.serialize();
    }
}

6. 资源服务器搭建

6.1 纯资源服务器配置

微服务架构中,独立的资源服务器负责校验 Access Token 并将 JWT claims 转换为本地认证上下文。

// ResourceServerConfig.java — 资源服务器核心配置
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuerUri;

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/health", "/api/actuator/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/orders/**").hasAuthority("SCOPE_read")
                .requestMatchers(HttpMethod.POST, "/api/orders/**").hasAuthority("SCOPE_write")
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtDecoder(jwtDecoder())
                    .jwtAuthenticationConverter(grantedAuthoritiesExtractor())
                )
            )
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));

        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        // 默认解码器通过 issuer-uri 自动发现公钥
        return JwtDecoders.fromIssuerLocation(issuerUri);
    }

    @Bean
    public JwtAuthenticationConverter grantedAuthoritiesExtractor() {
        JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
        // 从 "scope" 或 "scp" claim 提取权限(默认行为)
        authoritiesConverter.setAuthorityPrefix("SCOPE_");

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
        // 将 sub claim 映射为 Principal 名称
        jwtAuthenticationConverter.setPrincipalClaimName("sub");
        return jwtAuthenticationConverter;
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000", "https://app.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-ID"));
        config.setExposedHeaders(List.of("X-Total-Count"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

7. 方法级安全(@PreAuthorize)

7.1 启用方法安全与 SpEL 表达式

// MethodSecurityConfig.java — 启用全局方法安全
@Configuration
@EnableMethodSecurity(
    prePostEnabled = true,       // 启用 @PreAuthorize / @PostAuthorize
    securedEnabled = true,       // 启用 @Secured
    jsr250Enabled = true         // 启用 @RolesAllowed
)
public class MethodSecurityConfig {

    // 注册自定义 PermissionEvaluator 支持 ABAC 模型
    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
        handler.setPermissionEvaluator(new DomainPermissionEvaluator());
        return handler;
    }
}

// PermissionEvaluator 实现
public class DomainPermissionEvaluator implements PermissionEvaluator {

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomain, Object permission) {
        if (authentication == null || !authentication.isAuthenticated()) {
            return false;
        }
        // 示例:判断用户是否拥有目标对象的权限
        String targetType = targetDomain.getClass().getSimpleName().toUpperCase();
        return checkPrivilege(authentication, targetType, permission.toString().toUpperCase());
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        // 基于 ID 和类型的权限校验
        return false; // 实际项目中从数据库查询 ACL
    }

    private boolean checkPrivilege(Authentication auth, String target, String perm) {
        return auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .anyMatch(a -> a.equals(target + ":" + perm));
    }
}

7.2 Service 层使用示例

@Service
public class OrderService {

    // 要求用户拥有 ORDER:READ 权限或 ADMIN 角色
    @PreAuthorize("hasAuthority('ORDER:READ') or hasRole('ADMIN')")
    public Order getOrderById(Long id) {
        return orderRepository.findById(id)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }

    // 使用 SpEL 表达式:只能查看自己的订单,管理员可查看全部
    @PostAuthorize("returnObject.customerId == authentication.name or hasRole('ADMIN')")
    public Order getOrderSecure(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    // 检查用户名是否与路径变量一致(ABAC 属性校验)
    @PreAuthorize("#username == authentication.name")
    public List<Order> getUserOrders(String username) {
        return orderRepository.findByCustomerUsername(username);
    }

    // 基于自定义 PermissionEvaluator 的域对象权限校验
    @PreAuthorize("hasPermission(#orderId, 'Order', 'DELETE')")
    public void deleteOrder(Long orderId) {
        orderRepository.deleteById(orderId);
    }

    // 过滤返回集合:只返回用户有权限查看的数据
    @PostFilter("hasPermission(filterObject, 'READ')")
    public List<Order> getAllVisibleOrders() {
        return orderRepository.findAll();
    }
}

8. 前后端分离与 Stateless JWT

8.1 无状态认证架构

// StatelessAuthFilter.java — 自定义无状态认证过滤器(用于非标准 JWT 场景)
@Component
public class StatelessAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;

    public StatelessAuthFilter(JwtTokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = resolveToken(request);
            if (StringUtils.hasText(jwt) && tokenProvider.validateAccessToken(jwt)) {
                Claims claims = tokenProvider.parseAccessToken(jwt);

                // 构建 Spring Security 上下文
                List<SimpleGrantedAuthority> authorities =
                    ((List<?>) claims.get("roles", List.class)).stream()
                        .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                        .toList();

                UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                        new UserPrincipal(claims.getSubject(), claims.get("username", String.class)),
                        null,
                        authorities
                    );

                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("无法设置用户认证上下文", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

8.2 安全上下文持有策略

// SecurityContextConfig.java — 配置异步场景下的 SecurityContext 传播
@Configuration
public class SecurityContextConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "taskExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(500);
        // 关键:将当前 SecurityContext 传播到子线程
        executor.setTaskDecorator(new SecurityContextTaskDecorator());
        executor.initialize();
        return executor;
    }

    public static class SecurityContextTaskDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable runnable) {
            SecurityContext context = SecurityContextHolder.getContext();
            return () -> {
                try {
                    SecurityContextHolder.setContext(context);
                    runnable.run();
                } finally {
                    SecurityContextHolder.clearContext();
                }
            };
        }
    }
}

9. 攻击防护:CSRF、XSS 与重放

9.1 CSRF 与 XSS 综合防御

// SecurityHeadersConfig.java — 安全响应头配置
@Configuration
public class SecurityHeadersConfig {

    @Bean
    public SecurityFilterChain securityHeadersChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                // HSTS 强制 HTTPS
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)
                )
                // XSS 过滤器(现代浏览器已弃用,建议配合 CSP)
                .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK))
                // Content Security Policy 防止 XSS
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives("default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;")
                )
                // 防止站点被嵌入 iframe(点击劫持)
                .frameOptions(frame -> frame.deny())
                // 防止 MIME 嗅探
                .contentTypeOptions(Customizer.withDefaults())
                // Referrer Policy
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
                )
            );
        return http.build();
    }
}

9.2 Token 黑名单与重放防护

// TokenBlacklistService.java — Redis 实现 Token 黑名单(登出/刷新场景)
@Service
public class TokenBlacklistService {

    private final StringRedisTemplate redisTemplate;
    private static final String BLACKLIST_PREFIX = "jwt:blacklist:";

    public TokenBlacklistService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void blacklistToken(String jti, long expirationMillis) {
        if (jti == null) return;
        // 黑名单有效期与 Token 剩余有效期一致
        redisTemplate.opsForValue().set(
            BLACKLIST_PREFIX + jti,
            "1",
            expirationMillis,
            TimeUnit.MILLISECONDS
        );
    }

    public boolean isBlacklisted(String jti) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + jti));
    }

    // 在过滤器中校验 Token 是否已被吊销
    public void validateTokenNotBlacklisted(String token) {
        Claims claims = tokenProvider.parseAccessToken(token);
        String jti = claims.getId();
        if (jti != null && isBlacklisted(jti)) {
            throw new TokenBlacklistedException("Token 已被吊销");
        }
    }
}

10. 生产环境实战建议

10.1 密钥与算法安全

生产环境绝不可使用对称密钥(如 HS256)通过客户端分发,应使用非对称密钥(RS256/ES256)并定期轮换。Spring Authorization Server 支持通过 jwk-set-uri 暴露公钥集,实现热轮换。

# application-prod.yml
spring:
  security:
    oauth2:
      authorizationserver:
        issuer: https://auth.example.com
      resourceserver:
        jwt:
          issuer-uri: https://auth.example.com
          # 启用允许时钟偏差的 NTP 容差
          jws-algorithms: RS256,ES256
server:
  ssl:
    enabled: true
    key-store: classpath:keystore.p12
    key-store-type: PKCS12

10.2 监控与日志脱敏

// AuthEventListener.java — 审计日志(脱敏处理)
@Component
public class AuthEventListener {

    private static final Logger auditLog = LoggerFactory.getLogger("AUTH_AUDIT");

    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        Authentication auth = event.getAuthentication();
        auditLog.info("认证成功 | 用户:{} | 类型:{} | 时间:{}",
            maskUsername(auth.getName()),
            auth.getClass().getSimpleName(),
            Instant.now());
    }

    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        auditLog.warn("认证失败 | 用户:{} | 原因:{} | 时间:{}",
            maskUsername(event.getAuthentication().getName()),
            event.getException().getMessage(),
            Instant.now());
    }

    private String maskUsername(String username) {
        if (username == null || username.length() <= 2) return "***";
        return username.charAt(0) + "***" + username.charAt(username.length() - 1);
    }
}

10.3 Spring Security 6 迁移要点

  1. WebSecurityConfigurerAdapter 已移除,全面使用 SecurityFilterChain Bean。
  2. authorizeRequests() -> authorizeHttpRequests()(lambda DSL)。
  3. antMatchers() -> requestMatchers()
  4. 默认启用 Spring One Login Page,需注意 SPA 场景的 formLogin 配置。

FAQ

Q1: Access Token 与 Refresh Token 应该如何存储在 SPA 中?

Access Token 可存放在 memory 中以减少 XSS 风险。Refresh Token 必须使用 HttpOnly, Secure, SameSite=Strict Cookie 存储,并在授权服务器端绑定 client_id 与设备指纹,防止 Token 旋转攻击。

Q2: JWT 真的无状态吗?如何实现强制登出?

纯 JWT 本身是无状态的,强制登出需要引入状态层。推荐方案:通过 Redis 维护 jti 黑名单(有效期与 Token 相同),在网关或资源服务器校验黑名单。对于高安全场景,缩短 Access Token 有效期至 5-15 分钟。

Q3: Spring Security OAuth 项目与 Spring Authorization Server 的关系是什么?

Spring Security OAuth(spring-security-oauth2)已于 2020 年停止维护。Spring Authorization Server(spring-authorization-server)是官方继任项目,基于 Spring Security 6 构建,完全支持 OAuth2.1 与 OIDC 1.0。

Q4: Resource Server 如何校验 Token 而不引入巨大的授权服务器依赖?

资源服务器仅需引入 spring-boot-starter-oauth2-resource-server,配置 issuer-uri 即可通过 .well-known 端点自动拉取 JWKS 公钥完成自包含 JWT 校验。无需连接授权服务器数据库或 HTTP 内省端点。

Q5: 如何应对 JWT 密钥泄露或轮换场景?

  1. 使用 kid(Key ID)标识当前密钥,JWT Header 中包含 kid
  2. 授权服务器支持同时暴露新旧两组公钥在 JWKS 端点。
  3. 缩短 Token 有效期,确保旧密钥签发的 Token 尽快失效。
  4. 通过配置中心推送新密钥,无需重启资源服务器。

总结

Spring Security 6.x 配合 Spring Authorization Server 提供了企业级 OAuth2/JWT 方案的完整技术栈。核心要点包括:使用非对称密钥(RS256/ES256)签发 JWT,采用 PKCE 保护 SPA 和移动端授权码流程,资源服务器通过 JWKS 实现无状态自校验,方法级安全通过 PermissionEvaluator 实现 ABAC,生产环境务必配置 Token 黑名单、CSP 安全头和 HTTPS 强制策略。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. Spring Cloud 微服务全栈实践
  2. Spring Data JPA 高级指南:关联映射、N+1 与性能优化
  3. Spring Boot 3 深度解析:自动装配、Starter 开发与生产就绪