Spring Security 6 认证授权完全实战:Filter Chain、JWT 与 RBAC 从零到生产

深入解析 Spring Security 6 核心架构与认证授权实战,涵盖 Filter Chain 原理、JWT + OAuth2 资源保护、数据库驱动的 RBAC 动态授权、方法级权限控制,附完整可运行代码与生产环境避坑指南。

Java 后端 2026-06-06 22 分钟

Spring Security 是 Java 生态中事实上的安全标准框架,但它的复杂度也是出了名的——根据 Snyk 2025 年 Java 生态报告,超过 65% 的 Spring 应用存在安全配置缺陷,其中最常见的问题就是认证授权配置不当。很多开发者在 Stack Overflow 上复制一段 SecurityFilterChain 配置就上线了,直到被安全审计打回来才发现 CSRF 没关对、CORS 没配好、JWT 过期策略根本没设。Spring Security 6 对配置方式做了颠覆性改造,废弃了继承 WebSecurityConfigurerAdapter 的老模式,全面转向 Lambda DSL——如果你还在用旧写法,是时候升级了。

本文不讲理论概念,只讲生产环境真正需要的东西:Filter Chain 怎么配?JWT 怎么签发和验证?RBAC 权限怎么从数据库动态加载?方法级权限怎么控制? 每个方案都有完整可运行的代码,每个坑都是真实项目踩过的。

📌 记住: 安全不是「加上 Spring Security 依赖就完事」。它需要理解 Filter Chain 的执行顺序、Token 的生命周期管理、权限模型的设计,以及各种边界情况的处理。

🔐 一、Spring Security 6 架构:Filter Chain 深度解析

1.1 SecurityFilterChain 工作原理

Spring Security 的核心是一条 Filter Chain(过滤器链)。每个 HTTP 请求都会经过这条链,链上的每个 Filter 负责一个安全关注点:认证、授权、CSRF 防护、Session 管理、CORS 等。

理解 Filter Chain 的执行顺序是掌握 Spring Security 的关键。以下是核心 Filter 的执行流程:

请求 → SecurityContextPersistenceFilter
     → UsernamePasswordAuthenticationFilter (表单登录)
     → BearerTokenAuthenticationFilter (JWT/OAuth2)
     → ExceptionTranslationFilter (异常处理)
     → FilterSecurityInterceptor (授权决策)
     → Controller

Spring Security 6 的一大变化是 AuthorizationFilter 替代了 FilterSecurityInterceptor,授权逻辑从 AOP 切面移入了 Filter 链,这让调试和理解执行流程更加直观。

1.2 从旧配置到 Lambda DSL 的迁移

Spring Security 6 彻底废弃了 WebSecurityConfigurerAdapter。对比新旧写法:

旧写法(已废弃):

// 继承 WebSecurityConfigurerAdapter —— Spring Security 6 已移除
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .csrf().disable();
    }
}

新写法(Lambda DSL):

// Spring Security 6 推荐配置方式
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(Customizer.withDefaults())
            );
        return http.build();
    }
}

⚠️ 警告: 不要在生产环境无脑 csrf().disable()。只有纯 API 服务(前后端分离、使用 JWT)才应该禁用 CSRF。如果你的服务有 Session 认证,禁用 CSRF 会导致严重的安全漏洞。

Lambda DSL 的优势在于:代码更清晰、IDE 补全更好、每个配置块都是独立的 Lambda,不会因为 .and() 链式调用出错。

🔑 二、JWT 认证实战:从 Token 签发到资源保护

2.1 认证方案选型对比

在动手写代码之前,先搞清楚该用什么认证方案。很多开发者一上来就用 JWT,但实际上并非所有场景都适合 JWT。

对比维度 Session + Cookie JWT (无状态) OAuth2 + JWT
状态管理 服务端有状态 客户端自包含 服务端有状态 (Token 存储)
适用场景 传统 Web 应用 SPA / 移动端 API 第三方授权 / 微服务
扩展性 ⚠️ 需 Session 共享 ✅ 天然支持分布式 ✅ 天然支持分布式
Token 撤销 ✅ 删除 Session 即可 ❌ 需额外机制 ✅ 撤销 Refresh Token
安全性 ✅ HttpOnly Cookie 防 XSS ⚠️ 需防范 Token 泄露 ✅ 短期 Token + 刷新
实现复杂度 ⭐⭐ 低 ⭐⭐⭐ 中 ⭐⭐⭐⭐ 高

关键结论: 如果你的应用是纯后端 API + 前端 SPA,选 JWT + OAuth2 Resource Server;如果是传统服务端渲染应用,Session + Cookie 可能更简单安全。

2.2 JWT Token 生成与验证完整实现

