Authentication
Authentication 是 Spring Security 认证体系的核心数据载体,它统一表示"从用户提交凭据到认证成功后的主体信息"这一完整过程。在手写登录逻辑时,开发者往往自行定义 UserSession 类,各项目写法参差不齐——有的存用户名、有的存密码、有的存权限、有的存登录时间,导致混乱和安全隐患。Spring Security 用 Authentication 接口统一封装了认证全过程,无论是认证前的未验证 Token,还是认证后的完整主体信息,都用同一个抽象表示。
定义与作用
Authentication 接口继承自 java.security.Principal,定义了以下核心方法:
| 方法 | 说明 | 认证前 | 认证后 |
|---|---|---|---|
getPrincipal() | 用户主体 | 通常是用户名(String) | 通常是 UserDetails 对象 |
getCredentials() | 凭据 | 密码(String) | 通常被擦除(null) |
getAuthorities() | 权限列表 | 空集合(Collections.emptyList()) | 填充用户的角色/权限 |
getDetails() | 额外详情 | 可能包含 IP 地址、Session ID 等 | 保持原样或补充 |
isAuthenticated() | 认证状态 | false | true |
setAuthenticated(boolean) | 设置认证状态 | 框架内部调用 | 外部调用可能抛异常 |
为什么是统一接口?
在手写登录系统中,开发者通常这样做:
// 手写做法:混乱且不统一
HttpSession session = request.getSession();
session.setAttribute("user", username); // 项目A这么写
session.setAttribute("loginUser", userObj); // 项目B这么写
session.setAttribute("currentUser", userId); // 项目C这么写
session.setAttribute("roles", roleList); // 有的单独存权限
Spring Security 统一用 Authentication 表示:
- 认证前(提交凭据):
UsernamePasswordAuthenticationToken是Authentication的实现,此时principal是用户名,credentials是密码,isAuthenticated = false。 - 认证后(验证通过):
DaoAuthenticationProvider新建另一个UsernamePasswordAuthenticationToken,此时principal是UserDetails对象,credentials被擦除,authorities已填充,isAuthenticated = true。
这种统一带来了类型安全、代码一致、安全可审计三大好处。
核心方法详解
getPrincipal():认证前后类型变化
认证前(未验证的 Token):
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken("zhangsan", "123456");
// token.getPrincipal() → "zhangsan"(String 类型)
// token.getCredentials() → "123456"(String 类型)
// token.isAuthenticated() → false
认证后(验证通过):
// DaoAuthenticationProvider 验证通过后新建的 Token
UsernamePasswordAuthenticationToken authenticatedToken =
new UsernamePasswordAuthenticationToken(
userDetails, // principal → UserDetails 对象
null, // credentials → 已擦除
userDetails.getAuthorities() // authorities → 权限列表
);
// authenticatedToken.getPrincipal() → UserDetails 对象
// authenticatedToken.isAuthenticated() → true
getCredentials():认证后擦除的安全设计
认证成功后,DaoAuthenticationProvider 默认会调用 eraseCredentials() 擦除密码:
// 认证成功后,credentials 被擦除
System.out.println(auth.getCredentials()); // 输出 null(不是 Bug,是安全设计)
安全原因:
Authentication对象会被存入SecurityContextHolder,并可能在请求结束后被持久化到 Session。如果密码留在credentials中,任何能访问 Session 的代码(包括日志、调试、序列化)都可能意外泄露明文密码。
getAuthorities():权限列表的填充时机
权限列表只有在认证成功后才有值:
// 认证前
Collection<? extends GrantedAuthority> authorities = token.getAuthorities();
// authorities → 空集合(不可修改的空列表)
// 认证后
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
// authorities → [ROLE_STUDENT, ROLE_USER](从 UserDetails 复制)
getDetails():额外上下文信息
getDetails() 通常用于存储认证过程中的额外信息,例如:
WebAuthenticationDetails:包含请求 IP 地址、Session ID 等- 自定义
AuthenticationDetailsSource生成的对象
// 默认的 WebAuthenticationDetails 包含 IP 和 SessionId
Object details = auth.getDetails();
if (details instanceof WebAuthenticationDetails) {
WebAuthenticationDetails webDetails = (WebAuthenticationDetails) details;
System.out.println("IP: " + webDetails.getRemoteAddress());
System.out.println("SessionId: " + webDetails.getSessionId());
}
setAuthenticated(boolean):谨慎使用
Spring Security 对外部调用 setAuthenticated(true) 持严格警惕态度:
// ❌ 错误:自定义 Token 时直接设为 true,可能触发安全警告
token.setAuthenticated(true);
// 异常:Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead
正确做法:使用带 authorities 参数的构造函数:
// ✅ 正确:通过构造函数标记已认证状态
new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
核心原理:认证状态变化完整链路
Spring Security 的认证状态变化遵循严格的链路:
步骤 1:用户提交凭据
用户提交表单(POST /login),请求参数 username=zhangsan&password=123456。
步骤 2:过滤器构造未认证 Token
UsernamePasswordAuthenticationFilter 从请求中提取参数,构造未认证的 UsernamePasswordAuthenticationToken:
// 源码示意(UsernamePasswordAuthenticationFilter)
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(
username, // principal
password // credentials
);
// authRequest.isAuthenticated() → false(默认)
步骤 3:AuthenticationManager 调度认证
AuthenticationManager(通常是 ProviderManager)遍历 AuthenticationProvider 列表,找到支持 UsernamePasswordAuthenticationToken 的 DaoAuthenticationProvider:
// ProviderManager 核心逻辑(简化)
for (AuthenticationProvider provider : getProviders()) {
if (provider.supports(authRequest.getClass())) {
Authentication result = provider.authenticate(authRequest);
if (result != null) {
return result; // 认证成功,返回新的 Authentication
}
}
}
步骤 4:DaoAuthenticationProvider 执行验证
DaoAuthenticationProvider 完成实际验证:
- 调用
UserDetailsService.loadUserByUsername(username)加载UserDetails - 预检查:账户是否锁定、过期、禁用
- 调用
PasswordEncoder.matches(rawPassword, encodedPassword)比对密码 - 通过后检查:凭据是否过期
- 新建已认证的
UsernamePasswordAuthenticationToken(含 authorities)
// DaoAuthenticationProvider 验证成功后的构造(简化)
UsernamePasswordAuthenticationToken result =
new UsernamePasswordAuthenticationToken(
userDetails, // principal → UserDetails
null, // credentials → 擦除
userDetails.getAuthorities() // authorities → 权限列表
);
result.setDetails(authRequest.getDetails()); // 保留原 details
// result.isAuthenticated() → true
步骤 5:存入 SecurityContextHolder
认证成功后,结果存入 SecurityContextHolder:
SecurityContextHolder.getContext().setAuthentication(result);
步骤 6:请求结束后持久化到 Session
SecurityContextPersistenceFilter(或 SecurityContextHolderFilter)在请求结束时,将 SecurityContext 中的 Authentication 持久化到 HttpSession:
// 请求结束后,SecurityContext 被存入 Session
HttpSession session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
下次请求时,过滤器从 Session 中加载 SecurityContext,恢复 Authentication。
Authentication 状态变化时序图
以下时序图展示认证前与认证后各字段的取值变化:
状态对比表
| 字段 | 认证前(Token) | 认证后(已认证) | 说明 |
|---|---|---|---|
principal | "zhangsan"(String) | UserDetails 对象 | 类型从字符串变为完整用户对象 |
credentials | "123456"(String) | null | 安全擦除,防止泄露 |
authorities | [](空集合) | [ROLE_STUDENT, ...] | 从 UserDetails 复制 |
details | WebAuthenticationDetails | WebAuthenticationDetails | 保留原上下文信息 |
isAuthenticated | false | true | 标记认证状态 |
完整示例
示例一:从 Controller 中获取当前 Authentication 并判断状态
场景:学生管理系统,用户访问个人中心时判断登录状态,并打印认证信息。
操作前:手写 Session 检查,各项目写法不一致:
// ❌ 手写做法:混乱、不安全、不可移植
@GetMapping("/profile")
public String profile(HttpSession session) {
// 项目A:session.getAttribute("user")
// 项目B:session.getAttribute("loginUser")
// 项目C:session.getAttribute("currentUser")
Object user = session.getAttribute("user");
if (user == null) {
return "redirect:/login";
}
// 权限检查?每个项目自己实现,逻辑不同
return "profile";
}
操作后:统一通过 SecurityContextHolder 获取:
@RestController
@RequestMapping("/student")
public class StudentProfileController {
@GetMapping("/profile")
public Map<String, Object> getProfile() {
// 统一获取当前 Authentication(无论认证方式)
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Map<String, Object> result = new HashMap<>();
// 1. 判断认证状态
result.put("isAuthenticated", auth.isAuthenticated());
result.put("authClass", auth.getClass().getSimpleName());
// 2. 获取 principal 类型
Object principal = auth.getPrincipal();
result.put("principalType", principal.getClass().getSimpleName());
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
result.put("username", userDetails.getUsername());
result.put("accountNonExpired", userDetails.isAccountNonExpired());
result.put("accountNonLocked", userDetails.isAccountNonLocked());
} else {
// 匿名用户或 JWT 场景可能返回 String
result.put("username", principal.toString());
}
// 3. 获取权限列表
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
List<String> authorityList = authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
result.put("authorities", authorityList);
// 4. credentials(认证后已被擦除)
result.put("credentials", auth.getCredentials()); // 通常为 null
// 5. details(额外上下文)
Object details = auth.getDetails();
if (details instanceof WebAuthenticationDetails) {
WebAuthenticationDetails webDetails = (WebAuthenticationDetails) details;
result.put("remoteAddress", webDetails.getRemoteAddress());
result.put("sessionId", webDetails.getSessionId());
}
return result;
}
}
输出结果示例(已认证用户):
{
"isAuthenticated": true,
"authClass": "UsernamePasswordAuthenticationToken",
"principalType": "User",
"username": "zhangsan",
"accountNonExpired": true,
"accountNonLocked": true,
"authorities": ["ROLE_STUDENT", "ROLE_USER"],
"credentials": null,
"remoteAddress": "192.168.1.100",
"sessionId": "A7B3C9D2E1F4G5H6"
}
输出结果示例(匿名用户):
{
"isAuthenticated": true,
"authClass": "AnonymousAuthenticationToken",
"principalType": "String",
"username": "anonymousUser",
"authorities": ["ROLE_ANONYMOUS"],
"credentials": null
}
关键结论:
SecurityContextHolder.getContext().getAuthentication()是统一入口,无论用户通过表单登录、HTTP Basic、JWT 还是 OAuth2 登录,返回的都是Authentication对象,代码完全通用。
示例二:手动构造 Authentication 并设置到 SecurityContext(模拟登录/测试场景)
场景:后台定时任务需要以管理员身份执行敏感操作(如数据清理、统计报表生成),但任务本身没有用户请求上下文,需要手动构造 Authentication。
操作前:无安全上下文,方法调用被 @PreAuthorize 拦截:
@Service
public class DataCleanupService {
@PreAuthorize("hasRole('ADMIN')")
public void cleanupOldRecords() {
// 清理 90 天前的记录
}
}
// 定时任务中直接调用
@Component
public class CleanupJob {
@Autowired
private DataCleanupService cleanupService;
@Scheduled(cron = "0 0 2 * * ?")
public void run() {
cleanupService.cleanupOldRecords();
// ❌ 抛出 AccessDeniedException:没有 Authentication 上下文
}
}
操作后:手动构造 UsernamePasswordAuthenticationToken 并设置到 SecurityContextHolder:
@Component
public class CleanupJob {
@Autowired
private DataCleanupService cleanupService;
@Scheduled(cron = "0 0 2 * * ?")
public void run() {
// 1. 构造管理员权限列表
List<GrantedAuthority> authorities = Arrays.asList(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_SYSTEM")
);
// 2. 构造已认证的 Authentication Token
// 注意:使用带 authorities 的构造函数,自动标记 isAuthenticated = true
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
"system_admin", // principal(可以是用户名或 UserDetails)
null, // credentials(无需密码,设为 null)
authorities // 权限列表
);
// 3. 可选:填充额外详情
authToken.setDetails("ScheduledTask: DataCleanupJob");
// 4. 存入 SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authToken);
try {
// 5. 执行受保护的方法
cleanupService.cleanupOldRecords();
System.out.println("数据清理完成");
} finally {
// 6. ✅ 必须清理上下文!防止泄漏到后续线程或任务
SecurityContextHolder.clearContext();
System.out.println("SecurityContext 已清理");
}
}
}
结果分析:
- 方法调用成功:
@PreAuthorize("hasRole('ADMIN')")检查通过,因为Authentication包含ROLE_ADMIN。 try-finally确保清理:SecurityContextHolder.clearContext()是强制步骤,如果忘记清理,当前线程的SecurityContext会残留管理员权限,导致后续任务(或线程池复用时的其他请求)意外拥有管理员权限,造成严重安全漏洞。- 无密码风险:手动构造时
credentials设为null,不会泄露密码。
易错场景与面试考点
易错场景一:认证后 getCredentials() 返回 null,以为是 Bug
现象:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth.getCredentials()); // 输出 null
// 开发者疑惑:"密码怎么不见了?是不是认证失败了?"
原因:DaoAuthenticationProvider 的 eraseCredentials() 机制。认证成功后,Spring Security 会主动擦除 credentials 中的密码:
// 在 ProviderManager 中,认证成功后调用
Authentication result = provider.authenticate(authentication);
if (result != null) {
// 擦除凭据(如果配置了)
if (eraseCredentialsAfterAuthentication) {
eraseCredentials(result); // 将 credentials 设为 null
}
return result;
}
正确理解:credentials 为 null 是预期行为,不是 Bug。如果确实需要访问密码(不推荐),需要在 AuthenticationProvider 返回前自行获取,但绝不应将密码留在 Authentication 对象中存入 Session。
易错场景二:自定义 Token 时误用 setAuthenticated(true)
现象:
// ❌ 错误:自定义过滤器中直接 setAuthenticated(true)
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(userDetails, password);
token.setAuthenticated(true); // 抛出异常!
异常信息:
java.lang.IllegalArgumentException: Cannot set this token to trusted -
use constructor which takes a GrantedAuthority list instead
原因:UsernamePasswordAuthenticationToken 的 setAuthenticated() 方法做了安全校验,防止开发者在未经过 AuthenticationProvider 验证的情况下,将 Token 标记为已认证。
正确做法:
// ✅ 正确:使用带 authorities 的构造函数
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
// 此时 isAuthenticated 自动为 true,无需手动设置
面试考点
Q1:Authentication 和 UserDetails 的区别是什么?
| 维度 | Authentication | UserDetails |
|---|---|---|
| 定位 | 认证数据载体(Token) | 用户核心信息封装 |
| 生命周期 | 认证前 → 认证后,贯穿请求 | 认证时从数据源加载,认证后作为 principal |
| 包含密码 | 认证前含,认证后擦除 | 始终包含(用于密码比对) |
| 来源 | 由过滤器构造或 Provider 返回 | 由 UserDetailsService.loadUserByUsername() 加载 |
| 存储位置 | SecurityContextHolder → SecurityContext | 作为 Authentication.principal 的一部分 |
| 关系 | Authentication.getPrincipal() 返回 UserDetails | 被 Authentication 引用 |
Q2:principal 在认证前后分别是什么类型?
- 认证前:通常是
String(用户名),由UsernamePasswordAuthenticationFilter从请求参数提取。 - 认证后:通常是
UserDetails对象(如org.springframework.security.core.userdetails.User),由UserDetailsService加载并经过DaoAuthenticationProvider验证后封装进新的Authentication。
特殊场景:
- JWT 认证:
principal可能是Jwt对象(含 Claims) - OAuth2 登录:
principal可能是OAuth2User对象 - 匿名用户:
principal是字符串"anonymousUser"
Q3:为什么认证后要擦除 credentials?
- 安全存储:
Authentication会被存入SecurityContextHolder,并可能在请求结束后持久化到HttpSession。如果密码留在credentials中,Session 中的密码可能会被日志记录、序列化、调试工具暴露。 - 最小权限原则:认证成功后,系统不再需要原始密码。后续授权决策只依赖
authorities,不需要credentials。 - 审计合规:安全审计要求密码在任何持久化存储中都不以明文形式出现。
Q4:如何安全地手动构造 Authentication?
- 使用带
authorities参数的构造函数(自动标记isAuthenticated = true) credentials设为null(不需要密码时)- 操作完成后必须调用
SecurityContextHolder.clearContext() - 如果场景允许,优先使用
@WithMockUser等测试注解,而非手动构造
// 安全的手动构造模板
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
principal, null, authorities
);
SecurityContextHolder.getContext().setAuthentication(auth);
try {
// 执行受保护操作
} finally {
SecurityContextHolder.clearContext();
}
关键类速查
| 类 | 说明 |
|---|---|
Authentication | 认证接口,核心数据载体 |
UsernamePasswordAuthenticationToken | 用户名密码认证 Token 实现 |
AnonymousAuthenticationToken | 匿名用户认证 Token |
RememberMeAuthenticationToken | 记住我认证 Token |
PreAuthenticatedAuthenticationToken | 前置认证 Token(如 SSO) |
SecurityContextHolder | 持有当前线程的 SecurityContext |
SecurityContext | 封装 Authentication 的上下文 |
DaoAuthenticationProvider | 基于 UserDetailsService 的认证提供者 |
ProviderManager | 遍历 Provider 列表的认证管理器 |
AuthenticationDetailsSource | 生成 details 对象的来源 |
WebAuthenticationDetails | 默认的 details 实现,含 IP 和 SessionId |
版本说明:本文基于 Spring Security 5.x(对应 Spring Boot 2.x)。Spring Security 5.x 中
WebSecurityConfigurerAdapter仍可用,但核心Authentication接口与 6.x 保持一致。不涉及 Spring Core、Spring MVC、Spring Boot 的具体配置细节。
学习建议:理解
Authentication是认证体系的数据核心,与SecurityContextHolder(存储核心)、UserDetails(用户信息核心)构成铁三角。认证前关注principal和credentials的类型,认证后关注authorities和isAuthenticated的变化,是掌握 Spring Security 的第一步。