SpringSecurity身份认证流程分析

前言

本文适合能单独配置并运行 SpringSecurity框架,并且想了解验证具体详情的人员观看。非相关人员请撤离。
在这里插入图片描述
前方高能,非战斗人员请迅速撤离

使用到的版本

Spring Security为5.7.3
spring-boot-starter 为:2.7.4
(这里注意区分一下上面二者的区别)
SpringBoot版本也是2.7.4

示例代码

前言

关于本章所涉及的到的Spring Security的架构组件,诸如AuthenticationManagerProviderManagerAuthenticationAuthenticationProvider等等
可以查看Spring Security原文:Servlet Authentication Architecture

或者查看本人所做的大致翻译: SpringSecurity官网的Servlet Authentication Architecture部分的翻译

以便了解更加透彻。

当然其中非主要组件,请自行参考Spring Security的官方文档。

代码

//认证管理器
@Autowired
private AuthenticationManager authenticationManager;
//从前端传递来的用户名 密码
UsernamePasswordAuthenticationToken securityToken =
 new UsernamePasswordAuthenticationToken(userName ,
  password);
//SpringSecurity的认证 - 主要是这里
 Authentication authentication = 
 authenticationManager.authenticate(securityToken);

先来看看这几个类是干嘛的吧

AuthenticationManager

类上注释:

Processes an Authentication request.

处理认证请求

UsernamePasswordAuthenticationToken

类上的注释是这么写的:

An org.springframework.security.core.Authentication 
implementation that is designed for
 simple presentation of a username and password.
 
The principal and credentials should be set with an
 Object that provides the respective property via its 
 Object.toString() method. 
 The simplest such Object to use is String.

翻译:
身份验证实现类,旨在简单地表示用户名和密码

应该使用一个对象设置主体和凭据,该对象通过其Object.toString()方法提供相应的属性。
使用起来最简单的对象是String

也就是说上述这个类里面主要的作用是用于存储用户输入的用户名和密码

Authentication

类上的注释 其实看第一段就可以了

Represents the token for an authentication request or for an authenticated 
principal once the request has been processed 
by the AuthenticationManager.authenticate(Authentication) method.

Once the request has been authenticated, the Authentication will usually be stored in a thread-local SecurityContext managed by the SecurityContextHolder by the authentication mechanism which is being used. An explicit authentication can be achieved, without using one of Spring Security's authentication mechanisms, by creating an Authentication instance and using the code:
   SecurityContext context = SecurityContextHolder.createEmptyContext();
   context.setAuthentication(anAuthentication);
   SecurityContextHolder.setContext(context);
   
Note that unless the Authentication has the authenticated property set to true, it will still be authenticated by any security interceptor (for method or web invocations) which encounters it.

In most cases, the framework transparently takes care of managing the security context and authentication objects for you.

翻译:

表示token的身份验证请求或已经由AuthenticationManager.authenticate(authentication)方法处理了认证过的主体的请求。

一旦请求通过了身份验证,身份验证通常会存储在一个线程本地的SecurityContext中,由正在使用的身份验证机制的SecurityContextHolder管理。显式身份验证可以通过创建一个authentication实例并使用以下代码来实现,而不需要使用Spring Security的任何身份验证机制:

SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(anAuthentication);
SecurityContextHolder.setContext(context);

注意,除非Authentication将authenticated属性设置为true,否则它仍将由遇到它的安全拦截器(对于方法或web调用)进行身份验证。

在大多数情况下,框架透明地负责为您管理安全上下文和身份验证对象。

简单小结

AuthencationManager用于处理认证请求
UsernamePasswordAuthenticationToken用于将用户输入的用户名和密码封装
Authencation代表token的认证请求

认识用户DAO涉及到的类

SpringSecurity的话入门教程的话这篇还不错:SpringBoot整合SpringSecurity(通俗易懂)
就是版本老了点。

我们继续,SpringSecurity中有规定用户的实体类以及DAO的操作必须含有的方法

UserDetails

自定义User必须实现的接口,里面包含了验证时必需的方法。

先看类的注释:

Provides core user information.
Implementations are not used directly by Spring 
Security for security purposes. They simply store
 user information which is later encapsulated into
 Authentication objects. 
