本文正在参加「金石计划」
日积月累,水滴石穿 😄
前言
对 Spring Security 的文章也写了好几篇了,有基本使用的,有对其进行自定义逻辑的。但那时各位肯定不知道为什么需要这么去定义。所以,本篇呢,来理一理 Spring Security 中默认的表单认证流程源码!
依赖版本
| 名称 | 版本 |
|---|---|
| spring-boot-starter-parent | 2.3.12.RELEASE |
| spring-boot-starter-security | 2.3.12.RELEASE |
| spring-security-web | 5.3.9.RELEASE |
调试开始
就加入 Spring Security 依赖后,启动项目,访问登录页面,在页面输入默认的用户名、密码后,点击提交按钮,会发起一个请求类型为 POST,请求路径为 /login 的接口。
该请求会被 Spring Security 的过滤器链代理拦截。
该请求会来到 UsernamePasswordAuthenticationFilter。
那 UsernamePasswordAuthenticationFilter 这个对象是怎么来的呢? 我们可以看看 WebSecurityConfigurerAdapter 抽象类中的 configure(HttpSecurity http) 方法中提供了一个默认的配置,代码如下:
protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
- authorizeRequests:允许使用 RequestMatcher 实现(即通过URL模式)基于 HttpServletRequest 限制请求访问。
- anyRequest().authenticated():请求都需要被认证
- formLogin:指定支持基于表单的身份验证。如果
FormLoginConfigurer未指定 loginPage,将生成默认登录页。 - httpBasic:可以配置basic登录
进入 formLogin 方法。
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
return getOrApply(new FormLoginConfigurer<>());
}
可以看到这里创建了一个 FormLoginConfigurer对象,FormLoginConfigure是一个用户名密码表单登录的配置类,比如设置登录页面的Url,登录请求的Url 、登录认证成功、认证失败的回调、设置登录请求参数名等等。
public FormLoginConfigurer() {
super(new UsernamePasswordAuthenticationFilter(), null);
usernameParameter("username");
passwordParameter("password");
}
在其无参构造方法中,创建了一个 UsernamePasswordAuthenticationFilter 对象。将其传递给父类,父类会将该过滤器添加到过滤器链中。
AbstractAuthenticationProcessingFilter
上面说到请求会来到 UsernamePasswordAuthenticationFilter,但是它继承了 AbstractAuthenticationProcessingFilter,所以请求会先进入 AbstractAuthenticationProcessingFilter抽象类中,它是一个 Filter,那我们将断点打在 doFilter 方法中。
介绍一下 chain 中的属性,originalChain 表示原生的过滤器链,也就是 Web Filter;additionalFilters 表示 Spring Security 中的过滤器链;firewalledRequest 表示当前请求;size 表示过滤器链中过滤器的个数;currentPosition 则是过滤器链遍历时候的下标。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 进行请求过滤,判断当前调用的请求是否有适配的 xxxAuthenticationFilter 处理
// 因为 AbstractAuthenticationProcessingFilter 是一个过滤器,那就代表所有的接口请求都会进入该类的 doFilter 方法,
// 但又不是所有接口都是需要进行登录认证流程的,所以提前进行请求匹配过滤
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
// 身份验证对象
Authentication authResult;
try {
// 调用子类具体实现的 attemptAuthentication(尝试身份验证) 方法
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// 认证成功时,session 会话需要执行的逻辑
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
// 身份验证失败的默认处理
// 1、清除 SecurityContextHolder
// 2、通知配置的RememberMeServices登录失败
// 3、将其他行为委托给 AuthenticationFailureHandler,调用其登录失败回调方法(实际开发中会重写AuthenticationFailureHandler接口)
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// 认证成功
// 在successfulAuthentication执行前继续执行过滤器(默认为 false)
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 身份验证成功的处理
// 1、在SecurityContextHolder上设置成功的身份验证对象
// 2、通知配置的RememberMeServices成功登录
// 3、通过配置的ApplicationEventPublisher激发InteractiveAuthenticationSuccessEvent,身份验证成功的事件回调
// 4、将其他行为委托给AuthenticationSuccessHandler,调用其登录成功回调方法(实际开发中会重写AuthenticationSuccessHandler接口)
successfulAuthentication(request, response, chain, authResult);
}
上面逻辑很简单,先调用 attemptAuthentication 方法进行身份验证,如果认证失败则对其对应的处理,认证成功也进行对应的处理。那核心的认证逻辑就是在 attemptAuthentication 方法中了,该方法可以由子类实现。
UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// 默认的参数名称,可以通过其set方法进行自定义
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
// 是否仅仅post方式
private boolean postOnly = true;
// 对请求进行过滤,只有接口为 /login,请求方式为 POST,才会进入逻辑
// 在其父类的 requiresAuthentication 方法会进行匹配
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 获得参数值
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 使用请求参数传递的用户名和密码,封装一个未认证 UsernamePasswordAuthenticationToken(用户名密码身份验证令牌) 对象,
// 然后将该对象交给 provider 进行授权
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 设置该次请求信息,比如:调用地址、sessionId。
setDetails(request, authRequest);
// 获得父类的 AuthenticationManager,调用 authenticate 方法进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
封装 UsernamePasswordAuthenticationToken
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 用户名
private final Object principal;
// 密码
private Object credentials;
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
// 设置是否已认证状态为 false
setAuthenticated(false);
}
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
// 调用父类的 setAuthenticated 方法,设置是否已认证状态为 true
super.setAuthenticated(true);
}
// 该方法只能设置是否已认证状态为 false
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
// 密码擦除
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
AuthenticationManager
认证管理器,AuthenticationManager 是认证相关的核心接口,是发起认证的入口,用于处理认证请求,接口只提供了一个认证方法,方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。最常见的实现是ProviderManager。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
ProviderManager
在梳理具体的认证流程前,我们先看一下 Debugger 页面的 this。讲一下里面的两个属性:
-
providers 属性是一个类型为
AuthenticationProvider的列表。该属性可以存放多种认证方式,列表中的每种认证方式都将会被尝试认证。 现在列表中只有一个Provider,类型为AnonymousAuthenticationProvider。 -
parent属性的类型是ProviderManager,该属性就是一个AuthenticationManager,其中 providers 列表有一个类型为DaoAuthenticationProvider的Provider。
那可以知道ProviderManager管理了众多的 AuthenticationProvider实例。在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider,同时,ProviderManager还具有一个可选的 parent 属性,该属性可能为 null,如果当前 ProviderManager 中所有的AuthenticationProvider都认证失败,那么就会调用parent进行认证。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 遍历当前 ProviderManager 对象中的 providers
// 每个 AuthenticationProvider 都将与 toTest 未认证的身份认证对象进行匹配
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 调用对应的 AuthenticationProvider类的 authenticate 方法。
result = provider.authenticate(authentication);
// result不等于 null,说明认证成功,循环结束不再执行后续的 AuthenticationProvider
if (result != null) {
copyDetails(authentication, result);
break;
}
}
// 某个 AuthenticationProvider 认证出现异常,并不会结束,会将该异常暂存,继续执行下一个 AuthenticationProvider 认证流程
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
try {
// 如果当前 AuthenticationProvider 列表中的 Provider 都认证失败,那么使用 parent,也就是父 AuthenticationManager 继续认证
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
// 认证成功
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 身份验证已完成。擦除密码和其他机密数据
((CredentialsContainer) result).eraseCredentials();
}
//尝试认证并认证成功,则将发布 AuthenticationSuccessEvent 事件
//如果父 AuthenticationManager 已发布了 AuthentiationSuccessEvent 事件,则该判断可防止其重复发布事件
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// 父级为空,或引发认证异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
//尝试认证并认证失败,则将发布 AuthenticationEventPublisher 事件
//如果父 AuthenticationManager 已发布了 AuthenticationEventPublisher 事件,则该判断可防止其重复发布事件
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
AuthenticationProvider
AuthenticationProvider(身份验证提供者),可以将多个AuthenticationProvider实例添加到ProviderManager中。其每个AuthenticationProvider可以执行特定的 Authentication (身份验证)类型。例如:DaoAuthenticationProvider支持基于用户名+密码的 UsernamePasswordAuthenticationToken 身份验证。也可以自定义认证方式,比如:EmailVerificationCodeAuthenticationProvider支持邮箱 + 验证码的 EmailVerificationCodeAuthenticationToken 身份验证。
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
-
authenticate() 方法接收一个未通过认证 Authentication 对象,返回一个通过认证的 Authentication 对象。可以实现 authenticate() 方法来自定义身份验证逻辑。
-
supports(Class<?> authentication)方法接收一个 Authentication(身份验证) 对象,如果 AuthenticationProvider 支持指定的身份验证对象,则返回 true。 但返回 true并不保证 AuthenticationProvider 能够对提供的 Authentization 类实例进行正确身份验证;它只是表明它可以对其进行更深入的验证。authenticate方法可以返回 null,尝试其他的 AuthentitationProvider 进行验证。
AbstractUserDetailsAuthenticationProvider
从上面的截图可以知道,用户名密码流程的认证会交给 DaoAuthenticationProvider 进行处理,但是再看DaoAuthenticationProvider 的源码前,我们需要先看 AbstractUserDetailsAuthenticationProvider 类的源码,因为它是 DaoAuthenticationProvider 的父类。而且 authenticate、supports 方法在其父类中。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
// 交给子类进行实现,可以进行其他身份验证检查
protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
// 认证方法
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 根据用户名从缓存中获得UserDetails对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 如果缓存中没有信息,通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法,返回一个 UserDetails 对象
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 检查该用户对象的各种状态,比如:账户是否未锁定、账户是否启用、账户是否未过期
preAuthenticationChecks.check(user);
// 使用子类 DaoAuthenticationProvider 实现的 additionalAuthenticationChecks 方法,检查密码是否输入正确
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// 该用户信息可能来源于缓存,缓存中的信息可能是错误的、老旧的,重新通过子类 DaoAuthenticationProvider 实现的 retrieveUser 方法加载用户信息
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 检查该用户对象的各种状态,比如:凭证(密码)是否未过期
postAuthenticationChecks.check(user);
// 存入缓存
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 会调用子类方法,设置是否已认证为true,设置权限信息,到此,认证流程就完成了。
return createSuccessAuthentication(principalToReturn, authentication, user);
}
// 创建认证成功的 Authentication 对象,子类可以进行重写
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
//允许子类从特定的方式加载 UserDetails
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
// 验证传入的身份验证对象是否是 UsernamePasswordAuthenticationToken。如果是则返回 true。
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
DaoAuthenticationProvider
DaoAuthenticationProvider 重写了父类的additionalAuthenticationChecks、 retrieveUser、createSuccessAuthentication方法。
// 检查密码是否输入正确
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
// 检索用户
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 通过调用 UserDetailsService 的 loadUserByUsername 方法加载用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
// 创建认证成功的 Authentication 对象
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 是否需要再次进行密码加密,默认实现为 false,具体看提供的实现类
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
总结
- 输入用户名密码,发起请求会来到
AbstractAuthenticationProcessingFilter的doFilter方法 - 在方法内部进行请求 url 匹配,如果是认证请求则执行认证相关逻辑。
- 调用
UsernamePasswordAuthenticationFilter的attemptAuthentication方法,在attemptAuthentication方法内部,根据传入的用户名密码参数构建一个未通过认证的UsernamePasswordAuthenticationToken认证对象。 - 将未通过认证的
UsernamePasswordAuthenticationToken认证对象传入到AuthenticationManager的authenticate方法中,AuthenticationManager的具体实现为ProviderManager。在其方法内部进行认证方式匹配provider.supports(toTest),匹配成功后调用具体身份验证实现,用户名密码认证的实现为DaoAuthenticationProvider。 - 在
DaoAuthenticationProvider类中,根据用户名加载用户、检查用户状态、构造通过认证的身份验证对象。
扩展点
通过对认证流程的梳理,可以发现几个可扩展点。
- 可以 extends AbstractAuthenticationProcessingFilter,自定义其他认证方式,比如手机号、邮箱验证码登录。
- 可以 extends AbstractAuthenticationToken,自定义身份验证对象。
- 可以 implements AuthenticationProvider,自定义身份验证实现,通过手机号、邮箱查询用户信息。
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。