跳到主要内容

认证机制

认证(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 的认证流程涉及多个组件协作:

  1. 用户提交认证信息(用户名、密码)
  2. 认证过滤器创建 Authentication 令牌(未认证状态)
  3. AuthenticationManager 处理认证请求
  4. AuthenticationProvider 执行实际认证逻辑
  5. 认证成功,返回已认证的 Authentication 对象
  6. 将认证信息存入 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();
}

工作原理

  1. 用户访问受保护资源
  2. 服务器返回 401 状态码,响应头包含 WWW-Authenticate: Basic realm="..."
  3. 浏览器弹出登录对话框
  4. 用户输入凭据,浏览器在后续请求头中携带 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 的认证机制:

  • 认证流程涉及 AuthenticationAuthenticationManagerAuthenticationProvider 等核心组件
  • 表单登录是最常用的认证方式,可以自定义成功和失败处理
  • HTTP Basic 认证简单但安全性较低,适合 API 或内部工具
  • 用户存储支持内存、数据库、自定义实现
  • Remember Me 功能实现持久化登录
  • 注销登录可以清理 Session 和 Cookie

理解认证机制是构建安全应用的基础。下一章将深入学习授权控制,了解如何控制用户能访问哪些资源。