31-Spring Security 认证机制详解

2 阅读7分钟

Spring Security 认证机制详解

本章导读

认证是应用安全的第一道防线,也是 Spring Security 框架的核心基础。本章深入剖析 Spring Security 的认证机制原理,从传统的表单登录到现代的 JWT 无状态认证,帮助你理解认证流程的完整链路,掌握多种认证方式的实现方法。

学习目标

  • 目标1:理解 Spring Security 认证流程和核心组件职责
  • 目标2:掌握表单登录、JWT、OAuth2 等多种认证方式的实现
  • 目标3:能够自定义认证提供者和用户详情服务,解决实际认证需求

前置知识:Spring Boot 基础、HTTP 协议基础、Java Servlet 规范

阅读时长:约 45 分钟

一、知识概述

认证(Authentication)是安全框架的基础,用于验证用户身份的合法性。Spring Security 提供了完整的认证体系,支持多种认证方式,包括表单登录、HTTP Basic、OAuth2、JWT 等。

认证的核心概念:

  • Principal:认证主体(用户)
  • Credentials:认证凭证(密码)
  • Authentication:认证对象
  • AuthenticationManager:认证管理器

理解认证机制的原理,是构建安全应用的基础。

二、知识点详细讲解

2.1 认证流程

用户请求
    │
    ▼
AuthenticationFilter(认证过滤器)
    │
    ├── 提取认证信息
    │
    ▼
AuthenticationManager(认证管理器)
    │
    ├── 调用 Provider 进行认证
    │
    ▼
AuthenticationProvider(认证提供者)
    │
    ├── 调用 UserDetailsService 加载用户
    │
    ▼
UserDetailsService(用户详情服务)
    │
    ├── 查询数据库获取用户信息
    │
    ▼
PasswordEncoder(密码编码器)
    │
    ├── 验证密码
    │
    ▼
认证成功/失败
    │
    ├── 成功:存储 Authentication 到 SecurityContext
    │
    └── 失败:抛出 AuthenticationException

2.2 核心组件

SecurityContext
  • 存储当前用户的认证信息
  • 通过 SecurityContextHolder 访问
Authentication
  • 代表认证对象
  • 包含 Principal、Credentials、Authorities
AuthenticationManager
  • 认证管理器
  • 协调认证过程
AuthenticationProvider
  • 实际执行认证
  • 支持不同的认证方式

2.3 认证方式对比

方式特点适用场景
表单登录用户名密码传统 Web 应用
HTTP Basic简单认证API 测试
JWT Token无状态前后端分离
OAuth2第三方授权社交登录
LDAP企业目录企业内部应用

三、代码示例

3.1 基础安全配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 授权配置
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // 表单登录
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            )
            // 登出配置
            .logout(logout -> logout
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );
        
        return http.build();
    }
}

3.2 自定义用户认证

import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> 
                new UsernameNotFoundException("用户不存在: " + username));
        
        // 转换为 UserDetails
        return new CustomUserDetails(user);
    }
}

// 自定义 UserDetails
public class CustomUserDetails implements UserDetails {
    
    private final User user;
    private final List<GrantedAuthority> authorities;
    
    public CustomUserDetails(User user) {
        this.user = user;
        this.authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
    }
    
    @Override
    public String getUsername() { return user.getUsername(); }
    
    @Override
    public String getPassword() { return user.getPassword(); }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    
    @Override
    public boolean isAccountNonExpired() { return true; }
    
    @Override
    public boolean isAccountNonLocked() { return !user.isLocked(); }
    
    @Override
    public boolean isCredentialsNonExpired() { return true; }
    
    @Override
    public boolean isEnabled() { return user.isEnabled(); }
    
    public User getUser() { return user; }
}

3.3 密码编码器

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.*;

@Configuration
public class PasswordEncoderConfig {
    
    // BCrypt 编码器(推荐)
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 委托编码器(支持多种编码)
    @Bean
    public PasswordEncoder delegatingPasswordEncoder() {
        String idForEncode = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("argon2", new Argon2PasswordEncoder());
        
        return new DelegatingPasswordEncoder(idForEncode, encoders);
    }
}
// 使用示例
@Service
public class UserService {
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private UserRepository userRepository;
    
    public void registerUser(String username, String password) {
        // 加密密码
        String encodedPassword = passwordEncoder.encode(password);
        
        User user = new User();
        user.setUsername(username);
        user.setPassword(encodedPassword);
        
        userRepository.save(user);
    }
    
    public boolean verifyPassword(String rawPassword, String encodedPassword) {
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

3.4 JWT 认证

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.*;

@Component
public class JwtTokenProvider {
    
    private final SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private final long expiration = 86400000;  // 24小时
    
    // 生成 Token
    public String generateToken(String username, List<String> roles) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);
        
        return Jwts.builder()
            .setSubject(username)
            .claim("roles", roles)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(secretKey)
            .compact();
    }
    
    // 解析 Token
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
        
