Spring Security认证流程分析--练气后期
写在前面
在前一篇文章中,我们介绍了如何配置spring security的自定义认证页面,以及前后端分离场景下如何获取spring security的CSRF Token。在这一篇文章中我们将来分析一下spring security的认证流程。
提示:我使用的spring security的版本是5.3.4.RELEASE。如果读者使用的不是和我同一个版本,源码细微之处有些不同,但是大体流程都是一样的。
认证流程分析
通过查阅spring security的官方文档我们知道,spring security的认证过滤操作由UsernamePasswordAuthenticationFilter 完成。那么,我们这次的流程分析就从这个过滤器开始。
UsernamePasswordAuthenticationFilter
先上部分源码
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
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;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 1. 必须为POST请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
//2.取出用户填写的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
//3.防止出现空指针
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//4.去掉用户名的空格
username = username.trim();
//5.在层层校验后,开始对username和password进行封装
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 6.认证逻辑
return this.getAuthenticationManager()
.authenticate(authRequest);
}
}
从上面的分析我们知道了,当表单信息进入到这个过滤器之后,经过层层校验,将其封装成UsernamePasswordAuthenticationToken对象。接下来我们进入到这个对象里面看看。
一下是部分源码
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
//用户名
private final Object principal;
//密码
private Object credentials;
//5.1还未认证,走这个构造方法
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
}
AuthenticationManager
在上方第6步,进入了认证逻辑,(真正认证操作在AuthenticationManager里面 )我们接下来进入到AuthenticationManager对象的authenticate()方法里看看。

发现这是一个接口。从图中可以知道除了ProviderManager这个类之外,其他的都是内部类,所有我们就直接进入到ProviderManager对象的authenticate方法里看看
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();
//7.找到与之对应的认证方式(本系统账户登录。。微信登录等)
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
//8。 调用认证服务提供者的方法进行认证
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
// spring security将其所有认证方式都封装成一个AuthenticationProvider集合,第一步便是找出对应的认证方式
public List<AuthenticationProvider> getProviders() {
return providers;
}
}
AuthenticationProvider
在步骤8中,调用了认证提供者的认证方法,接下来我们进去看看。发现AuthenticationProvider是一个接口

