一、核心概念

在现代 Web 应用中,安全认证是保护资源、确保用户身份真实性的基石。Spring Boot 通过集成 Spring Security 提供了强大的安全框架。理解 Session、Token 和 JWT 是掌握认证机制的关键。

1. 认证 (Authentication) 与 授权 (Authorization)

  • 认证 (Auth): 确认“你是谁”。验证用户的身份(如用户名/密码、生物识别)。
  • 授权 (Authz): 确定“你能做什么”。在认证成功后,检查用户是否有权限访问特定资源或执行特定操作(如角色 ROLE_ADMIN 可访问管理后台)。

2. Session 认证

  • 概念: 基于服务器端会话(Session)的认证机制。
  • 流程:
    1. 用户登录,服务器验证凭据(如用户名/密码)。
    2. 验证成功,服务器在内存或分布式缓存(如 Redis)中创建一个 Session 对象,并生成一个唯一的 Session ID
    3. 服务器将 Session ID 通过 Set-Cookie 响应头发送给客户端(浏览器)。
    4. 客户端(浏览器)将 Session ID 存储在 Cookie 中。
    5. 后续每次请求,客户端自动在 Cookie 请求头中携带 Session ID
    6. 服务器收到请求,根据 Session ID 查找对应的 Session 对象,获取用户信息(如 Principal)。
    7. 服务器进行授权检查,决定是否响应请求。
  • 特点:
    • 有状态 (Stateful): 服务器需要存储 Session 数据。
    • 依赖 Cookie: 通常依赖浏览器的 Cookie 机制传递 Session ID
    • 适合传统 Web 应用: 与浏览器的交互模式天然契合。
    • 扩展性挑战: 在分布式系统中,需要 Session 共享(如使用 Redis)。

3. Token 认证

  • 概念: 基于令牌(Token)的无状态认证机制。Token 是一个字符串,代表用户的认证信息。
  • 流程:
    1. 用户登录,服务器验证凭据。
    2. 验证成功,服务器生成一个唯一的、随机的 Token(如 UUID),并将其与用户信息关联存储在服务器端(如数据库、Redis)。
    3. 服务器将 Token 返回给客户端(通常在响应体或 Authorization 头)。
    4. 客户端(如 Web App、Mobile App)将 Token 存储在 LocalStorageSessionStorage 或内存中。
    5. 后续每次请求,客户端在 Authorization 请求头中携带 Token(格式如 Bearer <token>)。
    6. 服务器收到请求,从 Authorization 头提取 Token,查询服务器端存储,验证 Token 有效性并获取用户信息。
    7. 服务器进行授权检查。
  • 特点:
    • 无状态 (Stateless) / 有状态 (Stateful): 严格来说,如果服务器需要查询存储来验证 Token,它仍然是“有状态”的(状态在存储中)。但客户端请求本身不依赖服务器的会话状态。
    • 跨域友好: 不依赖 Cookie,天然支持跨域请求(CORS)。
    • 适合 API / 前后端分离 / 移动端: 客户端灵活存储 Token。
    • Token 管理: 需要处理 Token 的存储、刷新、撤销(注销)。

4. JWT (JSON Web Token) 认证

  • 概念: 一种开放标准 (RFC 7519),用于在各方之间安全地传输信息作为 JSON 对象。JWT 是 Token 认证的一种具体实现,其核心特点是自包含 (Self-Contained)无状态 (Stateless)
  • 结构: Header.Payload.Signature 三部分,用 . 连接。
    • Header: 包含令牌类型和签名算法(如 {"alg": "HS256", "typ": "JWT"})。
    • Payload: 包含声明 (Claims)。分为三类:
      • Registered: 预定义的(如 iss 发行者, exp 过期时间, sub 主题, aud 受众)。
      • Public: 可自定义,但应避免冲突。
      • Private: 自定义,用于在双方间共享信息(如 userId, role)。
    • Signature:HeaderPayload 进行签名,确保令牌未被篡改。服务器用密钥(如 HMAC SHA256 或 RSA 私钥)生成签名。
  • 流程:
    1. 用户登录,服务器验证凭据。
    2. 验证成功,服务器根据用户信息生成 JWT(包含 exp 过期时间)。
    3. 服务器将 JWT 返回给客户端。
    4. 客户端存储 JWT。
    5. 后续每次请求,客户端在 Authorization 头中携带 JWT(Bearer <jwt>)。
    6. 服务器收到请求,不查询数据库,而是:
      • 验证 JWT 签名是否有效(使用密钥)。
      • 检查 exp 等时间声明是否过期。
      • 解析 Payload 获取用户信息(如 userId, role)。
    7. 服务器进行授权检查。
  • 特点:
    • 真正无状态 (Truly Stateless): 服务器无需存储 JWT(除了黑名单处理撤销),验证完全基于签名和内容。
    • 自包含: 所有需要的信息都在 Token 本身。
    • 跨域友好: 同 Token。
    • 可扩展性好: 适合分布式、微服务架构。
    • 挑战:
      • 无法撤销 (Revocation): 一旦签发,在过期前无法强制失效(除非引入黑名单机制,牺牲无状态性)。
      • 信息暴露: Payload 是 Base64 编码,可被解码查看(不要在 Payload 中存放敏感信息如密码)。
      • 密钥管理: 密钥(尤其是对称密钥 HS256)的安全至关重要。

