认证机制
认证(Authentication)是确认用户身份的过程,回答"你是谁"这个问题。Spring Security 提供了丰富的认证机制支持,包括表单登录、HTTP Basic、记住我等功能。本章将深入讲解各种认证方式的配置和原理。
认证核心概念
Authentication 对象
在 Spring Security 中,认证结果被封装在 Authentication 对象中,它包含三个核心信息:
- principal:主体,通常是
UserDetails实现,代表已认证的用户 - credentials:凭证,通常是密码,认证成功后会被清除
- authorities:权限列表,用户拥有的角色和权限
// Authentication 接口的核心方法
public interface Authentication extends Principal {
Collection<? extends GrantedAuthority> getAuthorities(); // 权限列表
Object getCredentials(); // 凭证
Object getDetails(); // 详细信息
Object getPrincipal(); // 主体
boolean isAuthenticated(); // 是否已认证
void setAuthenticated(boolean authenticated);
}
认证流程
Spring Security 的认证流程涉及多个组件协作:
- 用户提交认证信息(用户名、密码)
- 认证过滤器创建
Authentication令牌(未认证状态) AuthenticationManager处理认证请求AuthenticationProvider执行实际认证逻辑- 认证成功,返回已认证的
Authentication对象 - 将认证信息存入
SecurityContext
// 典型的认证代码流程
UsernamePasswordAuthenticationToken token =
UsernamePasswordAuthenticationToken.unauthenticated(username, password);
Authentication authentication = authenticationManager.authenticate(token);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
表单登录
表单登录是最常见的认证方式,用户通过 HTML 表单提交用户名和密码。
基本配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // 自定义登录页面
.loginProcessingUrl("/login") // 表单提交 URL
.defaultSuccessUrl("/home") // 登录成功默认跳转
.failureUrl("/login?error") // 登录失败跳转
.permitAll()
);
return http.build();
}
自定义登录成功处理
登录成功后的行为可以通过 AuthenticationSuccessHandler 自定义:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.successHandler(new CustomAuthenticationSuccessHandler())
);
return http.build();
}
public class CustomAuthenticationSuccessHandler
implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException {
// 根据用户角色重定向到不同页面
Collection<? extends GrantedAuthority> authorities =
authentication.getAuthorities();
String targetUrl = "/home"; // 默认页面
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals("ROLE_ADMIN")) {
targetUrl = "/admin/dashboard";
break;
}
}
response.sendRedirect(targetUrl);
}
}
自定义登录失败处理
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form
.failureHandler(new CustomAuthenticationFailureHandler())
);
return http.build();
}
public class CustomAuthenticationFailureHandler
implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException {
String errorMessage = "认证失败";
// 根据异常类型返回不同错误信息
if (exception instanceof BadCredentialsException) {
errorMessage = "用户名或密码错误";
} else if (exception instanceof DisabledException) {
errorMessage = "账户已被禁用";
} else if (exception instanceof AccountExpiredException) {
errorMessage = "账户已过期";
} else if (exception instanceof CredentialsExpiredException) {
errorMessage = "密码已过期";
} else if (exception instanceof LockedException) {
errorMessage = "账户已被锁定";
}
// 返回 JSON 格式错误(适合前后端分离)
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"error\": \"" + errorMessage + "\"}"
);
}
}
HTTP Basic 认证
HTTP Basic 认证是一种简单的认证方式,浏览器会在请求头中携带 Base64 编码的用户名密码。
配置方式
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.httpBasic(basic -> basic
.realmName("My Application") // 设置认证域
);
return http.build();
}
工作原理
- 用户访问受保护资源
- 服务器返回 401 状态码,响应头包含
WWW-Authenticate: Basic realm="..." - 浏览器弹出登录对话框
- 用户输入凭据,浏览器在后续请求头中携带
Authorization: Basic base64(username:password)
HTTP Basic 认证适合简单的 API 保护或内部工具,不建议用于面向公网的应用,因为密码只是 Base64 编码,容易被截获。
用户存储配置
用户存储定义了如何获取用户信息来验证身份。Spring Security 支持多种用户存储方式。
内存用户存储
适合快速原型开发和测试:
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.builder()
.username("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build());
manager.createUser(User.builder()
.username("admin")
.password(passwordEncoder.encode("admin"))
.roles("USER", "ADMIN")
.build());
return manager;
}
数据库用户存储
生产环境通常从数据库读取用户信息:
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
// 使用 JDBC 用户存储
JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
// 自定义用户查询 SQL
manager.setUsersByUsernameQuery(
"SELECT username, password, enabled FROM users WHERE username = ?"
);
// 自定义权限查询 SQL
manager.setAuthoritiesByUsernameQuery(
"SELECT username, authority FROM authorities WHERE username = ?"
);
return manager;
}
}
默认的数据库表结构:
-- 用户表
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL
);
-- 权限表
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users (username)
);
-- 创建唯一索引
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);
自定义用户存储
当需要从非标准来源获取用户信息时,可以实现自己的 UserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException("用户不存在: " + username));
// 获取用户角色
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
user.isAccountNonExpired(),
user.isCredentialsNonExpired(),
user.isAccountNonLocked(),
authorities
);
}
}
自定义 UserDetails
为了存储更多用户信息,可以实现自定义的 UserDetails:
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return user.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return user.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return user.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return user.isEnabled();
}
// 额外的用户信息
public Long getId() {
return user.getId();
}
public String getEmail() {
return user.getEmail();
}
public User getUser() {
return user;
}
}
在控制器中获取当前用户信息:
@GetMapping("/profile")
public String profile(Authentication authentication, Model model) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
model.addAttribute("user", userDetails.getUser());
return "profile";
}
// 或者使用注解方式
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal CustomUserDetails userDetails, Model model) {
model.addAttribute("user", userDetails.getUser());
return "profile";
}
AuthenticationManager 配置
发布 AuthenticationManager Bean
当需要在控制器或服务中手动进行认证时,需要配置 AuthenticationManager Bean:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return new ProviderManager(provider);
}
}
自定义认证端点
在 REST API 场景中,可能需要自定义认证端点:
@RestController
public class AuthenticationController {
private final AuthenticationManager authenticationManager;
private final SecurityContextHolderStrategy securityContextHolderStrategy;
private final SecurityContextRepository securityContextRepository;
public AuthenticationController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
this.securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();
this.securityContextRepository = new HttpSessionSecurityContextRepository();
}
@PostMapping("/api/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request,
HttpServletRequest httpRequest,
HttpServletResponse httpResponse) {
// 1. 创建认证令牌
UsernamePasswordAuthenticationToken token =
UsernamePasswordAuthenticationToken.unauthenticated(
request.username(),
request.password()
);
// 2. 执行认证
Authentication authentication = authenticationManager.authenticate(token);
// 3. 保存安全上下文
SecurityContext context = securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authentication);
securityContextHolderStrategy.setContext(context);
securityContextRepository.saveContext(context, httpRequest, httpResponse);
// 4. 返回用户信息
return ResponseEntity.ok(new LoginResponse(
authentication.getName(),
authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
));
}
public record LoginRequest(String username, String password) {}
public record LoginResponse(String username, List<String> authorities) {}
}
Remember Me 记住我
"记住我"功能允许用户在关闭浏览器后仍然保持登录状态。
基本配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.formLogin(form -> form
.permitAll()
)
.rememberMe(remember -> remember
.key("uniqueAndSecret") // 加密密钥
.tokenValiditySeconds(86400 * 14) // 有效期 14 天
.userDetailsService(userDetailsService)
);
return http.build();
}
持久化 Token
默认使用内存存储 Token,重启后失效。可以配置持久化存储:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
DataSource dataSource) throws Exception {
http
.rememberMe(remember -> remember
.tokenRepository(persistentTokenRepository(dataSource))
.tokenValiditySeconds(86400 * 14)
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}
需要的数据库表结构:
CREATE TABLE persistent_logins (
username VARCHAR(64) NOT NULL,
series VARCHAR(64) PRIMARY KEY,
token VARCHAR(64) NOT NULL,
last_used TIMESTAMP NOT NULL
);
登录表单添加 Remember Me
<form th:action="@{/login}" method="post">
<input type="text" name="username" />
<input type="password" name="password" />
<input type="checkbox" name="remember-me" /> 记住我
<button type="submit">登录</button>
</form>
注销登录
配置注销
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout(logout -> logout
.logoutUrl("/logout") // 注销 URL
.logoutSuccessUrl("/login?logout") // 注销成功跳转
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) // 自定义处理
.invalidateHttpSession(true) // 使 Session 失效
.deleteCookies("JSESSIONID") // 删除 Cookie
.clearAuthentication(true) // 清除认证信息
);
return http.build();
}
自定义注销成功处理
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException {
// 记录注销日志
if (authentication != null) {
System.out.println("用户 " + authentication.getName() + " 已注销");
}
// 返回 JSON 响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\": \"注销成功\"}");
}
}
小结
本章详细介绍了 Spring Security 的认证机制:
- 认证流程涉及
Authentication、AuthenticationManager、AuthenticationProvider等核心组件 - 表单登录是最常用的认证方式,可以自定义成功和失败处理
- HTTP Basic 认证简单但安全性较低,适合 API 或内部工具
- 用户存储支持内存、数据库、自定义实现
- Remember Me 功能实现持久化登录
- 注销登录可以清理 Session 和 Cookie
理解认证机制是构建安全应用的基础。下一章将深入学习授权控制,了解如何控制用户能访问哪些资源。