Spring Security认证的完整流程记录
作者:Pseudocode 发布时间:2021-12-15 13:04:03
前言
本文以用户名/密码验证方式为例,讲解 Spring Security 的认证流程,在此之前,需要你了解 Spring Security 用户名/密码认证的基本配置。
Spring Security 是基于过滤器的,通过一层一层的过滤器,处理认证的流程,拦截非法请求。
认证上下文的持久化
处于最前面的过滤器叫做 SecurityContextPersistenceFilter
,Spring Security 是通过 Session 来存储认证信息的,这个过滤器的 doFilter
方法在每次请求中只执行一次,作用就是,在请求时,将 Session 中的 SecurityContext 放到当前请求的线程中(如果有),在响应时,检查县城中是否有 SecurityContext,有的话将其放入 Session。可以理解为将 SecurityContext 进行 Session 范围的持久化。
认证信息的封装
接着进入 UsernamePasswordAuthenticationFilter
,这是基于用户名/密码认证过程中的主角之一。
默认情况下,这个过滤器会匹配路径为 /login
的 POST 请求,也就是 Spring Security 默认的用户名和密码登录的请求路径。
这里最关键的代码是 attemptAuthentication
方法(由 doFilter
方法调用),源码如下:
@Override
public Authentication attemptAuthentication ( HttpServletRequest request, HttpServletResponse response )
throws AuthenticationException {
if ( this.postOnly && !request.getMethod () .equals ( "POST" )) {
throw new AuthenticationServiceException ( "Authentication method not supported: " + request.getMethod ()) ;
}
String username = obtainUsername ( request ) ;
username = ( username != null ) ? username : "";
username = username.trim () ;
String password = obtainPassword ( request ) ;
password = ( password != null ) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken ( username, password ) ;
// Allow subclasses to set the "details" property
setDetails ( request, authRequest ) ;
return this.getAuthenticationManager () .authenticate ( authRequest ) ;
}
在 attemptAuthentication
方法代码的第 12 行,使用从 request 中获取到的用户名和密码,构建了一个 UsernamePasswordAuthenticationToken
对象,我们可以看到这个构造方法的代码,非常简单:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);
}
只是保存了用户名和密码的引用,并且将认证状态设置为 false
,因为此时只是封装了认证信息,还没有进行认证。
我们再回到 attemptAuthentication
的代码,在方法的最后一行,将创建好的认证信息,传递给了一个 AuthenticationManager
进行认证。这里实际工作的是 AuthenticationManager
的实现类 ProviderManager
。
查找处理认证的 Provider 类
进入 ProviderManager
可以从源码中找到 authenticate
方法,代码比较长,我就不贴在这里了,你可以自行查找,我简述一下代码中的逻辑。
ProviderManager
本身不执行认证操作,它管理着一个 AuthenticationProvider
列表,当需要对一个封装好的认证信息进行认证操作的时候,它会将认证信息和它管理者的 Provider 们,逐一进行匹配,找到合适的 Provider 处理认证的具体工作。
可以这样理解,ProviderManager
是一个管理者,管理着各种各样的 Provider。当有工作要做的时候,它从来都不亲自去做,而是把不同的工作,分配给不同的 Provider 去操作。
最后,它会将 Provider 的工作成果(已认证成功的信息)返回,或者抛出异常。
那么,它是怎么将一个认证信息交给合适的 Provider 的呢?
在上一部分中,我们说到,认证信息被封装成了一个 UsernamePasswordAuthenticationToken
,它是Authentication
的子类,ProviderManager
会将这个认证信息的类型,传递个每个 Provider 的 supports
方法,由 Provider 来告诉 ProviderManager
它是不是支持这个类型的认证信息。
认证逻辑
在 Spring Security 内置的 Provider 中,与 UsernamePasswordAuthenticationToken
对应的 Provider 是 DaoAuthenticationProvider
,authenticate
方法在它的父类 AbstractUserDetailsAuthenticationProvider
中。我们来看它的 authenticate
方法:
@Override
public Authentication authenticate ( Authentication authentication ) throws AuthenticationException {
Assert.isInstanceOf( UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage ( "AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported" )) ;
String username = determineUsername ( authentication ) ;
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache ( username ) ;
if ( user == null ) {
cacheWasUsed = false;
try {
user = retrieveUser ( username, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
catch ( UsernameNotFoundException ex ) {
this.logger.debug ( "Failed to find user '" + username + "'" ) ;
if ( !this.hideUserNotFoundExceptions ) {
throw ex;
}
throw new BadCredentialsException ( this.messages
.getMessage ( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials" )) ;
}
Assert.notNull( user, "retrieveUser returned null - a violation of the interface contract" ) ;
}
try {
this.preAuthenticationChecks.check ( user ) ;
additionalAuthenticationChecks ( user, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
catch ( AuthenticationException ex ) {
if ( !cacheWasUsed ) {
throw ex;
}
// 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 ) ;
this.preAuthenticationChecks.check ( user ) ;
additionalAuthenticationChecks ( user, ( UsernamePasswordAuthenticationToken ) authentication ) ;
}
this.postAuthenticationChecks.check ( user ) ;
if ( !cacheWasUsed ) {
this.userCache.putUserInCache ( user ) ;
}
Object principalToReturn = user;
if ( this.forcePrincipalAsString ) {
principalToReturn = user.getUsername () ;
}
return createSuccessAuthentication ( principalToReturn, authentication, user ) ;
}
代码比较长,我们说要点:
代码第 12 行,通过
retrieveUser
方法,获得UserDetails
信息,这个方法的具体实现,可以在DaoAuthenticationProvider
中找到,主要是通过UserDetailsService
的loadUserByUsername
方法,查找系统中的用户信息。代码第 25 行,通过
preAuthenticationChecks.check
方法,进行了认证前的一些校验。校验的具体实现可以在DefaultPreAuthenticationChecks
内部类中找到,主要是判断用户是否锁定、是否可用、是否过期。代码第 26 行,通过
additionalAuthenticationChecks
方法,对用户名和密码进行了校验。具体实现可以在DaoAuthenticationProvider
中找到。代码第 39 行,通过
postAuthenticationChecks.check
方法,校验了密码是否过期。具体实现可以在DefaultPostAuthenticationChecks
内部类中找到。最后,如果以上校验和认证都没有问题,则通过
createSuccessAuthentication
方法,创建成功的认证信息,并返回。此时,就成功通过了认证。
在最后的 createSuccessAuthentication
方法中,会创建一个新的 UsernamePasswordAuthenticationToken
认证信息,这个新的认证信息的认证状态为 true
。表示这是一个已经通过的认证。
这个认证信息会返回到 UsernamePasswordAuthenticationFilter
中,并作为 attemptAuthentication
方法的结果。
在 doFilter
方法中,会根据认证成功或失败的结果,调用相应的 Handler 类进行后续的处理,最后,认证的信息也会被保存在 SecurityContext 中,供后续使用。
来源:https://juejin.cn/post/7054569250307964958


猜你喜欢
- 前言插入排序狭义上指的是简单插入排序(选择集合,比较大小,插入元素),广义上还应该包括希尔排序(分治思想)及其两种实现方式,最激动人心的是
- 如下所示:using System;using System.Collections.Generic;using System.Linq;u
- 文件的读写是很多应用程序具有的功能,甚至某些应用程序就是围绕着某一种格式文件的处 理而开发的,所以文件读写是应用程序开发的一个基本功能。Qt
- 面向方面编程(Aspect Oriented Programming,简称AOP)是一种声明式编程(Declarative Programm
- 1.简介其实这个效果几天之前就写了,但是一直没有更新博客,本来想着把芝麻分雷达图也做好再发博客的,然后今天看到鸿洋的微信公众号有朋友发了芝麻
- MyBatis-Spring允许你在Service Bean中注入映射器。当使用映射器时,就像调用DAO那样来调用映射器就可以了,但是此时你
- 本文汇总了常用的DateTime日期类型格式化显示方法,方便读者在使用的时候参考借鉴一下。具体如下所示:1.绑定时格式化日期方法:<A
- 说到java中的重载和覆盖呢,大家都很熟悉了吧,但是呢我今天就要写这个。本文主题:一.什么是重载二.什么是覆盖三.两者之间的区别重载(ove
- Redis 3.X版本引入了集群的新特性,为了保证所开发系统的高可用性项目组决定引用Redis的集群特性。对于Redis数据访问的支持,目前
- 由于项目需求,需要为Java提供一套支持事件驱动机制的类库,可以实现类似于C#中的event和delegate机制。众所周知,Java语言本
- 很多情况下sql不好解决的多表查询,临时表分组,排序,尽量用java8新特性stream进行处理使用java8新特性,下面先来点基础的Lis
- 目前的App在安装后,第一次打开,都会显示两秒左右的logo,然后进入引导页。如果关闭App,再重新打开,则只会显示logo,然后直接进入主
- 本文实例为大家分享了MVPXlistView上拉下拉展示的具体代码,供大家参考,具体内容如下抽基类package com.gs.gg.day
- 一、单线程改造为多线程也是个技术活正如我们看到耗子叔叔博客里写的那样,原来是单线程的应用程序,”后来,我们的程序性能有问题,所以需要变成多线
- 以下内容给大家介绍Android数据存储提供了五种方式:1、SharedPreferences2、文件存储3、SQLite数据库4、Cont
- 本文介绍了ListView给每个Item上面的按钮添加事件,具体如下:1.先看下效果图:在这里仅供测试,我把数据都写死了,根据需要可以自己进
- 本文实例讲述了Android非XML形式动态生成、调用页面的方法。分享给大家供大家参考。具体分析如下:这个问题是这样的:我们不使用XML构建
- 前面一篇有说道如何在MyEclipse中搭建maven项目,这里将继续介绍如何在搭建好的基础maven项目中引入我们常用的javaweb框架
- 之前从他人的博文,还有一些书籍中了解到 常量是放在常量池 中,细节的内容无从得知,总觉得面前的东西是一个几乎完全的黑盒,总是觉得不舒服,于是
- 网上文章虽多,但是这种效果少之又少,我真诚的献上以供大家参考实现原理:自定义ImageView对此控件进行相应的layout(动态布局).这