This allows non-security related user information
 (such as email addresses,
  telephone numbers etc) 
  to be stored in a convenient location.
  
Concrete implementations must take particular care to 
ensure the non-null contract detailed for each method
 is enforced. 
 See User for a reference implementation
  (which you might like to extend or use in your code).

提供用户核心信息。
出于安全目的,Spring Security不会直接使用实现。它们只是存储稍后封装到Authentication对象中的用户信息。这允许将非安全相关的用户信息(如电子邮件地址、电话号码等)存储在一个方便的位置。

具体实现必须特别小心,以确保执行每个方法的详细非空契约。参见User获得参考实现(您可能希望在代码中扩展或使用该实现)。

UserDetailsService

先看类注释

Core interface which loads user-specific data.
It is used throughout the framework as a user DAO
 and is the strategy used by 
 the DaoAuthenticationProvider.
The interface requires only one read-only method,
 which simplifies support for 
 new data-access strategies

加载用户特定数据的核心接口。(也就是从数据库中根据用户名获取到的用户信息
它在整个框架中作为用户DAO使用,是DaoAuthenticationProvider使用的策略。
该接口只需要一个只读方法,这简化了对新的数据访问策略的支持

简单小结

UserDetails自定义用户类必须实现的接口,其中包含了验证时必要的方法

UserDetailsService : 根据用户名获取到用户信息的数据库操作的接口,用于验证时从数据库中取出数据

Debug流程

在这个部分我会对上面的数行代码进行debug,以此来分析执行的流程。为了展示细节及调用层级,大家需要注意我下面代码上标注的以下类型的注释

//跳转 - xxxx

就像以下这样
在这里插入图片描述

上述注释的含义是,该方法需要具体了解,其方法展开对应的位置在小标题为具体的认证的部分。

在这里插入图片描述

UsernamePasswordAuthenticationToken

调用代码

UsernamePasswordAuthenticationToken securityToken =
 new UsernamePasswordAuthenticationToken(userName ,
  password);

首先是构造一个UsernamePasswordAuthenticationToken,存储用户的信息
构造方法内部

	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		//设置权限为null
		super(null);
		//可以看作用户名
		this.principal = principal;
		//可以看作密码
		this.credentials = credentials;
		//设置认证标识为未认证
		setAuthenticated(false);
	}

再往下的认证语句,由ProviderManager提供认证方法的具体内容

authenticate(Authentication authentication)

调用代码
这里就是认证的具体实现了

//SpringSecurity的认证 - 主要是这里
 Authentication authentication = 
 authenticationManager.authenticate(securityToken);

进入方法内部

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	    //获取用于认证的authentication的类 
	    //这里是UsernamePasswordAuthenticationToken
		Class<? extends Authentication> toTest = authentication.getClass();
		//一些认证失败的异常
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		//本级认证的结果
		Authentication result = null;
		//上级认证的结果 - 因为是个过滤链 所以可以进行上级验证
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		//看看有没有与参数中的认证实体
		//这里是UsernamePasswordAuthenticationToken
		//对应的认证提供者-provider
		for (AuthenticationProvider provider : getProviders()) {
			//直到找到再往下执行
			if (!provider.supports(toTest)) {
				continue;
			}
			//日志level的trace是否可用
			if (logger.isTraceEnabled()) {
				//不可用的异常处理
			}
			try {
				//跳转 - 具体的认证
				result = provider.authenticate(authentication);
				if (result != null) {
					//将用户输入的authentication的信息
					//复制到验证成功的result中
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
			//异常的处理
			}
		}
		//本级认证通过 上级认证不为空 
		//则尝试上级认证以此认证信息认证
		if (result == null && this.parent != null) {
			//允许上级认证
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				//异常处理
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		//本级验证通过
		if (result != null) {
			//是否需要清除主体中的数据
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// 到此认证全部完成。
				//从身份验证中删除凭据和其他机密数据
				((CredentialsContainer) result).eraseCredentials();
			}
			//上级验证为空 则表明还未发送验证成功的事件
			//防止重复发送验证成功的事件
			if (parentResult == null) {
				//发送验证成功的事件
				this.eventPublisher.publishAuthenticationSuccess(result);
			}
			//返回认证的Authencation 认证结束
			return result;
		}
		// 上级异常处理
		if (lastException == null) {
			//一些异常的处理
		}
		//关于上级异常的处理
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

具体的验证

调用代码

result = provider.authenticate(authentication);

在这里进行具体的验证

方法内部

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		//用断言进行authentication的检查
		//必须是UsernamePasswordAuthenticationToken
		//才可以继续执行否者抛出异常
		Assert.isInstanceOf(...);
		//从认证对象中获取用户名
		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) {
				//一些异常的抛出
			}
			//确保user不为空
			Assert.notNull(...);
		}
		try {
			//验证前的预检查 确保当前用户是可用状态
			//而不是禁用、过期的、锁定等异常状态
			this.preAuthenticationChecks.check(user);
			//跳转 - 密码的验证
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			if (!cacheWasUsed) {
				throw ex;
			}
			//如果遇到异常就再验证一次
			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);
		}
		//用来返回的用户主体设置为user
		Object principalToReturn = user;
		//主体是否必须为字符串
		if (this.forcePrincipalAsString) {
			//是的话就只设置为用户名
			principalToReturn = user.getUsername();
		}
		//以上述信息创建一个认证成功的Authencation 
		//主要是将关键信息用加密器加密
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
检索用户

