目录
前提:springsecurity的功能的实现主要是由一系列过滤器链相互配合完成。
5.1.1 SecurityContextPersistenceFilter认识
5.1.2 SecurityContextPersistenceFilter debug 图解
5.1.3 UsernamePasswordAuthenticationFilter认识
5.1.4 UsernamePasswordAuthenticationFilter debug 图解
项目源代码:https://github.com/maojianqiu/Demos/tree/main/springsecurity01/demo02
5.身份认证解析+实例(demo02)
终于要学习实例啦,所有的学习都基于 springboot2.6.1 + spring security + mybatis+ Maven +IDEA工具。具体版本见上面源代码。
首先先将 demo01 拷贝一份
可跳过----------------------------------------------------------------
1.打开 demo01 文件夹,复制一份并将新的命名为 demo02 ,打开 demo02 ,删除里面的 .idea 文件夹,然后用 IDEA 的 import Project 打开 demo02 ,要选择 demo02 的pom.xml 文件;
2.打开 Project Structure 配置,修改这两处命名
3.成功
前提:springsecurity的功能的实现主要是由一系列过滤器链相互配合完成。
springsecurity的功能的实现主要是由一系列过滤器链相互配合完成。
我们需要的身份认证与授权认证都是基于过滤链实现的,并且 security 也会提供一个获取当前访问用户信息的快捷方式——SecurityContextHolder
在后面的学习中会慢慢学到,主要有以下过滤链:
在学习过滤链中发现了一个问题,还以为是springboot2.6.1的版本修改了,后来发现不是,如下:
下面是使用 http.authorizeRequests() 的默认过滤链
下面是使用 http.authorizeHttpRequests() 的默认过滤链
http.authorizeHttpRequests() 最后一个过滤链是 AuthorizationFilter ,
http.authorizeRequests() 最后一个过滤链是 FilterSecurityInterceptor,
现在网上关于 AuthorizationFilter 的资料太少,查不到,所以先学习http.authorizeRequests() 的。
(待学习 http.authorizeHttpRequests() http.authorizeRequests() 区别?)
5.1基于 session 的身份认证
其实上面学的的认证方式,也就是 security 默认的表单认证就是基于 session 的认证,这次学习的身份认证主要有 SecurityContextPersistenceFilter 、UsernamePasswordAuthenticationFilter这两个过滤链。
我们先用 debug 跑一下程序,程序正常运行后,按照下面添加两处断点:
每请求一次,都会经过这两个过滤器的。因为是过滤器,都会有 doFilter 方法,所以从 doFilter 方法看起。
看代码不要一行一行解读,先了解大致是做什么的,然后看相关的代码是不是主要解决这个问题的,然后通过 debug 判断,一层一层解析。
5.1.1 SecurityContextPersistenceFilter认识
SecurityContextPersistenceFilter 是 SecurityContext 持久化的过滤器,这个也是SecurityContext相关的过滤器。
网上解读这个类为:当有请求进来时,首先从 SecurityContextRepository 取出已经持久化的SecurityContext对象(如果没有则为null),并将其设置到SecurityContextHolder对象中,等后续的过滤器执行完后再将其从SecurityContextHolder中清除。
有疑问了:
- SecurityContext 是什么?
- SecurityContextHolder是什么?
- SecurityContextRepository是什么?
- 持久化是什么?有什么作用?
从网上查询到的结果并结合自己的学习理解,先写出来之后再慢慢调试验证:
- SecurityContext 解译为安全上下文,理解为存储"当前用户"账号信息和相关权限,每个请求来都会有对应的用户或者是空。(关于上下文的理解可以看这篇文章,是属于程序范围的解释:关于Context(上下文)的理解)
- SecurityContextHolder 是用来存放 SecurityContext 的对象,整个应用就一个SecurityContextHolder,这个类默认是使用ThreadLocal(没有熟练使用,但是理解意思,待学习)实现的,ThreadLocal就保证了本线程内所有的方法都可以获得SecurityContext对象,也就是在这一整个请求中,我们可以在任意地方拿取当前登录用户信息,不用必须通过参数传递用户信息,但是思考:既然有保存,就得有清除,不然后面其他用户请求时就会和之前的请求用户信息混淆了。(后面会看到,当过滤链执行完毕后在 finally 里面会清除的)
- SecurityContextRepository 是用来在HttpSession中获取保存安全上下文的,那么 HttpSession 又是什么呢?HttpSession是由tomcat创建的,当客户浏览器打开后第一次访问Tomcat服务器,Tomcat会创建一个HttpSesion对象,存入一个ConcurrentHashMap,Key是SessionId,Value就是HttpSession。然后请求完成后,在返回的报文中添加Set-Cookie:JSESSIONID=xxx,然后客户端浏览器会保存这个Cookie。当浏览器再次访问这个服务器的时候,都会带上这个Cookie。Tomcat接收到这个请求后,根据JSESSIONID把对应的HttpSession对象取出来,放入HttpSerlvetRequest对象里面。也就是说每次请求时 tomcat 都会先从 session 中取出当前访问用户唯一 sessionID 的内容也就是 HttpSession 并放到 HttpSerlvetRequest 中,这样我们就能通过 request 拿到 httpsession ,并从 httpsesiion 中拿到当前登录用户的信息(未登录就是空呗),当请求结束就会通过 SecurityContextRepository 将当前用户信息再放到 HttpSession 中并存到 tomcat 的session中。
- 持久化就是上面 2.3.合起来的意思
现在大致了解了 SecurityContextPersistenceFilter ,所以我们默认的 security 认证会通过 tomcat 基于 session 来认证,如果用户关闭了 cookie ,tomcat 就无法拿取 sessionID ,就获取不到用户登录信息啦。
5.1.2 SecurityContextPersistenceFilter debug 图解
下面来看 debug 图解(如果专注于身份认证可以跳过下面验证,直接看UsernamePasswordAuthenticationFilter):
当我们访问服务器网址时,会定位到这个类的 dofilter 方法里面,核心就在 :
public class SecurityContextPersistenceFilter extends GenericFilterBean {
private SecurityContextRepository repo;
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
...
//repo 就是 SecurityContextRepository ,loadContext() 就是获取 securityContext 对象,security 默认使用HttpSessionSecurityContextRepository 实现接口来获取 securityContext 信息,之后返回 SecurityContext 对象
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
...
try {
//拿到 securityContext 之后,存到 SecurityContextHolder 对象里面
SecurityContextHolder.setContext(contextBeforeChainExecution);
...
//之后就是调用下一个的过滤链,如果通过所有过滤链后,就会执行请求接口,执行完后会一级一级返回来
chain.doFilter(holder.getRequest(), holder.getResponse());
...
}
//当执行完后面过滤链并完成请求后,或者当前流程出现异常后,就执行finally ,也就是无论这个请求有没有完成后会进行下面操作
finally {
//拿到当前 security上下文
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
//清除 SecurityContextHolder 对象里面的上下文信息
SecurityContextHolder.clearContext();
//将当前上下文信息通过 SecurityContextRepository 再存储到 session 中
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
...
}
}
}
跟上面解答的内容是匹配的!
我们也看一下 SecurityContextRepository 的 默认是想方法HttpSessionSecurityContextRepository 是怎样工作的,可以进去这个类打断点,也可以从 SecurityContextPersistenceFilter 类的断点按 F7 进去:
在这里会拿取 request 中的 session 信息,而这个session 信息是 tomcat 给匹配 sessionid 后返回的数据(暂时是这样理解的,有不对的可以评论区留言~)
不过对于怎样从tomcat 中获取 session 信息,还是不懂,但是这不影响后面的使用(待学习)
5.1.3 UsernamePasswordAuthenticationFilter认识
UsernamePasswordAuthenticationFilter是主要用来处理用户登录时的验证操作,是 security 默认使用的,无论我们使用的是security 默认的登录页面还是我们自定义的登录页面都是通过这个类进行身份认证的。
网上解读结合自己的理解,这个类为:UsernamePasswordAuthenticationFilter 继承了 AbstractAuthenticationProcessingFilter 接口,在父类进行路径匹配后,就调用自己实现的 attemptAuthentication 方法将信息赋值给 UsernamePasswordAuthenticationToken authRequest,然后调用 AuthenticationManager 接口的authenticate(authRequest)对用户密码的正确性进行验证,ProviderManager 是 AuthenticationManager 的实现类,在 ProviderManager 中通过匹配对应的 AuthenticationProvider 接口来认证,认证失败就抛出异常,成功就返回Authentication对象。
有疑问了:
- AbstractAuthenticationProcessingFilter是干嘛的?为什么要在他这里进行路径匹配?
- UsernamePasswordAuthenticationToken是干什么的?为什么要将信息赋值给它?
- AuthenticationManager又是干什么的?
- AuthenticationProvider也不认识,是干什么的?为什么要匹配?怎么是在这个里面进行的身份认证呢?
- Authentication对象是最终的用户信息对象吗?是封装的吗?
从网上查询到的结果并结合自己的学习理解,先写出来之后再慢慢调试验证:
- AbstractAuthenticationProcessingFilter,官方文档说的很明白了:处理基于浏览器交互的HTTP验证请求。所以AbstractAuthenticationProcessingFilter的职责也就非常明确——处理所有HTTP Request和Response对象,并将其封装成AuthenticationMananger可以处理的Authentication。并且在身份验证成功或失败之后将对应的行为转换为HTTP的Response。同时还要处理一些Web特有的资源比如Session和Cookie。总结成一句话,就是替AuthenticationMananger把所有和Authentication没关系的事情全部给包圆了。想了解 AbstractAuthenticationProcessingFilter 得先把 AuthenticationMananger Authentication了解了。有一点,AbstractAuthenticationProcessingFilter 会通过匹配“登录”路径来确认是否需要进行身份认证的,如果没有匹配到“登录”路径就不会进行身份认证操作,比如已登录了,但是思考:为何核心逻辑都在基类 AbstractAuthenticationProcessingFilter 中,而尝试认证却需要子类来实现?(看完 3. 后发现,会有很多认证方式,那么 AbstractAuthenticationProcessingFilter 就只关心前面的认证数据,怎么通过认证的让 AuthenticationMananger 实现)
- UsernamePasswordAuthenticationToken,UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication,也就是他相当于 Authentication 对象的一个实现类。但是思考:为什么要有这个实现类呢?所以继续往下看。(看完 3. 后发现,因为有很多认证方式,所以就会有对应的存储认证信息的方式)
- AuthenticationManager 上面说到这个接口要处理 Authentication 的,但是它一般不直接认证,它的常用实现类 ProviderManager 内部会维护一个List列表 List<AuthenticationProvider> providers;存放多种认证方式,也就是 AuthenticationProvider 才是认证的,并且有多种认证方式,(实际上这是委托者模式的应用)但是思考:为什么有多种认证方式呢?(因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录,这就对应了三个AuthenticationProvider)
- AuthenticationProvider 是用户自定义身份认证,认证流程顶级接口,真正的是在这里身份认证的。我们也可以自定义 ,实现自定义身份认证。一般默认用的是 DaoAuthenticationProvider,并且这个最终获取的用户数据源是通过 UserDetailsService 来获取的!这不就和之前学的 UserDetailsService 对应上了吗!
- Authentication 直接继承自Principal类,Authentication在spring security中是最高级别的身份/认证的抽象接口。
5.1.4 UsernamePasswordAuthenticationFilter debug 图解
下面来看 UsernamePasswordAuthenticationFilter 的debug 图解:
现在看的 UsernamePasswordAuthenticationFilter 是 security 默认的一种使用用户名密码登录的认证方式,先查看学习这种方式,后面就可以基于这个进行自定义的认证!
下面 debug 关联的内容比较多,头疼:
1.先来看一下 UsernamePasswordAuthenticationFilter 的源码中:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
//这个是默认的登录路径匹配方式,默认就是 “/login”,就像我们第一次使用security 的时候,用的就是 “/login” 请求,可是后来我们又配置了 .formLogin().loginPage("/newlogin.html"),那么它的父类就记录了新的 "/newlogin.html" 登录接口
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
...
}
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
//这里存放的就是新的"/newlogin.html" 登录接口,所以就能匹配到我们的登陆请求!
private RequestMatcher requiresAuthenticationRequestMatcher;
...
}
题外话:还记得我们 day02 3.2.2 使用 .loginPage("/newlogin.html").loginProcessingUrl("login001") 自定义处理登陆的设置吗?当时就有提到并不会调用我们自己写的 "login001" controller接口,而是告诉 security 拦截这个接口然后去调用 security 的登陆处理!
记得测完后把 loginProcessingUrl("login001") 去掉,或者在 login.html 的 form action中修改为 “login001”,
也就是说保证你的登录页面(login.html)的请求 action === 你的登陆请求
我再测完上面的逻辑后,忘了把 loginProcessingUrl("login001") 去掉,而我的登录页面 action 还是/newlogin.html,这就导致登录页面的请求与实际登录请求不一致!
也不知道能不能表达清楚~
所以 UsernamePasswordAuthenticationFilter 只会拦截“登录”请求,非登录请求是不会拦截的!
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//requiresAuthentication()方法是判断是否是登陆请求,不是的话就 !false
if (!requiresAuthentication(request, response)) {
//不是就继续执行后面的过滤链
chain.doFilter(request, response);
return;
}
...
}
...
}
2.而拦截到登录接口之后,就会进行后面的身份认证:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
//若匹配到登录请求走下面
try {
//在这里调用 UsernamePasswordAuthenticationFilter 的 attemptAuthentication方法进行后面认证
Authentication authenticationResult = attemptAuthentication(request, response);
//如果没有认证成功直接return
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
...
}
...
}
我们去看 UsernamePasswordAuthenticationFilter 的 attemptAuthentication()
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
...
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
...
//1.这里是获取请求中的用户名和密码
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
//2.将用户名和密码封装给 Authentication (可查看Token的父类,就是 Authentication ),注意这里的 Authentication 对象是未认证的
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//3.这里将请求中的 ip session 等内容赋值给 Authentication
setDetails(request, authRequest);
//4.最后将请求的 Authentication 信息传给 AuthenticationManager 进行身份认证
return this.getAuthenticationManager().authenticate(authRequest);
}
...
}
UsernamePasswordAuthenticationToken 这个对象的意义是什么呢? 因为我们需要将请求的用户信息与存储的用户信息进行对比,如果把所有的信息通过好多入参出参进行传输,会很麻烦,所以不如将用户信息都封装到一起,直接传一个对象,里面设置一个状态:认证、未认证,那么就很方便的传输操作了!
我们看 UsernamePasswordAuthenticationToken 的源码就是一个封装类:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
private final Object principal;
private Object credentials;
//这里就是上方 3. 使用的构造方法,可以看到 Authenticated 是 false ,也就是没有身份认证哦!
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
//后面认证成功后,会使用这个构造方法在创建一个认证的!
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
public Object getCredentials() {
return this.credentials;
}
public Object getPrincipal() {
return this.principal;
}
//可以看到如果想要创建一个认证的token ,只能通过构造方法来创建,不能 set !!!!也是为了安全!
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
3.难点来了,AuthenticationManager ,我们在 4. 进入AuthenticationManager 的authenticate(authRequest); 可以看到默认实现的是 ProviderManager 类,
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
// 认证提供者列表。认证提供者主要根据传入的不同的 Authentication(也就是上方 4. 传的 入参) 决定使用哪种认证方式
// providers 集合在项目启动的时候会被初始化进去。我看到的主要初始化的提供者只有一个:
// AnonymousAuthenticationProvider
//但是有的文章中说是有两个,还有一个: DaoAuthenticationProvider
private List<AuthenticationProvider> providers;
//上一级认证管理者,我这边看到的是 DaoAuthenticationProvider,这个也是security默认处理方式
private AuthenticationManager parent;
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
//这里是获取所有的认证提供者列表
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
//这里进行认证提供者匹配,若匹配成功了就用当前匹配成功的 provider 进行身份认证
if (provider.supports(toTest)) {
...
try {
//使用当前匹配成功的 provider 进行身份认证
result = provider.authenticate(authentication);
...
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
...
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
...
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
//返回最后的结果 Authentication
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
...
}
疑问: 当前的提供者管理是默认的 ProviderManager ,里面提供者列表只有一个,是AnonymousAuthenticationProvider !而他的上级提供者管理也就是 parent 里面的提供者列表也只有一个,是DaoAuthenticationProvider !
可以看到最终调用的是 parent 的 DaoAuthenticationProvider
AnonymousAuthenticationProvider 是匿名登录认证,还不知道是怎么使用(待学习) 和 DaoAuthenticationProvider security 默认的认证提供者。
并不是没有匹配上而最终默认匹配成功的!一定是匹配上的!
看下图:
那么我们去 DaoAuthenticationProvider 的 supports 方法查看!
//继承了 AbstractUserDetailsAuthenticationProvider ,没有覆写 supports 方法,所以去 父类看 supports 方法
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
}
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
//可以看到 这个类默认的就是 UsernamePasswordAuthenticationToken !
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
...
}
所以真相了!我们使用 security 默认的身份封装就是 UsernamePasswordAuthenticationToken ,而默认的 身份认证提供就是 DaoAuthenticationProvider ,这样我们就能进行匹配啦!
4.说完匹配身份认证提供者,下面开始看提供者怎样进行身份认证:
我们从上方 ProviderManager 类的 authenticate(...) 进入 result = provider.authenticate(authentication); 断点,我们其实进入的是他的父类的 authenticate 方法
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
//这里是拿取的内存?(待学习)
UserDetails user = this.userCache.getUserFromCache(username);
//总之如果没有获取到 user ,就走下面的
if (user == null) {
...
try {
// 在这里根据用户名检索用户信息,重点是这里!
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
...
}
...
}
//这里会拿取到 user ,进行检验
try {
//依序校验:账户锁定、账户可用、账户过期
this.preAuthenticationChecks.check(user);
// 校验请求的用户和数据库中的 用户名和密码是否一致! this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
//这里是有问题,所以再次执行一下
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
// 校验:凭证过期
this.postAuthenticationChecks.check(user);
// 如果没有缓存用户信息,则使用userCache缓存。
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//返回 Authentication 对象,即认证对象
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
...
}
我们看重点 user = this.retrieveUser(username, (...)authentication); !!进入断点查看:
我们发现实际上调用的还是 DaoAuthenticationProvider 类的 retrieveUser()方法,到这里我们看到熟悉的三个单词:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
一个 UserDetails 接口,一个 UserDetailsService 接口,一个 loadUserByUsername ()方法。
有没有很熟悉,这不就是我们在学习4.2自定义的数据库模型认证与授权的时候使用的吗!
我们自定义的用户实体类实现 UserDetails 接口,来封装用户对象。然后我们自定义的获取用户信息类实现 UserDetailsService 接口,并覆写 loadUserByUsername(username);方法,在 loadUserByUsername 方法里面调用我们自己的 mapper (持久层 mybatis 调用数据库)获取用户信息。
我们 demo02 是直接复制的 demo01 ,所以也会有实体类和自定义的 UserDetailsService 接口,那么这里会不会使用我们自定义的的呢?
我们通过 debug 发现,这里就是我们自定义的类 MyUserDetailsService !
不信的话,我们 F7 进入方法里面,发现就是我们自己定义的 MyUserDetailsService 类
所以在这里拿取到我们的数据源用户信息,之后就需要进行验证啦!
5.拿取到用户后,在哪里验证呢?可以看到还是在 provider 实现类里面进行验证!
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
//无论如何这里之前会拿取到 user ,进行检验,如果数据库中没有这个用户,那么上面就会抛出并catch到异常,根本走不到验证!
try {
//依序校验:账户锁定、账户可用、账户过期
this.preAuthenticationChecks.check(user);
// 校验请求的用户和数据库中的 用户名和密码是否一致!也是 DaoAuthenticationProvider 覆写的!
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
//这里是上方验证有异常,再进行一次验证!一样的流程
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
...
}
...
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
...
} else {
//这里是拿取请求中用户的密码
String presentedPassword = authentication.getCredentials().toString();
//将请求中用户密码与数据源里的密码进行对比,如果一致当前方法就结束了
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
//如果不一致,就会抛出异常,也就是没有匹配上
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
...
}
验证成功就会进行缓存等操作,最后返回 Authentication 对象也就是已认证的对象。
注意如果验证失败,没有这个用户或者用户密码不对都会重新定位到登录页面(待分析)
6.最后我们要一层一层向上面返值啦!首先我们之前提到过在 UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 中创建了一个没有认证的 UsernamePasswordAuthenticationToken ,也就是我们一直向后面传的请求的用户信息。
现在既然已经认证成功,那么按照之前 5.1.3 中的 2. 的描述,我们不能更改 Token 的状态,只能重新创建一个已认证的 UsernamePasswordAuthenticationToken ,对不多?我们来看是不是这样的,我们继续看 AbstractUserDetailsAuthenticationProvider 的 authenticate () 方法的返回值:
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
// 校验:凭证过期
this.postAuthenticationChecks.check(user);
// 如果没有缓存用户信息,则使用userCache缓存。
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//返回 Authentication 对象,即认证对象
//看这个方法!,DaoAuthenticationProvider 也覆写了,但是最终还是又调用了super 类的了,看 DaoAuthenticationProvider 类
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
//先看传参
//principal:用户名,authentication:请求的用户信息,user:数据源的用户信息
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
//在这里我们创建了一个已认证的用户信息!!!
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
//并把请求中的详细内容也赋值给这个用户信息
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
//这里进行返回
return result;
}
...
}
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
//子类覆写的
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
//网上的意思是:如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。
//但是没有其他详细解释,这里的 this.passwordEncoder 就是我们在配置里面使用的 BCryptPasswordEncoder (待学习)
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);
}
...
}
到此我们通过 this.createSuccessAuthentication(principalToReturn, authentication, user); 就拿到了已认证的用户信息!
然后一路返值:
- DaoAuthenticationProvider 的 authenticate() 返值给 ProviderManager 的 authenticate() ;
- ProviderManager 的 authenticate() 返值给 UsernamePasswordAuthenticationFilter 的 attemptAuthentication () ;
- UsernamePasswordAuthenticationFilter 的 attemptAuthentication () 返值给父类 AbstractAuthenticationProcessingFilter 的 doFilter 方法;
7.最终终于又来到了 AbstractAuthenticationProcessingFilter (UsernamePasswordAuthenticationFilter 父类)过滤链,我们尝试拿到用户信息后需要进行什么操作呢?一定是保存当前认证用户,一个是方便当前请求的后续的过滤链使用,一个是方便后续请求使用。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
try {
//在这里拿到获取认证后的用户信息
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
//没有得到认证结果,表明子类实现中无法处理该类型的认证
return;
}
//放到 session 缓存中,后面应该会保存到 tomcat 的 session 中,方便后续请求使用(待学习)(是 CompositeSessionAuthenticationStrategy 实现类)
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//注意这里,当返回认证成功的用户后,需要进行存储对吧,就是在这里处理哒!
successfulAuthentication(request, response, chain, authenticationResult);
}
...
}
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//这里通过 SecurityContextHolder 创建一个 security上下文
SecurityContext context = SecurityContextHolder.createEmptyContext();
//把当前用户信息放入当前上下文中
context.setAuthentication(authResult);
//把当前上下文放入 SecurityContextHolder 中,方便当前请求接口后续业务流程使用!
SecurityContextHolder.setContext(context);
...
//认证成功,会调用认证成功的处理器,(后面学习自定义成功处理器!)
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
...
}
完美~一切完成,东西太多了最后总结一下:
5.1.5 总结
- 过滤链走到UsernamePasswordAuthenticationFilter 类时,调用父类AbstractAuthenticationProcessingFilter#doFilter()方法,拦截器的入口,如果拦截到登陆请求(security 默认 “/login”,可以自定义),就走2,否则走 。
- 调用其子类UsernamePasswordAuthenticationFilter#attemptAuthentication()方法,然后根据用户名密码构造暂未认证的UsernamePasswordAuthenticationToken对象,并提交给认证管理器(AuthenticationManager)
- AuthenticationManager 本身并不做验证处理,其核心是用来管理所有的 AuthenticationProvider,由它的实现类 ProviderManager 通过 for-each 遍历找到符合当前登录方式的一个 AuthenticationProvider,并交给它进行验证处理
- ProviderManager 查找发现 AbstractUserDetailsAuthenticationProvider 支持UsernamePasswordAuthenticationToken
- AbstractUserDetailsAuthenticationProvider#authenticate()
认证过程需要用户权限信息,就让其子类 DaoAuthenticationProvider#retrieveUser()
去获取用户数据并检验密码的正确性- DaoAuthenticationProvider 调用我们自定义UserDetailsService重写的loadUserByUsername()方法获取到用户信息,并返回给父类
- AbstractUserDetailsAuthenticationProvider#authenticate()获取到用户权限信息之后,重新构造一个完全认证的Authentication对象,并依次往回传递,最终由AbstractAuthenticationProcessingFilter接收
- AbstractAuthenticationProcessingFilter接收的完全认证的Authentication对象后,进行后续处理,如会话、存储Authentication对象到上下文、调用认证成功或失败的处理程序。
终于整清楚了!下面就开始自定义 provider ,来实现自定义的认证逻辑!
文章学习:
Spring Security小教程 Vol 3. 身份验证的入口-AbstractAuthenticationProcessingFilter