SpringSecurity从关门放狗到关门打狗(三)
———————SpringSecurity登录认证授权原理
上一期我们写了个基本的授权认证功能,结尾的时候说这期来点骚的,但是这一期恐怕让各位失望了,这期咱们来搞点基础的东西–SpringSecurity登录认证授权原理。虽然这个其他人很多博客里也都写过,但是我还是要再说一下,因为后面我们可能会用到,而且也可以让我们对这个安全框架有更加深层次(♂)的了解,本篇内容参考(cv)了博客SpringSecurity认证流程源码详解和Spring-Security登录认证授权原理,人家已经讲得很详细了,我就是整理加丰富一下,各位不要骂我哈。
开整!
一、认证处理流程说明
首先,只要是一提springsecurity认证流程,这张《认证处理流程说明原理图》是跳不过去的,下面咱们就按照这张图的流程结合源码走一遍。
先贴一下源码的仓库大家可以拉取一下对照着看,或者也可以从IDEA中点击 DownLoad Sources下载源码进行查看
https://github.com/spring-projects/spring-security
UsernamePasswordAuthenticationFilter
我们先来查找一下这个了UsernamePasswordAuthenticationFilter.java
类,看名字我们就知道这是个过滤器(实际上Spring Security就是通过过滤器链实现的),该过滤器是用来处理用户认证逻辑的。在前台输入完用户名密码之后就会经过一些列的过滤器,其中就有要UsernamePasswordAuthenticationFilter
类中去获取请求中的用户名和密码,去构建一个UsernamePasswordAuthenticationToken
对象。
在UsernamePasswordAuthenticationFilter.java
类中的主要的方法attemptAuthentication
(实际上这个方法是在本过滤器的抽象类AbstractAuthenticationProcessingFilter.java
的doFilter
方法中调用的)中我们可以看到:
(1)可以看到它默认的登录请求url是"/login",并且只允许POST方式的请求
(2)他是通过obtainUsername()方法默认根据参数名为"username"和"password"来获取用户名和密码的
(3)通过构造方法实例化一个UsernamePasswordAuthenticationToken
对象,此时调用的是UsernamePasswordAuthenticationToken
的两个参数的构造函数,如图构造方法一:刚才(3)步骤中使用本类的有参构造方法创建了对象,其中super(null)调用的是父类的构造方法,传入的是权限集合,因为目前还没有认证通过,我们不知道有什么权限信息,所以这里设置为null,然后将用户名和密码分别赋值给principal和credentials,同样因为此时还未进行身份认证,所以给标记是否认证的字段进行赋值setAuthenticated(false)
(记住这张图,后面我们还要用到构造方法二)
(4)setDetails(request, authRequest)
是将当前的请求信息设置到authRequest中,包括ip、session等内容
(5)通过调用getAuthenticationManager()
来获取认证管理器对象AuthenticationManager
,然后调用他的实现类方法authenticate(Authentication authentication)
。认证管理器对象本身不包含验证的逻辑,它的作用是用来管理AuthenticationProvider
(存放在认证管理器接口的实现类内部的集合中)。
认证管理器接口的实现类ProviderManager
实现了authenticate(Authentication authentication)
方法,在该方法中遍历所有的AuthenticationProvider
,再通过遍历到的每一个AuthenticationProvider
对象的supports(Class<?> authentication)
方法判断本provider是否是支持该Authentication类型(如UsernamePasswordAuthenticationToken)认证方式的provider。如果支持,调用该provider的authenticate
方法进行认证。
如图代码所示
实际上authenticate的校验逻辑就写在了AbstractUserDetailsAuthenticationProvider
抽象类中,首先去缓存中取UserDetails对象,如果取不到就调用retrieveUser
方法来获取用户信息UserDetails调用了retrieveUser
方法获取到了一个user对象,retrieveUser是一个抽象方法。
为什么AuthenticationProvider
是一个集合去进行循环?是因为不同的登陆方式认证逻辑是不一样的,可能是微信等社交平台登陆,也可能是用户名密码登陆。AuthenticationManager
将AuthenticationProvider
收集起来,然后登陆的时候挨个去AuthenticationProvider
中问你这种验证逻辑支不支持此次登陆的方式,根据传进来的Authentication类型会挑出一个适合的provider来进行校验处理。
然后去调用provider的验证方法authenticate
方法,它是DaoAuthenticationProvider
类中的一个方法,DaoAuthenticationProvider
继承了AbstractUserDetailsAuthenticationProvider
。
用户信息UserDetails是个接口,我们进入查看,它包含以下6个接口方法:
Collection<? extends GrantedAuthority> getAuthorities();//获取权限集合
String getPassword(); //获取密码
String getUsername(); //获取用户名
boolean isAccountNonExpired(); //账户未过期
boolean isAccountNonLocked(); //账户未锁定
boolean isCredentialsNonExpired(); //密码未过期
boolean isEnabled(); //账户可用
查看它的继承关系发现User类实现了该接口,并实现了该接口的所有方法
DaoAuthenticationProvider
实现了retrieveUser
方法,在实现的方法中实例化了UserDetails对象。
也就是相当于自定义验证逻辑的那个类,去实现UserDetailService类,这个返回结果就是我们自己在数据库中根据username查询出来的用户信息。
在AbstractUserDetailsAuthenticationProvider
中如果没拿到信息就会抛出异常,如果查到了就回到AbstractUserDetailsAuthenticationProvider
中去调用preAuthenticationChecks的check
方法去进行预检查。
在预检查中进行了三个检查,因为UserDetail类中有四个布尔类型,去检查其中的三个,用户是否未锁定、用户是否未过期,用户是否不可用。
预检查之后紧接着去调用了additionalAuthenticationChecks
方法去进行附加检查,这个方法也是一个抽象方法,在DaoAuthenticationProvider
中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。
4.如果通过了预检查和附加检查,还会进行后检查,检查4个布尔值中的最后一个密码是否过期。
所有的检查都通过,则认为用户认证是成功的。
用户认证成功之后,会将这些认证信息和user传递进去,调用createSuccessAuthentication
方法。在这个方法中同样会实例化一个user,但是这个方法不会调用之前传两个参数的函数,而是会调用三个参数的构造函数。这个时候,在调super的构造函数中不会再传null,会将authorities权限设进去,之后将用户密码设进去,最后setAuthenticated(true)
,代表验证已经通过,如上面第三张图中的构造方法二。
二、认证结果如何在多个请求之间共享
在验证成功之后,其中会调用AbstractAuthenticationFilter
中的successfulAuthentication
方法,在这个方法最后会调用我们自定义的successHandle登陆成功处理器,在调用这个方法之前会调用SecurityContextHolder.getContext()
的setAuthentication
方法,会将我们验证成功的那个Authentication放到SecurityContext中,然后再放到SecurityContextHolder中。SecurityContextImpl
中只是重写了hashcode方法和equals方法去保证Authentication的唯一。
SecurityContextHolder是ThreadLocal的一个封装,ThreadLocal是线程绑定的一个map,在同一个线程里在这个方法里往ThreadLocal里设置的变量是可以在另一个线程中读取到的。它是一个线程级的全局变量,在一个线程中操作ThreadLocal中的数据会影响另一个线程。也就是说创建成功之后,塞进去,此次登陆所有的请求都会通过SecurityContextPersisenceFilter去SecurityContextHolder拿那个Authentication。SecurityContextHolder在整个过滤器的最前面。
当请求进来的时候,会先经过SecurityContextPersistenceFilter,SecurityContextPersistenceFilter会去session中去查SecurityContext的验证信息,如果有,就把SecurityContext的验证信息放到线程里直接返回回去,如果没有则通过,去通过其他的过滤器,当请求处理完回来之后,SecurityContextHolder会去检查当前线程中有没有SecurityContext的验证信息,如果有,则将SecurityContext放到session中。通过这样将不同的请求就可以从同一个session里拿到验证信息。
简单来说就是进来的时候检查session,有认证信息放到线程里。出去的时候检查线程,有认证信息放到session里。
因为整个请求和响应的过程都是在一个线程里去完成的,所以在线程的其他位置随时可以用SecurityContextHolder来拿到认证信息。
搬运的也差的不多,这些东西好像已经够用了,后面如果用到别的咱们再补充,告辞!
后记:
最近看到有大佬的博客讲得很好,大家有兴趣可以去看看
https://cloud.tencent.com/developer/article/1110641