写在前面

在前一篇文章中,我们介绍了如何配置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);
});
}

这两个获取方式的逻辑都比较简单,相信大家能看的明白。

稍微总结一下:

  1. UsernamePasswordAuthenticationFilter拦截到用户填写的表单信息后,先进行校参处理(判断请求是否为POST请求,将null值转为空字符串),然后将参数封装成UsernamePasswordAuthenticationToken(这是一个Authentication实现类AbstractAuthenticationToken的子类)对象,再然后调用AuthenticationManager对象的实现类ProviderManager的authenticate方法进行认证操作;

  2. ProviderManager在接收到token后,先根据token的className比对spring security内置的认证方式,找到后调用AuthenticationProvider的实现类AbstractUserDetailsAuthenticationProvider的authenticate方法进行认证操作

  3. AbstractUserDetailsAuthenticationProvider对象在收到Authentication对象后,先确定用户名,再根据用户名从缓存里查找用户信息,找不到则调用retrieveUser方法在持久层查找数据(持久层数据可以是文本、数据库里的数据)。在spring security中,只有DaoAuthenticationProvider实现了这个方法(目前为止)。这时DaoAuthenticationProvider便调用UserDetailsService的loadUserByUsername方法找到userDetails。在通过了一系列的判断验证后,调用createSuccessAuthentication方法给授权,并将其(UsernamePasswordAuthenticationToken)返回给了AuthenticationManager的实现类ProviderManager。

  4. ProviderManager在收到UsernamePasswordAuthenticationToken对象后,先进行参数校验(判空,判null),之后调用事件发布者eventPublisher的publishAuthenticationSuccess方法将验证结果发布出去。最后将结果返回给UsernamePasswordAuthenticationFilter。至此验证流程大体上就结束了.

也就述说,UsernamePasswordAuthenticationFilter负责拦截,AuthenticationManager负责组织流程,真正执行操作的是认证AuthenticationProvider的子类AbstractUserDetailsAuthenticationProvider对象。

End

给大家画了一张简化版的认证时序图

Spring Security认证流程分析--练气后期的更多相关文章

  1. Spring Security拦截器加载流程分析--练气中期

    写在前面 上回我们讲了spring security整合spring springmvc的流程,并且知道了spring security是通过过滤器链来进行认证授权操作的.今天我们来分析一下sprin ...

  2. 03 spring security执行流程分析

    spring security主要是依赖一系列的Filter来实现权限验证的,责任链设计模式是跑不了的.下面简单记录一下spring操作这些Filter的过程. 1. WebSecurityConfi ...

  3. Spring Security 认证流程

    请求之间共享SecurityContext原因:

  4. Spring Security使用数据库数据完成认证--练气后期2

    写在前面 没错,这篇文章还是练气后期!但作者我相信筑基指日可待! 在前一篇文章当中,我们简单地分析了一下Spring Security的认证流程,知道了如果想要实现对自己用户数据(账户.角色.权限)的 ...

  5. shiro认证流程源码分析--练气初期

    写在前面 在上一篇文章当中,我们通过一个简单的例子,简单地认识了一下shiro.在这篇文章当中,我们将通过阅读源码的方式了解shiro的认证流程. 建议大家边读文章边动手调试代码,这样效果会更好. 认 ...

  6. Spring Security 源码分析(四):Spring Social实现微信社交登录

    社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ.人人网.开心网.新浪微博.搜狐微博.腾讯微博.淘宝.豆瓣.MSN.Google等社会化媒体账号登录该网站. 前言 ...

  7. Spring Security认证配置(三)

    学习本章之前,可以先了解下上篇Spring Security认证配置(二) 本篇想要达到这样几个目的: 1.登录成功处理 2.登录失败处理 3.调用方自定义登录后处理类型 具体配置代码如下: spri ...

  8. Spring Security 源码分析 --- WebSecurity

    概述 spring security 源码分析系列文章. 源码分析 我们想一下,我们使用 ss 框架的步骤是怎么样的. @Configuration @EnableWebSecurity @Enabl ...

  9. 阶段5 3.微服务项目【学成在线】_day17 用户认证 Zuul_01-用户认证-用户认证流程分析

    1 用户认证 1.1 用户认证流程分析 用户认证流程如下: 访问下面的资源需要携带身份令牌和jwt令牌,客户端可以通过身份认证的令牌从服务端拿到长令牌, 一会要实现认证服务请求用户中心从数据库内来查询 ...

随机推荐

  1. django表单使用

    一.表单常用字段类型及参数 表单可以自动生成html代码,每一个字段默认有一个html显示样式,大多数默认为输入框. 字段相当于正则表达式的集合,能够对表单传入的数据进行校验,并且某一部分校验失败时会 ...

  2. JS控制语句及小练习

    一.控制语句 判断: ①if() {} ; if(){}else{} ; if(){}else if{};…… ② switch(){ case "": break; } 循环: ...

  3. PHP filetype() 函数

    定义和用法 filetype() 函数返回指定文件或目录的类型. 如果成功,该函数返回 7 种可能的值之一.如果失败,则返回 FALSE. 可能的返回值: fifo char dir block li ...

  4. setOff与scrollTop区别

    1.offsetTop     : 当前对象到其上级层顶部的距离. 不能对其进行赋值.设置对象到页面顶部的距离请用style.top属性. 2.offsetLeft    : 当前对象到其上级层左边的 ...

  5. vmware15虚拟机安装教程

    自己碰到的问题:本人win7 64位旗舰版系统.之前用VMware12pro版本的软件,在安装Ubuntu18.04之后,有时候开启Ubuntu虚拟机时有点问题,重启就可以了.但是不稳定,所以改用VM ...

  6. e分钟带你利用Python制作词云图

    随着大数据时代的来临,数据分析与可视化,显得越来越重要,今天给小伙伴们带来一种最常见的数据可视化图形-词云图的制作方法. 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法 ...

  7. 常哥带你认识NoSQL和Redis的强大

    各位朋友,这篇文章是针对Redis快速了解的内容,为了学好Redis在这里首先跟大家聊聊NoSQL相关内容,有了概念和方向后,我们再学习Redis大家会感觉得心应手. [公众号dotNet工控上位机: ...

  8. [深度学习] Pytorch(三)—— 多/单GPU、CPU,训练保存、加载模型参数问题

    [深度学习] Pytorch(三)-- 多/单GPU.CPU,训练保存.加载预测模型问题 上一篇实践学习中,遇到了在多/单个GPU.GPU与CPU的不同环境下训练保存.加载使用使用模型的问题,如果保存 ...

  9. java SFTP工具类

    需要导入jsch-0.1.52.jar import java.io.File; import java.io.FileInputStream; import java.io.FileOutputSt ...

  10. C#LeetCode刷题之#559-N叉树的最大深度​​​​​​​(Maximum Depth of N-ary Tree)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4088 访问. 给定一个 N 叉树,找到其最大深度. 最大深度是指 ...