检查用户是否存在于数据库中

调用代码

retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);

方法内部

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		//检查密码加密器是否存在
		prepareTimingAttackProtection();
		try {
		    //调用UserDetailsService根据用户名获取到数据库里面用户数据
			UserDetails loadedUser = this.getUserDetailsService()
									.loadUserByUsername(username);
			//为空则抛出异常
			if (loadedUser == null) {
				//异常处理
			}
			//返回对应的用户数据
			return loadedUser;
		}
		//异常的抛出
	}
密码的验证

在这里对从数据库中取出的密码用户前端输入的密码进行对比验证。

调用代码

additionalAuthenticationChecks(user,
 (UsernamePasswordAuthenticationToken) authentication);

方法内部

//userDetails - 数据库里的用户信息 
//authentication-用户输入信息
protected void additionalAuthenticationChecks(
	UserDetails userDetails,
	UsernamePasswordAuthenticationToken authentication) 
throws AuthenticationException {
			//确保用户输入的密码不为空
		if (authentication.getCredentials() == null) {
			//为空则抛出异常并进行日志记录
		}
		//获取用户输入的密码
		String presentedPassword =
		 authentication.getCredentials().toString();
		 //跳转 - 密码的匹配
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			//密码不匹配则抛出异常
		}
	}
密码的匹配

验证密码是否匹配 这里使用了SpringSecurity提供的加密器

调用代码

if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {

上述代码需要注意的是userDetails.getPassword() 取到的数据必须是经过加密器处理的密文,如果数据库内部是明文存储(不建议),那么这里的密码是需要加密处理过的才行。

方法内部

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
    	//用户输入密码不为空
        if (rawPassword == null) {
           //为空则抛出异常
        } else if (encodedPassword != null && encodedPassword.length() != 0) {
         //数据库中取出的 已加密处理的密码不为空且长度不为0 
         //否则抛出异常
                return false;
            } else {
            //用户输入的密码 
            //丛数据库中获取到的、经过加密处理密码都正常,
            //再进行密码验证 这一块我就不再深究了 
            //感兴趣的朋友 可以自行了解
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

流程图演示

认证流程

在这里插入图片描述

具体流程

  • 获取认证的Provider进行具体认证
    在这里插入图片描述
    • 开始具体认证 先检索数据库中是否存在该用户名用户
      在这里插入图片描述

      • 检索用户的内部逻辑
        在这里插入图片描述
    • 检索用户没问题 则检查用户状态是否正常,再进行密码验证
      在这里插入图片描述

      • 密码验证
        在这里插入图片描述
    • 密码正确后 检查请求是否过期、并将用户加入缓存、以及返回认证成功的Authencation
      在这里插入图片描述

  • 认证结果的一些必要处理以及验证成功事件的发送
    在这里插入图片描述

官方文档

具体认识SpringSecuritySpringSecurity官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值