二、操作步骤(非常详细)

场景设定

为一个简单的 RESTful API (/api/v1/users) 添加安全认证。实现基于 JWT 的认证流程。

步骤 1:添加依赖

<!-- pom.xml -->
<dependencies>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- JWT 支持 (推荐使用 jjwt-api, jjwt-impl, jjwt-jackson) -->
    <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> <!-- or jjwt-gson if using Gson -->
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <!-- Lombok (可选,简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

步骤 2:创建用户实体与服务

// User.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String username;
    private String password; // 实际应用中应存储加密后的密码
    private String role; // 如 "ROLE_USER", "ROLE_ADMIN"
    // ... 其他字段
}

// UserService.java
@Service
public class UserService {
    // 模拟用户存储 (实际应用应使用数据库)
    private final Map<String, User> users = new HashMap<>();

    public UserService() {
        // 初始化测试用户
        User user = User.builder()
                .id(1L)
                .username("user")
                .password("$2a$10$gTJXqy9v7JZq6v2W9q1Z5eU6q3Y7v8w9x0y1z2a3b4c5d6e7f8g9h0i1j2k3") // BCrypt 加密的 "password"
                .role("ROLE_USER")
                .build();
        users.put(user.getUsername(), user);

        User admin = User.builder()
                .id(2L)
                .username("admin")
                .password("$2a$10$gTJXqy9v7JZq6v2W9q1Z5eU6q3Y7v8w9x0y1z2a3b4c5d6e7f8g9h0i1j2k3") // 同上
                .role("ROLE_ADMIN")
                .build();
        users.put(admin.getUsername(), admin);
    }

    public User findByUsername(String username) {
        return users.get(username);
    }
}

步骤 3:创建 JWT 工具类

// JwtUtil.java
@Component
public class JwtUtil {

    // 密钥 (生产环境务必保密且足够长!)
    private final String SECRET_KEY = "your-256-bit-secret-your-256-bit-secret-your-256-bit-secret"; // 示例,长度需匹配算法
    // 过期时间 (毫秒)
    private final long EXPIRATION_TIME = 86400000; // 24 hours

    /**
     * 从用户名生成 JWT
     * @param username 用户名
     * @return JWT 字符串
     */
    public String generateToken(String username) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + EXPIRATION_TIME);

        return Jwts.builder()
                .setSubject(username) // 主题 (通常为用户名)
                .setIssuedAt(now) // 签发时间
                .setExpiration(expiryDate) // 过期时间
                // 可以添加自定义声明 (Claims)
                .claim("role", getUserRole(username)) // 示例:添加角色 (实际应从用户服务获取)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // 使用 HS256 和密钥签名
                .compact(); // 构建并返回 JWT 字符串
    }

    /**
     * 从 JWT 中提取用户名
     * @param token JWT 字符串
     * @return 用户名
     */
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    /**
     * 从 JWT 中提取特定声明 (Claim)
     * @param token JWT 字符串
     * @param claimsResolver 声明解析器 (Function<Claims, T>)
     * @return 声明值
     */
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    /**
     * 验证 JWT 是否有效
     * @param token JWT 字符串
     * @param userDetails 用户详细信息
     * @return true if valid
     */
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    /**
     * 检查 JWT 是否过期
     * @param token JWT 字符串
     * @return true if expired
     */
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    /**
     * 从 JWT 中提取过期时间
     * @param token JWT 字符串
     * @return 过期时间 Date
     */
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    /**
     * 解析 JWT 获取所有声明 (Claims)
     * @param token JWT 字符串
     * @return Claims 对象
     */
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY) // 设置签名密钥用于验证
                .parseClaimsJws(token) // 解析 JWT
                .getBody(); // 获取 Payload (Claims)
    }

    // 辅助方法 (实际应从 UserService 获取)
    private String getUserRole(String username) {
        User user = userService.findByUsername(username);
        return user != null ? user.getRole() : null;
    }

    // 注入 UserService (如果需要从数据库获取角色等信息)
    @Autowired
    private UserService userService;
}

