UsernamePasswordAuthenticationToken
定义与作用
UsernamePasswordAuthenticationToken 是 Spring Security 中最常用的 Authentication 实现,封装基于用户名和密码的认证凭据。它在认证流程中扮演双重角色:
- 未认证状态:由过滤器构造,携带用户提交的原始凭据,作为认证请求的"入场券"
- 已认证状态:由
AuthenticationProvider返回,携带用户主体、权限列表和已认证标记,作为认证成功的"身份证明"
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal; // 未认证时=用户名,已认证时=UserDetails
private Object credentials; // 未认证时=密码,已认证时=null(已擦除)
// 构造未认证 Token(用于提交认证请求)
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false); // 显式标记未认证
}
// 构造已认证 Token(用于返回认证结果)
public UsernamePasswordAuthenticationToken(
Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // 标记已认证
}
}
手写过滤器的痛点
手写登录时,通常自己定义登录请求对象,但各层之间传递的信息不统一:
// 痛点:登录请求对象、Session 中的用户对象、权限校验对象各写各的
public class LoginRequest {
private String username;
private String password;
} // 仅用于接收请求
public class SessionUser {
private String userId;
private String nickname;
private List<String> permissions;
} // 用于存入 Session
// 两者没有统一接口,过滤器、Controller、Service 层各自转换
// 无法接入 Spring Security 的授权体系(如 @PreAuthorize)
UsernamePasswordAuthenticationToken 作为 Authentication 的标准实现,天然被 Spring Security 的所有组件识别,从过滤器到 ProviderManager 到 SecurityContextHolder 全程通用。
核心原理
两种状态转换
状态字段语义变化
| 字段 | 未认证时 | 已认证时 | 说明 |
|---|---|---|---|
principal | String(用户名) | UserDetails(用户主体) | 认证成功后升级为完整用户对象 |
credentials | String(原始密码) | null | 认证成功后主动擦除,防止密码泄露 |
authorities | null | Collection<GrantedAuthority> | 从 UserDetails.getAuthorities() 复制 |
authenticated | false | true | 通过 super.setAuthenticated(true) 设置,且会校验不可从外部设为 false 后 true |
安全设计:防止外部伪造已认证 Token
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor instead");
}
super.setAuthenticated(false);
}
开发者不能通过 setAuthenticated(true) 将未认证 Token 改为已认证,必须通过带 authorities 参数的构造器创建。这防止了恶意代码伪造认证状态。
示例一:自定义过滤器构造未认证 Token
场景说明
实现一个自定义的短信验证码登录过滤器,需要构造 UsernamePasswordAuthenticationToken 并提交给 AuthenticationManager。
操作前配置
没有自定义过滤器,只有标准表单登录:
http.formLogin(Customizer.withDefaults()); // 只支持用户名密码登录
操作后配置
自定义短信登录过滤器,手动构造并提交 Token:
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/login/sms", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String mobile = request.getParameter("mobile");
String code = request.getParameter("code");
if (mobile == null) mobile = "";
if (code == null) code = "";
mobile = mobile.trim();
// 构造未认证的 UsernamePasswordAuthenticationToken
// 这里用 mobile 作为 principal,code 作为 credentials
UsernamePasswordAuthenticationToken authRequest =
new UsernamePasswordAuthenticationToken(mobile, code);
// 设置请求详情(IP、SessionId),用于后续安全分析
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request)
);
// 提交给 AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
}
}
配置自定义 Provider 处理此 Token:
@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Autowired
private SmsCodeService smsCodeService;
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String mobile = authentication.getPrincipal().toString();
String code = authentication.getCredentials().toString();
if (!smsCodeService.verify(mobile, code)) {
throw new BadCredentialsException("短信验证码错误");
}
UserDetails user = userDetailsService.loadUserByUsername(mobile);
// 构造已认证的 Token:principal=UserDetails, credentials=null, authorities=权限列表
return new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities()
);
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
注册过滤器:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsAuthenticationProvider smsAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(smsAuthenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.antMatchers("/login/sms").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new SmsAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.formLogin(Customizer.withDefaults());
}
}
结果分析
- 自定义过滤器通过
new UsernamePasswordAuthenticationToken(mobile, code)构造未认证 Token SmsAuthenticationProvider验证通过后,通过new UsernamePasswordAuthenticationToken(user, null, authorities)构造已认证 Token- 两个构造器的区别仅在于第三个参数
authorities的有无,这是框架区分认证状态的关键
示例二:已认证 Token 在 SecurityContext 中的流转
场景说明
观察认证成功后,UsernamePasswordAuthenticationToken 如何从 ProviderManager 流入 SecurityContextHolder,再到 Controller 层被使用。
操作前配置
认证成功后直接写 Session,没有使用 SecurityContext:
// 自定义过滤器内部
if (passwordCorrect) {
request.getSession().setAttribute("user", user); // 自己写 Session
// Spring Security 的 @PreAuthorize 等方法级安全注解无法识别
}
操作后配置
使用标准 SecurityContextHolder 机制:
public class StandardLoginFilter extends UsernamePasswordAuthenticationFilter {
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) {
// authResult 是 ProviderManager 返回的已认证 Token
// principal=UserDetails, credentials=null, authorities=[ROLE_USER]
// 1. 存入 SecurityContext
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
// 2. 触发 SecurityContextPersistenceFilter 将其存入 Session
// 无需手动操作,框架自动完成
// 3. 调用成功处理器
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
}
Controller 中获取已认证 Token 的信息:
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@GetMapping("/info")
public ResponseEntity<?> getInfo(@AuthenticationPrincipal UserDetails userDetails) {
// @AuthenticationPrincipal 自动从 SecurityContext 取出 principal
String username = userDetails.getUsername();
return ResponseEntity.ok(Map.of("username", username));
}
@GetMapping("/authorities")
public ResponseEntity<?> getAuthorities() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// auth 就是 UsernamePasswordAuthenticationToken(已认证状态)
List<String> authorities = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
boolean isAuthenticated = auth.isAuthenticated(); // true
Object principal = auth.getPrincipal(); // UserDetails 对象
Object credentials = auth.getCredentials(); // null(已擦除)
return ResponseEntity.ok(Map.of(
"authenticated", isAuthenticated,
"authorities", authorities,
"principalType", principal.getClass().getSimpleName(),
"credentials", credentials
));
}
}
结果分析
UsernamePasswordAuthenticationToken作为标准载体,贯穿"过滤器 → 认证管理器 → 安全上下文 → 业务层"全流程- 已认证状态下
credentials为null,防止密码在业务层泄露 principal从String升级为UserDetails,让业务层能获取完整用户信息
易错场景:错误地构造已认证 Token 导致安全漏洞
问题描述
开发者试图在业务代码中手动标记用户为已登录,直接使用 setAuthenticated(true) 或调用带 authorities 的构造器但传入伪造的权限。
错误代码
@Service
public class BadLoginService {
public void quickLogin(String username) {
// 极度危险!绕过所有认证逻辑,直接伪造已认证 Token
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(username, null);
auth.setAuthenticated(true); // 抛出异常!框架已阻止
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
// 另一种错误:构造已认证 Token 但传入任意权限
List<GrantedAuthority> fakeAuthorities = Arrays.asList(
new SimpleGrantedAuthority("ROLE_ADMIN")
);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username, null, fakeAuthorities // 任何人都能伪造 ADMIN 权限!
);
SecurityContextHolder.getContext().setAuthentication(auth);
原因分析
UsernamePasswordAuthenticationToken 的 setAuthenticated(true) 被显式禁止,就是为了防止此类攻击。但如果开发者使用带 authorities 的构造器(这是合法途径),却绕过了真正的认证流程(如 AuthenticationManager),就能伪造任意权限。
正确做法
永远不要让未经验证的代码路径构造已认证的 UsernamePasswordAuthenticationToken。所有认证必须经过 AuthenticationManager:
// 正确:委托给 AuthenticationManager
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
Authentication result = authenticationManager.authenticate(token);
// result 已经过 Provider 验证,权限来自 UserDetailsService,不可伪造
SecurityContextHolder.getContext().setAuthentication(result);
对于测试或内部系统调用,使用 TestingAuthenticationToken 或 RunAsManager,而不是绕过 AuthenticationManager。
面试考点:
UsernamePasswordAuthenticationToken的两个构造器有什么区别?- 为什么
setAuthenticated(true)会抛出异常?框架的设计意图是什么?- 认证成功后,
principal和credentials分别变成什么?- 手写过滤器时,如何正确构造和提交
UsernamePasswordAuthenticationToken?