我们从实现类的名称当中猜一个进去看看,就看AbstractUserDetailsAuthenticationProvider这个类。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//8.1尝试从缓存中获取用户
boolean cacheWasUsed = true;
//UserDetails就是spring Security内定义的用户对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
//8.2如果缓存中不存在用户,则开始检索
try {
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);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
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();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
在步骤8.2中,调用了retrieveUser方法查找用户,接下来我们进去看看
protected abstract UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException;
发现它是一个抽象的方法,接下来点进去,看看它已经提供好的实现方法。这个方法在DaoAuthenticationProvider对象中
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//8.2.1通过用户名加载用户
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);
}
}
通过阅读代码发现,它又调用了UserDetailsService对象的loadUserByUsername(方法去做加载操作,我们点进去看看
UserDetailsService
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
发现这是一个接口,并且到了这一步就得到了我们的用户对象UserDetails。如果说大家要自定义认证信息检索,查找自己定义的User对象话就实现这个接口,并且让自己的用户对象实现UserDetails接口。并且实现相关查询方法和注册。
接下来我们看spring security已经提供好的实现类它的实现类

我们重点关注的有两个,一个是JdbcDaoImpl,一个是CachingUserDetailsService。前者从数据库中查询用户,后者从缓存中查询用户信息
我们先看CachingUserDetailsService的源码
public class CachingUserDetailsService implements UserDetailsService {
private UserCache userCache = new NullUserCache();
private final UserDetailsService delegate;
public CachingUserDetailsService(UserDetailsService delegate) {
this.delegate = delegate;
}
public UserCache getUserCache() {
return userCache;
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public UserDetails loadUserByUsername(String username) {
UserDetails user = userCache.getUserFromCache(username);
if (user == null) {
user = delegate.loadUserByUsername(username);
}
Assert.notNull(user, () -> "UserDetailsService " + delegate
+ " returned null for username " + username + ". "
+ "This is an interface contract violation");
userCache.putUserInCache(user);
return user;
}
}
再看JdbcDaoImpl(部分)
public class JdbcDaoImpl extends JdbcDaoSupport
implements UserDetailsService, MessageSourceAware {
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
List<UserDetails> users = loadUsersByUsername(username);
if (users.size() == 0) {
this.logger.debug("Query returned no results for user '" + username + "'");
throw new UsernameNotFoundException(
this.messages.getMessage("JdbcDaoImpl.notFound",
new Object[] { username }, "Username {0} not found"));
}
UserDetails user = users.get(0); // contains no GrantedAuthority[]
Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
if (this.enableAuthorities) {
dbAuthsSet.addAll(loadUserAuthorities(user.getUsername()));
}
if (this.enableGroups) {
dbAuthsSet.addAll(loadGroupAuthorities(user.getUsername()));
}
List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
addCustomAuthorities(user.getUsername(), dbAuths);
if (dbAuths.size() == 0) {
this.logger.debug("User '" + username
+ "' has no authorities and will be treated as 'not found'");
throw new UsernameNotFoundException(this.messages.getMessage(
"JdbcDaoImpl.noAuthority", new Object[] { username },
"User {0} has no GrantedAuthority"));
}
return createUserDetails(username, user, dbAuths);
}
protected List<UserDetails> loadUsersByUsername(String username) {
return getJdbcTemplate().query(this.usersByUsernameQuery,
new String[] { username }, (rs, rowNum) -> {
String username1 = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username1, password, enabled, true, true, true,
AuthorityUtils.NO_AUTHORITIES);
});
}
这两个获取方式的逻辑都比较简单,相信大家能看的明白。
稍微总结一下:
UsernamePasswordAuthenticationFilter拦截到用户填写的表单信息后,先进行校参处理(判断请求是否为POST请求,将null值转为空字符串),然后将参数封装成UsernamePasswordAuthenticationToken(这是一个Authentication实现类AbstractAuthenticationToken的子类)对象,再然后调用AuthenticationManager对象的实现类ProviderManager的authenticate方法进行认证操作;
ProviderManager在接收到token后,先根据token的className比对spring security内置的认证方式,找到后调用AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider的authenticate方法进行认证操作
AbstractUserDetailsAuthenticationProvider对象在收到Authentication对象后,先确定用户名,再根据用户名从缓存里查找用户信息,找不到则调用retrieveUser方法在持久层查找数据(持久层数据可以是文本、数据库里的数据)。在spring security中,只有DaoAuthenticationProvider实现了这个方法(目前为止)。这时DaoAuthenticationProvider便调用UserDetailsService的loadUserByUsername方法找到userDetails。在通过了一系列的判断验证后,调用createSuccessAuthentication方法给授权,并将其(UsernamePasswordAuthenticationToken)返回给了AuthenticationManager的实现类ProviderManager。
ProviderManager在收到UsernamePasswordAuthenticationToken对象后,先进行参数校验(判空,判null),之后调用事件发布者eventPublisher的publishAuthenticationSuccess方法将验证结果发布出去。最后将结果返回给UsernamePasswordAuthenticationFilter。至此验证流程大体上就结束了.
也就述说,UsernamePasswordAuthenticationFilter负责拦截,AuthenticationManager负责组织流程,真正执行操作的是认证AuthenticationProvider的子类AbstractUserDetailsAuthenticationProvider对象。
End
给大家画了一张简化版的认证时序图

Spring Security认证流程分析--练气后期的更多相关文章
- Spring Security拦截器加载流程分析--练气中期
写在前面 上回我们讲了spring security整合spring springmvc的流程,并且知道了spring security是通过过滤器链来进行认证授权操作的.今天我们来分析一下sprin ...
- 03 spring security执行流程分析
spring security主要是依赖一系列的Filter来实现权限验证的,责任链设计模式是跑不了的.下面简单记录一下spring操作这些Filter的过程. 1. WebSecurityConfi ...
- Spring Security 认证流程
请求之间共享SecurityContext原因:
- Spring Security使用数据库数据完成认证--练气后期2
写在前面 没错,这篇文章还是练气后期!但作者我相信筑基指日可待! 在前一篇文章当中,我们简单地分析了一下Spring Security的认证流程,知道了如果想要实现对自己用户数据(账户.角色.权限)的 ...
- shiro认证流程源码分析--练气初期
写在前面 在上一篇文章当中,我们通过一个简单的例子,简单地认识了一下shiro.在这篇文章当中,我们将通过阅读源码的方式了解shiro的认证流程. 建议大家边读文章边动手调试代码,这样效果会更好. 认 ...
- Spring Security 源码分析(四):Spring Social实现微信社交登录
社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...
- Spring Security认证配置(三)
学习本章之前,可以先了解下上篇Spring Security认证配置(二) 本篇想要达到这样几个目的: 1.登录成功处理 2.登录失败处理 3.调用方自定义登录后处理类型 具体配置代码如下: spri ...
- Spring Security 源码分析 --- WebSecurity
概述 spring security 源码分析系列文章. 源码分析 我们想一下,我们使用 ss 框架的步骤是怎么样的. @Configuration @EnableWebSecurity @Enabl ...
- 阶段5 3.微服务项目【学成在线】_day17 用户认证 Zuul_01-用户认证-用户认证流程分析
1 用户认证 1.1 用户认证流程分析 用户认证流程如下: 访问下面的资源需要携带身份令牌和jwt令牌,客户端可以通过身份认证的令牌从服务端拿到长令牌, 一会要实现认证服务请求用户中心从数据库内来查询 ...
随机推荐
- 渲染导航菜单的同时给每个菜单绑定不同的router跳转
这个问题一开始的时候,我总想着router跳转只有两种方式 一种@click,一种router-link 然后我想着@click,绑定一个事件,事件下面无法确定我当前是哪个菜单,解决不了. 然后< ...
- 读/写docx文件
安装 pip install python-docx 1.建立新Word文档 建立新文档需要调用Document对象的save方法,一个Document对象代表一个Word文档,该方法的参数是保存的文 ...
- Python os.tmpfile() 方法
概述 os.tmpfile() 方法用于返回一个打开的模式为(w+b)的临时文件对象,这文件对象没有文件夹入口,没有文件描述符,将会自动删除.高佣联盟 www.cgewang.com 语法 tmpfi ...
- Nginx使用中遇到的问题记录
问题一.关于空格 nginx配置对空格十分敏感,在关键字和符号的前后,一定记得有空格(或换行).一个典型的场景是 if { } 语句,大括号前后要有空格,否则可能出现非预期行为. 问题二.关于serv ...
- 【01python基础】
1.python的环境搭建(Python3 下载) Python 官网:https://www.python.org/ Python文档下载地址:https://www.python.org/doc/ ...
- 【洛谷P3802】小魔女帕琪 题解(概率期望)
前言:蒟蒻太弱了,不会推式子QAQ -------------------- 题目链接 题目大意:给定$7$种能量晶体各$a_i$个,每次随机摸到一个晶体,如果连续摸到$7$个不同的晶体就会触发一次伤 ...
- .NETCore微服务探寻(三) - 远程过程调用(RPC)
前言 一直以来对于.NETCore微服务相关的技术栈都处于一个浅尝辄止的了解阶段,在现实工作中也对于微服务也一直没有使用的业务环境,所以一直也没有整合过一个完整的基于.NETCore技术栈的微服务项目 ...
- Docker 阿里镜像
Docker 配置阿里镜像 Dokcer 拉取镜像非常慢,配置阿里镜像加速. 步骤 首先注册阿里云,找到 "容器镜像服务" --> "镜像加速器" ,复制 ...
- 使用Luhn算法实现信用卡号验证
问题描述: 2:信用卡号的验证 [信用卡号的验证] 当你输入信用卡号码的时候,有没有担心输错了而造成损失呢?其实可以不必这么 担心,因为并不是一个随便的信用卡号码都是合法的,它必须通过 Luhn 算法 ...
- 基于Python的AT命令测试脚本
对于各种有线&无线调制解调器(modem)产品来说,AT命令是事实上的标准接口之一,在工业界被广泛使用. 我开发了一套基于Python的AT命令测试脚本,源代码可在github上获取: htt ...