以下是一个生产级的 JWT 工具类,包含 Token 签发、验证和过期处理:

// JWT 工具类:签发、验证、解析 Token
@Component
public class JwtTokenProvider {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${jwt.access-token-expiration:3600000}") // 默认 1 小时
    private long accessTokenExpiration;

    @Value("${jwt.refresh-token-expiration:604800000}") // 默认 7 天
    private long refreshTokenExpiration;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
    }

    // 签发 Access Token
    public String generateAccessToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        claims.put("type", "access");

        return Jwts.builder()
            .claims(claims)
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + accessTokenExpiration))
            .signWith(getSigningKey())
            .compact();
    }

    // 签发 Refresh Token
    public String generateRefreshToken(UserDetails userDetails) {
        return Jwts.builder()
            .claim("type", "refresh")
            .subject(userDetails.getUsername())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + refreshTokenExpiration))
            .signWith(getSigningKey())
            .compact();
    }

    // 验证 Token 有效性
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token);
            return true;
        } catch (ExpiredJwtException e) {
            log.warn("JWT Token 已过期: {}", e.getMessage());
            return false;
        } catch (JwtException e) {
            log.warn("JWT Token 无效: {}", e.getMessage());
            return false;
        }
    }

    // 从 Token 中提取用户名
    public String getUsernameFromToken(String token) {
        return Jwts.parser()
            .verifyWith(getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload()
            .getSubject();
    }
}

💡 提示: JWT Secret 必须至少 256 位(32 字节),建议使用 Base64 编码的随机字符串。永远不要把 Secret 硬编码在代码里,通过环境变量或配置中心注入。

2.3 双 Token 刷新流程

单 Token 方案有一个致命问题:Token 一旦签发就无法撤销。如果 Access Token 泄露,在过期之前攻击者可以一直使用。双 Token 方案(Access Token + Refresh Token)是业界标准解决方案:

// Token 刷新端点:用 Refresh Token 换取新的 Access Token
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final JwtTokenProvider jwtTokenProvider;
    private final AuthenticationManager authenticationManager;
    private final RefreshTokenRepository refreshTokenRepository;

    @PostMapping("/refresh")
    public ResponseEntity<TokenResponse> refreshToken(@RequestBody RefreshRequest request) {
        // 1. 验证 Refresh Token 有效性
        if (!jwtTokenProvider.validateToken(request.getRefreshToken())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // 2. 检查 Refresh Token 是否在数据库中(防止已撤销的 Token 被使用)
        RefreshToken storedToken = refreshTokenRepository
            .findByToken(request.getRefreshToken())
            .orElseThrow(() -> new RuntimeException("Refresh Token 已失效"));

        // 3. 检查是否过期
        if (storedToken.getExpiryDate().isBefore(Instant.now())) {
            refreshTokenRepository.delete(storedToken);
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        // 4. 签发新的 Access Token
        String username = jwtTokenProvider.getUsernameFromToken(request.getRefreshToken());
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        String newAccessToken = jwtTokenProvider.generateAccessToken(userDetails);

        // 5. 可选:Refresh Token 轮转(每次刷新都换新的 Refresh Token,更安全)
        // refreshTokenRepository.delete(storedToken);
        // String newRefreshToken = jwtTokenProvider.generateRefreshToken(userDetails);
        // saveRefreshToken(newRefreshToken, username);

        return ResponseEntity.ok(new TokenResponse(newAccessToken, request.getRefreshToken()));
    }
}

📌 记住: Refresh Token 轮转(Rotation)是指每次使用 Refresh Token 时,旧的立即失效,同时签发新的 Refresh Token。这样即使 Refresh Token 泄露,攻击者也只能使用一次。Spring Authorization Server 默认就启用了这个策略。

2.4 OAuth2 Resource Server 配置

在微服务架构中,通常有一个专门的 Authorization Server 签发 Token,其他服务作为 Resource Server 验证 Token。Spring Security 6 对 OAuth2 Resource Server 的支持非常完善:

// OAuth2 Resource Server 配置:验证 JWT Token
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")  // 只对 /api/** 路径生效
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers(HttpMethod.GET, "/api/articles/**").permitAll()
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        return http.build();
    }

    // 自定义 JWT -> Authentication 转换器
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");

        JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
        authenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        return authenticationConverter;
    }
}

application.yml 配置:

# OAuth2 Resource Server JWT 配置
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 方式一:直接配置 JWK Set URI(推荐)
          jwk-set-uri: https://auth.example.com/.well-known/jwks.json
          # 方式二:本地密钥验证
          # issuer-uri: https://auth.example.com

👥 三、RBAC 权限控制:数据库驱动的动态授权