步骤 4:创建 UserDetails 实现

// UserDetailsImpl.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public static UserDetailsImpl build(User user) {
        List<GrantedAuthority> authorities = Collections
                .singletonList(new SimpleGrantedAuthority(user.getRole()));

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true; // 简化
    }

    @Override
    public boolean isAccountNonLocked() {
        return true; // 简化
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true; // 简化
    }

    @Override
    public boolean isEnabled() {
        return true; // 简化
    }
}

步骤 5:创建 JWT 认证过滤器

// JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        try {
            // 1. 从请求头中提取 JWT
            String jwt = extractJwtFromRequest(request);

            if (jwt != null && jwtUtil.validateToken(jwt, getUserDetailsFromToken(jwt))) {
                // 2. 如果 JWT 有效,创建 Authentication 对象
                Authentication authentication = createAuthentication(jwt);
                // 3. 将 Authentication 对象存入 SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }

        // 继续过滤链
        filterChain.doFilter(request, response);
    }

    /**
     * 从请求中提取 JWT
     * @param request HttpServletRequest
     * @return JWT 字符串,如果不存在则返回 null
     */
    private String extractJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7); // 移除 "Bearer " 前缀
        }
        return null;
    }

    /**
     * 从 JWT 中获取 UserDetails
     * @param jwt JWT 字符串
     * @return UserDetails
     */
    private UserDetails getUserDetailsFromToken(String jwt) {
        String username = jwtUtil.extractUsername(jwt);
        User user = userService.findByUsername(username);
        return user != null ? UserDetailsImpl.build(user) : null;
    }

    /**
     * 根据 JWT 创建 Authentication 对象
     * @param jwt JWT 字符串
     * @return Authentication
     */
    private Authentication createAuthentication(String jwt) {
        String username = jwtUtil.extractUsername(jwt);
        UserDetails userDetails = getUserDetailsFromToken(jwt);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

步骤 6:配置 Spring Security

// SecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用方法级安全注解
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 推荐使用 BCrypt
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF (对于 JWT API 通常不需要,但如果是传统表单登录则需考虑)
            .csrf(csrf -> csrf.disable())
            // 禁用 Session (使用 JWT 时)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // 配置请求授权
            .authorizeHttpRequests(authz -> authz
                // 允许访问登录和注册端点
                .requestMatchers("/api/v1/auth/login", "/api/v1/auth/register").permitAll()
                // 其他所有请求都需要认证
                .anyRequest().authenticated()
            )
            // 添加 JWT 过滤器,在 UsernamePasswordAuthenticationFilter 之前
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

步骤 7:创建认证 Controller

// AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private UserService userService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 用户登录
     * @param loginRequest 包含 username 和 password
     * @return 包含 JWT 的响应
     */
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        try {
            // 1. 使用 AuthenticationManager 进行认证
            Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            loginRequest.getUsername(),
                            loginRequest.getPassword()
                    )
            );

            // 2. 认证成功,获取 UserDetails
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();

            // 3. 生成 JWT
            String jwt = jwtUtil.generateToken(userDetails.getUsername());

            // 4. 返回 JWT (通常在响应体中)
            return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), userDetails.getAuthorities()));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
        } catch (AuthenticationException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication failed");
        }
    }

    // ... 可以添加注册等其他端点

    // DTOs
    @Data
    static class LoginRequest {
        private String username;
        private String password;
    }

    @Data
    @AllArgsConstructor
    static class JwtResponse {
        private String token;
        private Long id;
        private String username;
        private Collection<? extends GrantedAuthority> roles;
    }
}

步骤 8:保护资源

// UserController.java
@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    @GetMapping("/profile")
    @PreAuthorize("hasRole('USER')") // 方法级授权
    public ResponseEntity<User> getProfile(Authentication authentication) {
        // 从 SecurityContext 获取认证信息
        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        User user = userService.findByUsername(userDetails.getUsername());
        return ResponseEntity.ok(user);
    }

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<String> adminOnly() {
        return ResponseEntity.ok("Hello Admin!");
    }
}

