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 |
最佳实践
- 密码安全:使用强加密算法
- Token 安全:设置合理的过期时间
- 失败处理:提供友好的错误提示
- 日志审计:记录登录行为
Spring Security 认证机制是安全框架的基础,掌握其原理和配置,能够构建出安全可靠的应用系统。
六、思考与练习
思考题
-
基础题:AuthenticationManager、AuthenticationProvider、UserDetailsService 三者在认证流程中各自的职责是什么?它们之间是如何协作的?
-
进阶题:JWT 无状态认证与传统的 Session 认证各有什么优缺点?在什么场景下应该选择 JWT?什么场景下应该选择 Session?
-
实战题:如何设计一个支持多种认证方式(用户名密码、手机验证码、第三方登录)的统一认证系统?需要注意哪些安全问题?
编程练习
练习:实现一个基于 JWT 的用户认证系统,要求:
- 用户注册接口(密码加密存储)
- 用户登录接口(返回 JWT Token)
- 用户信息查询接口(需要认证)
- Token 刷新机制
- 登录失败次数限制(超过 5 次锁定账户)
章节关联
- 前置章节:无(基础章节)
- 后续章节:《授权模型详解》- 在掌握认证机制后,深入学习权限控制
- 扩展阅读:
- OAuth2.0 规范:oauth.net/2/
- JWT 官网:jwt.io/
- Spring Security 官方文档:docs.spring.io/spring-secu…
📝 下一章预告
下一章将深入讲解 Spring Security 的授权模型,包括权限表达式、方法级安全控制、自定义权限决策等内容,帮助你构建细粒度的权限控制体系。
本章完