3.1 RBAC 权限模型设计

生产级的权限系统通常采用 RBAC(Role-Based Access Control)模型。以下是基于 MySQL 的核心表结构 DDL:

-- RBAC 权限模型:5 张核心表
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,  -- BCrypt 加密后固定 60 字符
    enabled TINYINT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_name VARCHAR(50) NOT NULL,     -- 显示名:管理员
    role_code VARCHAR(50) NOT NULL UNIQUE -- 角色码:ROLE_ADMIN
);

CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    permission_name VARCHAR(100) NOT NULL,     -- 显示名:查看用户
    permission_code VARCHAR(100) NOT NULL UNIQUE, -- 权限码:user:read
    resource_url VARCHAR(200)                   -- 可选:对应的 URL 模式
);

CREATE TABLE sys_user_role (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id)
);

CREATE TABLE sys_role_permission (
    role_id BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id)
);

💡 提示: 权限码(permission_code)的命名建议采用 资源:操作 的格式,如 user:readorder:exportreport:delete。这种命名方式语义清晰,且方便在 @PreAuthorize 注解中使用通配符匹配,例如 hasAuthority('user:*') 匹配所有用户相关权限。

3.2 UserDetailsService 实现

Spring Security 通过 UserDetailsService 接口加载用户信息。以下是数据库驱动的实现:

// 基于数据库的 UserDetailsService 实现
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户基本信息
        SysUser user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "用户不存在: " + username));

        // 查询用户角色和权限
        List<SysRole> roles = roleRepository.findByUserId(user.getId());

        // 构建 GrantedAuthority 列表
        List<GrantedAuthority> authorities = roles.stream()
            .flatMap(role -> {
                List<GrantedAuthority> roleAuthorities = new ArrayList<>();
                // 添加角色(如 ROLE_ADMIN)
                roleAuthorities.add(new SimpleGrantedAuthority(role.getRoleCode()));
                // 添加权限(如 user:read, user:write)
                role.getPermissions().forEach(perm ->
                    roleAuthorities.add(new SimpleGrantedAuthority(perm.getPermissionCode()))
                );
                return roleAuthorities.stream();
            })
            .distinct()
            .collect(Collectors.toList());

        return new CustomUserDetails(
            user.getId(),
            user.getUsername(),
            user.getPassword(),
            user.getEnabled(),
            authorities
        );
    }
}

3.3 方法级权限控制

Spring Security 支持通过注解在方法级别进行权限控制,这比 URL 级别控制更精细:

// 启用方法级安全控制
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // Spring Security 6 默认已启用 @PreAuthorize
    // 无需额外配置即可使用
}

// 在 Service 层使用方法级权限控制
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    // 只有拥有 user:read 权限的用户才能调用
    @PreAuthorize("hasAuthority('user:read')")
    public UserDTO getUser(Long userId) {
        SysUser user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
        return UserDTO.fromEntity(user);
    }

    // 只有 ADMIN 角色或用户本人可以修改
    @PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
    public UserDTO updateUser(Long userId, UpdateUserRequest request) {
        SysUser user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("用户不存在"));
        user.setEmail(request.getEmail());
        userRepository.save(user);
        return UserDTO.fromEntity(user);
    }

    // 需要同时满足多个权限
    @PreAuthorize("hasAuthority('user:delete') and hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }

    // 支持 SpEL 表达式的复杂权限逻辑
    @PreAuthorize("@permissionService.hasProjectAccess(#projectId, authentication)")
    public ProjectDTO getProject(Long projectId) {
        // 项目级权限检查:自定义 PermissionService Bean
        return projectService.findById(projectId);
    }
}

⚠️ 警告: @PreAuthorize 只能保护 Spring 管理的 Bean 的方法。如果一个类内部调用自己的 @PreAuthorize 方法(this.method()),权限检查不会生效——因为 Spring AOP 代理不拦截自调用。这是最常见的坑之一。

⚡ 四、生产环境避坑指南

4.1 常见配置陷阱与解决方案

在实际项目中,以下问题出现频率极高:

坑点 1:CSRF 与前后端分离

前后端分离项目中,前端通常使用 localStorage 存储 JWT,此时不需要 CSRF 保护。但如果你错误地在禁用 CSRF 的同时保留了 Session 认证,就会暴露 CSRF 漏洞。

// ✅ 正确:前后端分离 API 服务
http.csrf(csrf -> csrf.disable())  // JWT 认证不需要 CSRF
    .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// ❌ 错误:有 Session 的 Web 应用禁用了 CSRF
http.csrf(csrf -> csrf.disable())  // 这会让表单提交暴露给 CSRF 攻击!
    .sessionManagement(session ->
        session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));

