写在最前,本人也只是个大三的学生,如果你发现任何我写的不对的,请在评论中指出。
之前写过一份shiro认证、授权的源码解析,但是是建立在以我自己编写的一些过滤器、匹配器上,不是很友好,这次对springsecurity的认证与授权源码分析就靠springsecurity自家提供的UsernamePasswordAuthenticationFilter、UsernamePasswordAuthenticationToken、ProviderManager、DaoAuthenticationProvider
这几个类来研究。
- 前景提要:请把springsecurity配置成最简单的表单校验,稍微记一下下图的执行流程
那么正式开始,启动demo的debug,在UsernamePasswordAuthenticationFilter
的attemptAuthentication()
方法处打上断点如下:
对于该方法我拆分成四个部分来解析:
if判断处:
# 这一部分很好理解, 主要是为了校验当前请求的方式是不是POST,若不是就抛出异常(因为是表单登录默认为POST)
# postOnly默认是true
获取username和password
# 这里获取username和password都是通过一下方式获取得到
# request.getParameter(this.usernameParameter);
# usernameParameter=SPRING_SECURITY_FORM_USERNAME_KEY="username";
# 需要关注的可能是UsernamePasswordAuthenticationToken类
- UsernamePasswordAuthenticationToken
我们直接进入该类的源码,首先先看它继承了什么类:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
---
public abstract class AbstractAuthenticationToken implements Authentication
可以看到它继承了AbstractAuthenticationToken,而AbstractAuthenticationToken又继承了Authentication,在我上面的执行流程图中,最后一步都会指向一个被包含着的Authentication这个对象,这里依次解释流程图中的三个对象在springsecurity的意义:
- SecurityContext:
是用来存储当前认证的用户的详细信息, 直译就是安全上下文
- SecurityContextHolder:
是一个工具类,它提供了对安全上下文的访问。 默认情况下使用一个ThreadLocal对象存储上下文对象,意味着它线程安全。
- Authentication: 存储了当前用户(交互的主体,也就是可能不是用户)的详细信息,里面包括
Principl:可以理解为用户的信息; Credentials: 可以理解为密码; Authorities可以理解为权限
,Authentication是springsecurity运作的核心单位。
回到UsernamePasswordAuthenticationToken,可以看到它有两个属性principal和credentials
是与Authentication对应,实际上它就是被设计成简单的包裹着用户名和密码在这一条链路中进行验证的凭依。
允许设置"details" 属性以及交付到AuthenticationManager挑选provider进行验证
实际上我对setDetails(request, authRequest);
也不太了解,根据DEBUG它会走到WebAuthenticationDetailsSource new 一个WebAuthenticationDetails对象
返回并set到UsernamePasswordAuthenticationToken中,官方给出解释是当类希望创建新的身份验证详细信息实例时由类调用。 我的理解是因为springsecurity提供了userdetail(可以被继承并新增一些IP字段等)类可以被userDetailService的loadUserByUsername方法做自定义验证,如果不设置则没办法调用这个过程。
继续向下DEBUG,我们会来到ProviderManager
,这里会挑选出一个对应着token的Provider进行校验。
toTest与provider.supports(toTest)方法
实际上这里的实现逻辑是这样的,首先通过getClass()
获取当前保存着用户名和密码token的Class信息, 然后循环获取当前this中的AuthenticationProvider
(第一次遍历的时候只有匿名和记住我两个AuthenticationProvider,配置不同情况不同),若变量result=null和当前parent不为空,则直接调用parent.authention()的方法,也就是调用了DaoAuthenticationProvider.authenticate(),DaoAuthenticationProvider是默认设置的AuthenticationProvider
而这个provider.supports(toTest)
方法的实现结果基本都是:
return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
到了这儿, 就是去调用AbstractUserDetailsAuthenticationProvider.authenticate()
真正实现认证的逻辑
那么挑重点说:
UserDetails user = this.userCache.getUserFromCache(username);
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
首先springsecurity首先会从缓冲中获取对象,如果缓冲中不存在对象,则会调用retrieveUser()方法
来获取值。这个方法也好理解,点进去我们会看到一行代码
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
# 是不是豁然开朗
# 如果你实现过UserDeatilService就一定知道这个方法
# 我们可以在这个方法内自定义去读取在数据库或者其他地方保存着的user信息
# 进行自定义验证并且返回
this.preAuthenticationChecks.check(user);
那么得到user之后,会对这个userdetail进行前置检测,这个前置检测你继承过userDetail也会很熟悉
# userDetail中会存在一些:
user.isAccountNonLocked()
user.isEnabled()
用于判断当前userdetail是否在状态上有误
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
这个方法,你点进去也一目了然,它会回到DaoAuthenticationProvider
并调用additionalAuthenticationChecks
进行密码的校验,若密码为空或者密码错误,则会抛出BadCredentialsException
异常。
当这些都执行完了,会执行后置校验this.postAuthenticationChecks.check(user);
,然后若开启了缓存,会放置一份数据到缓存,最后创建一份成功的Authentication对象交给securitycontext保存管理。
像是短信验证登录/注册之类的,我都是参照着usernamepasswordXXX等类的实现模仿编写的。