SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解
用户认证流程
UsernamePasswordAuthenticationFilter
我们直接来看UsernamePasswordAuthenticationFilter
类,
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 判断是否是 POST 请求
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 获取请求中的用户,密码。
// 就是最简单的:request.getParameter(xxx)
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 生成 authRequest,本质就是个 usernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 把 request 请求也一同塞进 token 里
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
// 将 authRequest 塞进 AuthenticationManager并返回
return this.getAuthenticationManager().authenticate(authRequest);
}
}
在attemptAuthentication()
方法中:主要是先进行请求判断并获取username
和password
的值,然后再生成一个UsernamePasswordAuthenticationToken
对象,将这个对象塞进AuthenticationManager
对象并返回,注意:此时的authRequest
的权限是没有任何值的。
UsernamePasswordAuthenticationToken
不过我们可以先看看UsernamePasswordAuthenticationToken
的构造方法:
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
其实UsernamePasswordAuthenticationToken
是继承于Authentication
,该对象在学习笔记一中的中"自定义处理登录成功/失败"章节里的自定义登录成功里有提到过,它是处理登录成功回调方法中的一个参数,里面包含了用户信息、请求信息等参数。
来一张继承关系图,对其有个大概的认识,注意到Authentication
继承了Principal
。
AuthenticationManager
AuthenticationManager
是一个接口,它的所有实现类如图:
其中一个十分核心的类就是:ProviderManager
,在attemptAuthentication()
方法最后返回的就是这个类
this.getAuthenticationManager().authenticate(authRequest);
进入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();
for (AuthenticationProvider provider : getProviders()) {
// 1.判断是否有provider支持该Authentication
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
// 2. 真正的逻辑判断
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
……
}
}
……
}
这里首先通过 provider 判断是否支持当前传入进来的Authentication
,目前我们使用的是UsernamePasswordAuthenticationToken
,因为除了帐号密码登录的方式,还会有其他的方式,比如JwtAuthenticationToken
。
从整体来看Authentication
的实现类如图:
官方 API 文档列出了所有的子类
从整体来看AuthenticationProvider
的实现类如图:
官方 API 文档列出了所有的子类
根据我们目前所使用的UsernamePasswordAuthenticationToken
,provider 对应的是AbstractUserDetailsAuthenticationProvider
抽象类的子类DaoAuthenticationProvider
,其authenticate()
属于抽象类本身的方法。
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();
boolean cacheWasUsed = true;
// 1.从缓存中获取 UserDetails
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 2.缓存获取不到,就去接口实现类中获取
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
……
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
// 3.用户信息预检查(用户是否密码过期,用户信息被删除等)
preAuthenticationChecks.check(user);
// 4.附加的检查(密码检查:匹配用户的密码和服务器中的用户密码是否一致)
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;
}
}
// 5.最后的检查
postAuthenticationChecks.check(user);
……
// 6.返回真正的经过认证的Authentication
return createSuccessAuthentication(principalToReturn, authentication, user);
}
注意:retrieveUser()
的具体方法实现是由DaoAuthenticationProvider
类完成的:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 获取用户信息
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) {
……
}
}
}
同时createSuccessAuthentication()
的方法也是由DaoAuthenticationProvider
类来完成的:
// 子类拿 user 对象
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
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);
}
// 调用父类的方法完成 Authentication 的创建
return super.createSuccessAuthentication(principal, authentication, user);
}
// 创建已认证的 Authentication
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
小结:authenticate()
的认证逻辑
- 去调用自己实现的
UserDetailsService
,返回UserDetails
- 对 UserDetails 的信息进行校验,主要是帐号是否被冻结,是否过期等
- 对密码进行检查,这里调用了
PasswordEncoder
,检查 UserDetails 是否可用。 - 返回经过认证的
Authentication
编码技巧提示:这里在认证之前使用了
Assert.isInstanceOf()
进行断言校验,方法内部也不断用了Assert.notNull()
,这种编码非常的灵巧,省去了后续的类型判断。
这里的两次对UserDetails
的检查,主要就是通过它的四个返回 boolean 类型的方法(isAccountNonExpired()
,isAccountNonLocked()
,isCredentialsNonExpired()
,isEnabled()
)。
经过信息的校验之后,通过UsernamePasswordAuthenticationToken
的全参构造方法,返回了一个已经过认证的Authentication
。
拿到经过认证的Authentication
之后,至此UsernamePasswordAuthenticationFilter
的过滤步骤就完全结束了,之后就会进入BasicAuthenticationFilter
,具体来说就是去调用successHandler
。或者未通过认证,去调用failureHandler
。
已认证数据共享
完成了用户认证处理流程之后,我们思考一下是如何在多个请求之间共享这个认证结果的呢?因为没有做关于这方面的配置,所以可以联想到默认的方式应该是在session中存入了认证结果。思考:那么是什么时候存放入session中的呢?
认证流程完毕之后,再看是谁调用的它,发现是AbstractAuthenticationProcessingFilter
的doFilter()
进行调用的,这是AbstractAuthenticationProcessingFilter
继承关系结构图:
当认证成功之后会调用successfulAuthentication(request, response, chain, authResult)
,该方法中,不仅调用了successHandler
,还有一行比较重要的代码:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 调用了 UsernamePasswordAuthenticationFilter
authResult = attemptAuthentication(request, response);
……
// 调用方法,目的是保存到session
successfulAuthentication(request, response, chain, authResult);
}
// 将成功认证的用户信息保存到session
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
// 保存到 SecurityContextHolder 的静态属性 SecurityContextHolderStrategy 里, 非常重要的代码
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
}
// SecurityContextHolder类中存着 静态属性:SecurityContextHolderStrategy
public class SecurityContextHolder {
……
private static SecurityContextHolderStrategy strategy;
……
public static void setContext(SecurityContext context) {
strategy.setContext(context);
}
}
SecurityContextHolderStrategy
接口的所有实现类:
非常显眼的看出:ThreadLocalSecurityContextHolderStrategy
类:
final class ThreadLocalSecurityContextHolderStrategy implements
SecurityContextHolderStrategy {
private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
……
public void setContext(SecurityContext context) {
Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
// 将已认证的用户对象保存到 ThreadLocal<SecurityContext> 中
contextHolder.set(context);
}
……
}
注意:
SecurityContext
类的equals()
和hashCode()
方法已经重写了,用来保证了authentication的唯一性。
身份认证成功后,最后在UsernamePasswordAuthenticationFilter
返回后会进入一个AbstractAuthenticationProcessingFilter
类中调用successfulAuthentication()
方法,这个方法最后会返回我们自己定义的登录成功处理器handler
。
在返回之前,它会调用SecurityContext
,最后将认证的结果放入SecurityContextHolder
中,SecurityContext 类很简单,重写了equals()
方法和hashCode()
方法,保证了authentication的唯一性。
从代码可以看出:SecurityContextHolder
类实际上是对ThreadLocal
的一个封装,可以在不同方法之间进行通信,可以简单理解为线程级别的一个全局变量。
因此,可以在同一个线程中的不同方法中获取到认证信息。最后会被SecurityContextPersistenceFilter
过滤器使用,这个过滤器的作用是:
当一个请求来的时候,它会将 session 中的值传入到该线程中,当请求返回的时候,它会判断该请求线程是否有 SecurityContext
,如果有它会将其放入到 session 中,因此保证了请求结果可以在不同的请求之间共享。
用户认证流程总结
引用徐靖峰在个人博客Spring Security(一)--Architecture Overview中的概括性总结,非常的到位:
- 用户名和密码被过滤器获取到,封装成
Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。 AuthenticationManager
身份管理器负责验证这个Authentication
。- 认证成功后,
AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。 SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(…)
方法,设置到其中。
高度概括起来本章节所有用的核心认证相关接口:SecurityContextHolder
是
身份信息的存放容器,Authentication
是身份信息的抽象,AuthenticationManager
是身份认证器,一般常用的是用户名+密码的身份认证器,还有其它认证器,如邮箱+密码、手机号码+密码等。
再引用一张十分流行的流程图来表示用户的认证过程:
架构概览图
为了更加形象的理解,在徐靖峰大佬的经典架构图之上,根据自己的理解,做了更多的细化和调整:
获取认证用户信息
如果我们需要获取用的校验过的所有信息,该如何获取呢?上面我们知道了会将校验结果放入 session 中,因此,我们可以通过 session 获取:
@GetMapping("/me1")
@ResponseBody
public Object getMeDetail() {
return SecurityContextHolder.getContext().getAuthentication();
}
@GetMapping("/me2")
@ResponseBody
public Object getMeDetail(Authentication authentication){
return authentication;
}
在登录成功之后,上面有两种方式来获取,访问上面的请求,就会获取用户全部的校验信息,包括ip地址等信息。
如果我们只想获取用户名和密码以及它的权限,不需要ip地址等太多的信息可以使用下面的方式来获取信息:
@GetMapping("/me3")
@ResponseBody
public Object getMeDetail(@AuthenticationPrincipal UserDetails userDetails){
return userDetails;
}
参考资料:
https://www.cnkirito.moe/spring-security-1/
https://blog.csdn.net/u013435893/article/details/79605239
https://blog.csdn.net/qq_37142346/article/details/80032336
SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解的更多相关文章
- Spring Security教程(八):用户认证流程源码详解
本篇文章主要围绕下面几个问题来深入源码: 用户认证流程 认证结果如何在多个请求之间共享 获取认证用户信息 一.用户认证流程 上节中提到Spring Security核心就是一系列的过滤器链,当一个请求 ...
- SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能
在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...
- SpringBoot + Spring Security 学习笔记(三)实现图片验证码认证
整体实现逻辑 前端在登录页面时,自动从后台获取最新的验证码图片 服务器接收获取生成验证码请求,生成验证码和对应的图片,图片响应回前端,验证码保存一份到服务器的 session 中 前端用户登录时携带当 ...
- SpringBoot + Spring Security 学习笔记(一)自定义基本使用及个性化登录配置
官方文档参考,5.1.2 中文参考文档,4.1 中文参考文档,4.1 官方文档中文翻译与源码解读 SpringSecurity 核心功能: 认证(你是谁) 授权(你能干什么) 攻击防护(防止伪造身份) ...
- SpringBoot + Spring Security 学习笔记(四)记住我功能实现
记住我功能的基本原理 当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的 ...
- [转]Spring Security学习总结二
原文链接: http://www.blogjava.net/redhatlinux/archive/2008/08/20/223148.html http://www.blogjava.net/red ...
- 2017.3.31 spring mvc教程(二)核心流程及配置详解
学习的博客:http://elf8848.iteye.com/blog/875830/ 我项目中所用的版本:4.2.0.博客的时间比较早,11年的,学习的是Spring3 MVC.不知道版本上有没有变 ...
- Ext.Net学习笔记22:Ext.Net Tree 用法详解
Ext.Net学习笔记22:Ext.Net Tree 用法详解 上面的图片是一个简单的树,使用Ext.Net来创建这样的树结构非常简单,代码如下: <ext:TreePanel runat=&q ...
- Ext.Net学习笔记23:Ext.Net TabPanel用法详解
Ext.Net学习笔记23:Ext.Net TabPanel用法详解 上面的图片中给出了TabPanel的一个效果图,我们来看一下代码: <ext:TabPanel runat="se ...
随机推荐
- MySQL 开发实践 8 问,你能 hold 住几个?
最近研发的项目对DB依赖比较重,梳理了这段时间使用MySQL遇到的8个比较具有代表性的问题,答案也比较偏自己的开发实践,没有DBA专业和深入,有出入的请使劲拍砖!- MySQL读写性能是多少,有哪些性 ...
- 【NOI赛前训练】——专项测试1·网络流
T1: 题目大意: 传送门 给一个长度为$n(n<=200)$的数列$h$,再给$m$个可以无限使用的操作,第$i$个操作为给长度为花费$c_i$的价值给长度为$l_i$的数列子序列+1或-1, ...
- BZOJ_3524_[Poi2014]Couriers_主席树
BZOJ_3524_[Poi2014]Couriers_主席树 题意:给一个长度为n的序列a.1≤a[i]≤n. m组询问,每次询问一个区间[l,r],是否存在一个数在[l,r]中出现的次数大于(r- ...
- POJ1038 Bugs Integrated, Inc 状压DP+优化
(1) 最简单的4^10*N的枚举(理论上20%) (2) 优化优化200^3*N的枚举(理论上至少50%) (3) Dfs优化状压dp O(我不知道,反正过不了,需要再优化)(理论上80%) (4) ...
- 为什么说JAVA中要慎重使用继承
JAVA中使用到继承就会有两个无法回避的缺点: 打破了封装性,迫使开发者去了解超类的实现细节,子类和超类耦合. 超类更新后可能会导致错误. 继承打破了封装性 关于这一点,下面是一个详细的例子(来源于E ...
- postman接口测试举例情况
http请求:http请求分为请求头和请求体,get请求只有请求头没有请求体. 1.get请求 是可以直接在浏览器访问,不需要借助任何工具.好看一些,可以打开postman测试接口 http://xx ...
- WebGL three.js学习笔记 使用粒子系统模拟时空隧道(虫洞)
WebGL three.js学习笔记 使用粒子系统模拟时空隧道 本例的运行结果如图: 时空隧道demo演示 Demo地址:https://nsytsqdtn.github.io/demo/sprite ...
- C# winform 检测当前电脑安装的.net framework版本
private static bool GetDotNetRelease(int release) { const string subkey = @"SOFTWARE\Microsoft\ ...
- php session序列化攻击面浅析
目录 0x00 首先,session_start()是什么? 0x01 初识php-session序列化机制 0x02 php_serialize引擎(反)序列化测试 0x03 当使用不同的引擎来处理 ...
- angular2-7中的变化监测
最近做公司新项目用的angular7,中碰到了一个很头疼的问题在绑定对象中的数据改变时,页面视图没有跟新,需点击页面中的时间元素后才会更新.以前使用angularJs也经常碰到类似情况,这种时候一 ...