坑点 2:CORS 配置顺序

Spring Security 6 中,CORS 配置必须在 authorizeHttpRequests 之前,否则预检请求(OPTIONS)会被拦截:

// ✅ 正确的配置顺序
http
    .cors(cors -> cors.configurationSource(corsConfigurationSource()))  // CORS 在前
    .csrf(csrf -> csrf.disable())
    .authorizeHttpRequests(authorize -> authorize  // 授权在后
        .anyRequest().authenticated()
    );

坑点 3:Filter 优先级冲突

当项目中有多个 SecurityFilterChain Bean 时,Spring Security 按 @Order 注解排序,第一个匹配的 Filter Chain 生效。如果配置不当,可能所有请求都走了错误的 Chain。

// 多 Filter Chain 配置示例
@Configuration
@EnableWebSecurity
public class MultiSecurityConfig {

    // API 路径走 JWT 认证
    @Bean
    @Order(1)  // 优先级更高
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        return http.build();
    }

    // 其他路径走表单登录
    @Bean
    @Order(2)
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/login", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

4.2 安全加固 Checklist

部署到生产环境前,务必检查以下配置:

  • JWT Secret 足够强:至少 256 位,通过环境变量注入
  • Token 过期时间合理:Access Token 15-60 分钟,Refresh Token 7-30 天
  • 启用 HTTPS:生产环境必须使用 TLS
  • 密码使用 BCrypt@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
  • 配置安全响应头:X-Content-Type-Options、X-Frame-Options、Strict-Transport-Security
  • 日志不记录敏感信息:Token、密码、密钥不能出现在日志中
  • 不要自己实现加密算法:使用 Spring Security 内置的密码编码器和 JWT 库
  • 不要在 URL 中传递 Token:Token 应该放在 Authorization Header 中

⚠️ 警告: 如果你的应用需要支持 Token 撤销(如用户登出后立即失效),单纯使用无状态 JWT 是不够的。你需要引入 Redis 维护一个 Token 黑名单,或者使用短 Access Token(5-15 分钟)配合 Refresh Token 轮转策略。

4.3 安全测试:用 MockMvc 验证权限配置

很多开发者写完安全配置就直接上线了,从不写测试验证权限是否生效。Spring Security Test 提供了强大的测试支持:

// Spring Security 集成测试:验证权限配置是否正确
@SpringBootTest
@AutoConfigureMockMvc
class SecurityIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    @WithMockUser(username = "user1", roles = {"USER"})
    void userCanAccessOwnProfile() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "user1", roles = {"USER"})
    void userCannotAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    void adminCanAccessAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
            .andExpect(status().isOk());
    }

    @Test
    void unauthenticatedRequestReturns401() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isUnauthorized());
    }
}

📌 记住: 安全测试不是可选的。每个权限规则都应该有对应的测试用例。特别要测试「反向场景」——验证无权限用户确实被拒绝,比验证有权限用户能访问更重要。

📊 总结与最佳实践

Spring Security 6 的学习曲线确实陡峭,但一旦理解了 Filter Chain 的核心模型,其他功能都是在这个模型上的扩展。以下是我的实践建议:

架构选型建议:

场景 推荐方案 理由
前后端分离 SPA JWT + OAuth2 Resource Server 无状态,易扩展
传统服务端渲染 Session + Cookie + CSRF 简单安全,成熟可靠
微服务架构 OAuth2 Authorization Server + JWT 统一认证,集中管理
移动端 API JWT + Refresh Token 适合移动端存储

核心原则:

  • ✅ 认证方案选对,不要所有场景都用 JWT
  • ✅ 使用 Lambda DSL 配置,不要再用废弃的继承方式
  • ✅ 方法级权限用 @PreAuthorize,比 URL 级别更精细
  • ✅ 生产环境做好安全加固 Checklist
  • ❌ 不要无脑禁用 CSRF,搞清楚你的认证模式
  • ❌ 不要在代码中硬编码密钥

相关工具推荐:

  • Spring Authorization Server:Spring 官方的 OAuth2 授权服务器,替代过时的 Spring Security OAuth
  • jjwt:最流行的 Java JWT 库,本文代码示例基于此库
  • Keycloak:开源身份和访问管理(IAM)服务器,适合不想自建认证系统的团队
  • Spring Security Test@WithMockUser@WithJwt 等测试注解,让安全测试更简单

Spring Security 的本质是 Filter Chain + Authentication + Authorization 三件套。把这三件搞清楚,你就能应对 90% 的安全需求。剩下的 10%——比如 OAuth2 授权码流程、PKCE、Passkey——那是另一篇文章的事了。

📚 相关文章