        return claims.getSubject();
    }
    
    // 验证 Token
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
    
    // 获取权限
    public List<String> getRolesFromToken(String token) {
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .build()
            .parseClaimsJws(token)
            .getBody();
        
        return claims.get("roles", List.class);
    }
}
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.*;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request, 
            HttpServletResponse response, 
            FilterChain filterChain) throws ServletException, IOException {
        
        // 提取 Token
        String token = extractToken(request);
        
        if (token != null && tokenProvider.validateToken(token)) {
            // 获取用户名
            String username = tokenProvider.getUsernameFromToken(token);
            
            // 加载用户信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            // 创建认证对象
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
            
            // 存储到 SecurityContext
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String extractToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}

3.5 登录接口

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            // 认证
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(), 
                    request.getPassword()
                )
            );
            
            // 生成 Token
            List<String> roles = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
            
            String token = tokenProvider.generateToken(
                request.getUsername(), roles);
            
            return ResponseEntity.ok(new LoginResponse(token));
            
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body("用户名或密码错误");
        }
    }
    
    @PostMapping("/logout")
    public ResponseEntity<?> logout() {
        // JWT 无状态,客户端删除 Token 即可
        return ResponseEntity.ok("登出成功");
    }
    
    @GetMapping("/me")
    public ResponseEntity<?> getCurrentUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        if (auth != null && auth.getPrincipal() instanceof UserDetails) {
            UserDetails userDetails = (UserDetails) auth.getPrincipal();
            return ResponseEntity.ok(userDetails);
        }
        
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}

3.6 多种认证方式

@Configuration
@EnableWebSecurity
public class MultiAuthConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            // 表单登录
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
            )
            // HTTP Basic 认证
            .httpBasic(basic -> {})
            // OAuth2 登录
            .oauth2Login(oauth -> oauth
                .loginPage("/oauth2/authorization")
                .defaultSuccessUrl("/home")
            )
            // 记住我
            .rememberMe(remember -> remember
                .key("uniqueAndSecret")
                .tokenValiditySeconds(86400)
            );
        
        return http.build();
    }
}

3.7 自定义认证提供者

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        
        // 加载用户
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        // 验证密码
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
        
        // 检查账户状态
        if (!userDetails.isAccountNonLocked()) {
            throw new LockedException("账户已锁定");
        }
        
        if (!userDetails.isEnabled()) {
            throw new DisabledException("账户已禁用");
        }
        
        // 返回认证对象
        return new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities());
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

四、实战应用场景

4.1 手机号验证码登录

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.stereotype.Component;

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private SmsService smsService;
    
    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        
        String phone = authentication.getName();
        String code = authentication.getCredentials().toString();
        
        // 验证短信验证码
        if (!smsService.verifyCode(phone, code)) {
            throw new BadCredentialsException("验证码错误");
        }
        
        // 查找或创建用户
        User user = userService.findByPhoneOrCreate(phone);
        
        // 返回认证对象
        return new SmsAuthenticationToken(user, null, 
            user.getAuthorities());
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

// 自定义 Token
public class SmsAuthenticationToken extends UsernamePasswordAuthenticationToken {
    public SmsAuthenticationToken(Object principal, Object credentials, 
                                 Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

4.2 登录失败处理

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.*;
import org.springframework.stereotype.Component;
import javax.servlet.*;

@Component
public class CustomAuthenticationFailureHandler 
        implements AuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(
            HttpServletRequest request,
            HttpServletResponse response,
            AuthenticationException exception) throws IOException {
        
        String message;
        
        if (exception instanceof BadCredentialsException) {
            message = "用户名或密码错误";
        } else if (exception instanceof LockedException) {
            message = "账户已锁定";
        } else if (exception instanceof DisabledException) {
            message = "账户已禁用";
        } else if (exception instanceof AccountExpiredException) {
            message = "账户已过期";
        } else {
            message = "登录失败";
        }
        
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", message);
        
        response.getWriter().write(new ObjectMapper().writeValueAsString(result));
    }
}

五、总结与最佳实践

认证方式选择

场景推荐
传统 Web表单登录 + Session
前后端分离JWT Token
微服务OAuth2 + JWT
企业内部LDAP

最佳实践

  1. 密码安全:使用强加密算法
  2. Token 安全:设置合理的过期时间
  3. 失败处理:提供友好的错误提示
  4. 日志审计:记录登录行为

Spring Security 认证机制是安全框架的基础,掌握其原理和配置,能够构建出安全可靠的应用系统。


六、思考与练习

思考题

  1. 基础题:AuthenticationManager、AuthenticationProvider、UserDetailsService 三者在认证流程中各自的职责是什么?它们之间是如何协作的?

  2. 进阶题:JWT 无状态认证与传统的 Session 认证各有什么优缺点?在什么场景下应该选择 JWT?什么场景下应该选择 Session?

  3. 实战题:如何设计一个支持多种认证方式(用户名密码、手机验证码、第三方登录)的统一认证系统?需要注意哪些安全问题?

编程练习

练习:实现一个基于 JWT 的用户认证系统,要求:

  1. 用户注册接口(密码加密存储)
  2. 用户登录接口(返回 JWT Token)
  3. 用户信息查询接口(需要认证)
  4. Token 刷新机制
  5. 登录失败次数限制(超过 5 次锁定账户)

章节关联

  • 前置章节:无(基础章节)
  • 后续章节:《授权模型详解》- 在掌握认证机制后,深入学习权限控制
  • 扩展阅读

📝 下一章预告

下一章将深入讲解 Spring Security 的授权模型,包括权限表达式、方法级安全控制、自定义权限决策等内容,帮助你构建细粒度的权限控制体系。


本章完