步骤 9:测试

  1. 启动应用。
  2. 登录获取 JWT:
    curl -X POST http://localhost:8080/api/v1/auth/login \
         -H "Content-Type: application/json" \
         -d '{"username":"user", "password":"password"}'
    
    • 响应: {"token":"eyJhb...","id":1,"username":"user","roles":[{"authority":"ROLE_USER"}]}
  3. 使用 JWT 访问受保护资源:
    curl http://localhost:8080/api/v1/users/profile \
         -H "Authorization: Bearer eyJhb..."
    
    • 响应: 用户信息。
  4. 尝试访问无权限资源:
    curl http://localhost:8080/api/v1/users/admin \
         -H "Authorization: Bearer eyJhb..." # (user 的 token)
    
    • 响应: 403 Forbidden

三、常见错误

  1. 401 Unauthorized / 403 Forbidden:

    • 原因: 未提供 Token、Token 无效(签名错误、过期)、权限不足。
    • 解决: 检查 Authorization 头格式 (Bearer <token>);确认 JWT 未过期;检查 @PreAuthorizehasRole 配置。
  2. CSRF 相关错误 (如 Invalid CSRF Token):

    • 原因: Spring Security 默认启用 CSRF 保护,但 JWT API 通常不需要。
    • 解决:SecurityConfig.csrf().disable()
  3. No qualifying bean of type 'AuthenticationManager':

    • 原因: 未正确暴露 AuthenticationManager Bean。
    • 解决:SecurityConfig 中定义 @Bean 方法返回 AuthenticationManager
  4. JWT 签名异常 (SignatureException, MalformedJwtException):

    • 原因: 密钥不匹配、JWT 格式错误、算法不匹配。
    • 解决: 确保 SECRET_KEYJwtUtilJwts.parser() 中一致;检查 JWT 字符串是否完整。
  5. UserDetails 为空或转换错误:

    • 原因: JwtAuthenticationFiltergetUserDetailsFromToken 返回 null 或类型不匹配。
    • 解决: 确保 UserService 能根据用户名找到用户;UserDetailsImpl 实现正确。
  6. Session 相关问题 (在 JWT 中):

    • 原因: 配置了 SessionCreationPolicy.STATELESS 但仍有 Session 相关操作。
    • 解决: 确认 SecurityConfig 中设置了 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
  7. CORS 错误:

    • 原因: 前端请求跨域,后端未配置 CORS。
    • 解决: 配置 CORS。
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 或具体域名
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true); // 如果需要发送 Cookie (JWT 通常不需要)
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    • 或在 SecurityConfig 中配置:
    http.cors().and()... // 确保先配置 cors()
    

四、注意事项

  1. 密钥安全: JWT 的 SECRET_KEY 必须保密,绝不能硬编码在代码中或提交到版本控制。使用环境变量或配置中心。
  2. 密码安全: 永远不要存储明文密码。使用 BCryptPasswordEncoder 等强哈希算法。
  3. Token 存储: 客户端存储 JWT 时,避免使用 LocalStorage(易受 XSS 攻击)。考虑使用 HttpOnly Cookie(需配合 CSRF 保护)或内存存储。
  4. Token 过期: 设置合理的过期时间 (exp)。过短影响用户体验,过长增加安全风险。考虑实现 Token 刷新机制。
  5. Token 撤销: JWT 无法直接撤销。对于敏感操作或强制下线,需要引入黑名单(如 Redis 存储失效的 JWT ID jti)或缩短过期时间。
  6. 敏感信息: 不要在 JWT Payload 中存放密码、信用卡号等敏感信息。即使签名,Payload 也是可读的。
  7. 算法选择: HS256 (对称) 简单,但密钥需在所有服务间共享。RS256 (非对称) 更安全,公钥可分发,私钥保密,适合微服务。
  8. @PreAuthorize vs hasRole: hasRole('ADMIN') 会自动添加 ROLE_ 前缀。如果权限名是 ADMIN,应使用 hasAuthority('ADMIN')hasRole('ROLE_ADMIN')
  9. 过滤器顺序: JwtAuthenticationFilter 必须在 UsernamePasswordAuthenticationFilter 之前执行,以确保在表单登录前处理 JWT。
  10. 错误处理:JwtAuthenticationFilter 中捕获异常,避免因 JWT 问题导致整个请求链中断。

