Chapter 04

Spring Security 与 JWT

认证授权体系设计,SecurityFilterChain 配置,JWT 无状态认证完整实现,RBAC 权限模型。

认证(Authentication)vs 授权(Authorization)

这两个概念经常被混用,但它们解决的是完全不同的问题:

认证(Authentication)— 你是谁?

  • 验证用户身份的过程
  • 典型方式:用户名+密码、OAuth2、证书
  • 结果:确认身份,颁发凭证(Token)
  • Spring Security 对应:AuthenticationManager

授权(Authorization)— 你能做什么?

  • 验证用户是否有权访问资源
  • 基于角色(RBAC)或权限(ABAC)
  • 结果:允许或拒绝访问
  • Spring Security 对应:AccessDecisionManager

核心概念名词解释

SecurityFilterChain
Spring Security 5.x+ 的核心配置方式,替代了旧版 WebSecurityConfigurerAdapter。通过定义 SecurityFilterChain Bean 来配置哪些 URL 需要认证、使用什么认证方式、CORS/CSRF 等安全策略。
UserDetailsService
Spring Security 提供的接口,只有一个方法 loadUserByUsername(String username)。实现此接口来告诉 Spring Security 如何从数据库加载用户信息(用户名、密码哈希、角色列表)。
PasswordEncoder
密码编码器接口。BCryptPasswordEncoder 是最常用的实现,使用 bcrypt 算法对密码进行单向哈希,每次哈希时加入随机盐值,使得相同密码的哈希结果不同,有效防御彩虹表攻击。
JWT(JSON Web Token)
一种紧凑的、自包含的令牌格式,用于在各方之间安全传递信息。服务端签发 JWT 后不需要存储 Session,每次请求只需验证签名即可,实现无状态认证(Stateless Authentication)。
Claims(声明)
JWT Payload 中的键值对,包含用户信息。标准 Claims:sub(主题/用户ID)、iat(签发时间)、exp(过期时间)。自定义 Claims 可以包含 roles、email 等业务信息。
BCrypt
一种密码哈希算法,专为密码存储设计。其工作因子(cost factor)可调整,使得随着硬件性能提升,可以通过增加工作因子来保持暴力破解的计算成本。Spring Security 默认使用 cost=10。
RBAC(基于角色的访问控制)
Role-Based Access Control,将权限分配给角色,再将角色分配给用户。如 ROLE_ADMIN 拥有所有权限,ROLE_USER 只能访问自己的数据。Spring Security 通过 @PreAuthorize 等注解实现。

JWT 认证完整流程

┌─────────────────────────────────────────────────────────────────┐ │ JWT 无状态认证流程 │ └─────────────────────────────────────────────────────────────────┘ 【登录阶段】 客户端 │ POST /auth/login { username, password } ▼ Spring Security(UsernamePasswordAuthenticationFilter) │ loadUserByUsername → 从数据库加载用户 │ BCrypt.verify(inputPassword, storedHash) ▼ 认证成功 → JwtService.generateToken(user) │ │ 返回 { accessToken: "eyJ...", refreshToken: "eyJ..." } ▼ 客户端存储 Token(localStorage 或 HttpOnly Cookie) 【请求阶段】 客户端 │ GET /api/users Header: Authorization: Bearer eyJ... ▼ JwtAuthenticationFilter(自定义 OncePerRequestFilter) │ 1. 提取 Header 中的 Token │ 2. JwtService.validateToken(token) │ ├── 验证签名(防篡改) │ ├── 检查过期时间 │ └── 解析 Claims │ 3. 将用户信息放入 SecurityContext ▼ Controller 正常处理请求 │ ▼ 响应数据

JWT 结构解析

JWT 由三个 Base64URL 编码的部分组成,用 . 分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9    ← Header(算法+类型)
.
eyJzdWIiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDM2MDB9
                                          ← Payload(用户信息+过期时间)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
                                          ← Signature(HMAC-SHA256 签名)

Payload 解码后:{"sub":"123","email":"user@example.com","roles":["ROLE_USER"],"iat":1700000000,"exp":1700003600}

完整实现代码

Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

JwtService — JWT 工具类

@Service
public class JwtService {

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

    @Value("${jwt.expiration:3600}")  // 默认 1 小时
    private long expiration;

    private SecretKey getKey() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
    }

    public String generateToken(UserDetails userDetails) {
        return Jwts.builder()
            .subject(userDetails.getUsername())
            .claim("roles", userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority).toList())
            .issuedAt(new Date())
            .expiration(new Date(System.currentTimeMillis() + expiration * 1000))
            .signWith(getKey())
            .compact();
    }

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

    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractClaims(token).getSubject();
        return username.equals(userDetails.getUsername())
            && !extractClaims(token).getExpiration().before(new Date());
    }
}

SecurityFilterChain 配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // 开启 @PreAuthorize 等方法级安全
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;
    private final UserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)  // JWT 无状态,禁用 CSRF
            .sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/actuator/health").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // cost factor 默认 10
    }

    @Bean
    public AuthenticationManager authManager(AuthenticationConfiguration config)
            throws Exception {
        return config.getAuthenticationManager();
    }
}

// 自定义 JWT 过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest req,
            HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {

        final String authHeader = req.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(req, res);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String username = jwtService.extractClaims(jwt).getSubject();

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                SecurityContextHolder.getContext().setAuthentication(auth);
            }
        }
        chain.doFilter(req, res);
    }
}

方法级权限控制

@RestController
@RequestMapping("/admin")
public class AdminController {

    // 只有 ADMIN 角色可以访问
    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    public List<UserResponseDTO> listAllUsers() { ... }

    // 管理员或本人才能查看
    @GetMapping("/users/{id}")
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public UserResponseDTO getUser(@PathVariable Long id) { ... }
}
Danger 密码必须使用 BCrypt 等不可逆哈希算法存储,绝对不能明文存储,也不能使用 MD5/SHA1 等普通哈希(易受彩虹表攻击)。BCryptPasswordEncoder 会在每次哈希时自动加随机盐值,即使两个用户密码相同,存储的哈希值也不同。
Warning JWT Secret 必须足够长(至少 256 位/32字节),且不能硬编码在代码中。应通过环境变量注入:JWT_SECRET=your-256-bit-secret-here,在 application.yml 中用 ${JWT_SECRET} 引用。