五、使用技巧

  1. 使用 @Value 注入密钥:

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

    application.yml:

    jwt:
      secret: ${JWT_SECRET:your-default-secret} # 使用环境变量 JWT_SECRET,否则用默认值
      expiration: 86400000
    
  2. 自定义异常处理:

    • 创建 @ControllerAdvice 处理 AuthenticationException 并返回 JSON 错误。
  3. 使用 @AuthenticationPrincipal:

    • 简化获取当前用户。
    @GetMapping("/profile")
    public ResponseEntity<User> getProfile(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        User user = userService.findByUsername(userDetails.getUsername());
        return ResponseEntity.ok(user);
    }
    
  4. 实现 Token 刷新:

    • 发放两个 Token:Access Token (短时效) 和 Refresh Token (长时效,存储在服务端)。Access Token 过期后,用 Refresh Token 申请新的 Access Token
  5. 使用 jti (JWT ID):

    • 在生成 JWT 时添加唯一 jti,便于追踪和在黑名单中标识特定 Token。
  6. 配置 ignored 路径:

    • SecurityConfig 中配置 .requestMatchers("/static/**", "/favicon.ico").permitAll() 等。
  7. 使用 WebSecurity 忽略静态资源:

    @Configuration
    public class WebSecurityConfig {
        @Bean
        public WebSecurityCustomizer webSecurityCustomizer() {
            return (web) -> web.ignoring().requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/webjars/**");
        }
    }
    
    • 注意: WebSecurity.ignoring() 的路径完全绕过 Spring Security 过滤器链。对于需要认证的 API,应使用 HttpSecurity.authorizeHttpRequests()
  8. 集成 OAuth2 / OpenID Connect:

    • 使用 spring-boot-starter-oauth2-clientspring-boot-starter-oauth2-resource-server 实现社会化登录或保护资源服务器。

六、最佳实践

  1. 最小权限原则: 用户和角色只授予完成任务所必需的最小权限。
  2. 使用 HTTPS: 所有涉及认证和敏感数据的通信必须使用 HTTPS,防止中间人攻击和 Token 窃取。
  3. 安全的密钥管理: 使用密钥管理服务 (KMS) 或配置中心管理密钥。
  4. 输入验证: 对所有用户输入(包括登录凭据)进行严格验证和清理。
  5. 日志与监控: 记录登录成功/失败、权限拒绝等安全事件。监控异常登录行为。
  6. 定期轮换密钥: 定期更换 JWT 签名密钥。
  7. 清晰的文档: 使用 Swagger UI 明确标注需要认证的端点和所需的权限。
  8. 前端安全: 前端应用也应实施安全措施,如防止 XSS、CSRF。
  9. 依赖更新: 及时更新 Spring Security、JJWT 等安全相关依赖,修复已知漏洞。
  10. 安全审计: 定期进行安全代码审计和渗透测试。

七、性能优化

  1. JWT 验证性能: JWT 签名验证(尤其是非对称算法 RS256)比简单的 Token 查询开销大。确保服务器有足够的计算资源。
  2. 减少 Payload 大小: JWT 越大,传输和解析开销越大。只在 Payload 中存放必要信息。
  3. 缓存用户信息: 虽然 JWT 无状态,但 JwtAuthenticationFiltergetUserDetailsFromToken 可能需要查询数据库获取用户角色等完整信息。可以缓存 UserDetails 对象(如使用 @Cacheable)。
  4. 选择合适的算法: HS256RS256 验证更快。在性能要求极高且服务间信任度高的场景,可考虑 HS256
  5. 连接池: 确保数据库连接池(如 HikariCP)配置合理。
  6. 异步处理: 对于复杂的授权逻辑,考虑异步执行。
  7. CDN: 静态资源(CSS, JS, Images)使用 CDN 加速,减轻应用服务器负担。
  8. 负载均衡: 在高并发场景,使用负载均衡器分发请求。
  9. 监控与调优: 使用 APM 工具监控认证相关接口的性能瓶颈。

总结: Spring Boot 结合 Spring Security 和 JWT 提供了强大且灵活的安全解决方案。理解 Session、Token、JWT 的核心区别是基础。实践中,应优先考虑 JWT 用于 API 认证,注重密钥安全、密码安全和合理的 Token 管理策略。通过 SecurityConfig 精确控制访问权限,利用 @PreAuthorize 进行方法级保护。遵循安全最佳实践,可以构建出既安全又高效的 Web 应用。记住,安全是一个持续的过程,需要不断关注和更新。