超详解(源码)SpringSecurity的认证过程!!


最近被SpringSecurity搞傻了,不服气的我决定深入理解

它的整个执行流程需要从,身份认证开始,

纵观整个SpringSecurity,身份认证要从一个叫AbstractAuthenticationProcessingFilter的抽象类说起,好戏开场了。。。。。。。🐤🐤🐤🐤🐤🐤🐤🐤🐤🐤

AbstractAuthenticationProcessingFilter

基于浏览器http的身份验证请求抽象处理器,里面规定了身份认证的过程!

UsernamePasswordAuthenticationFilter是继承了AbstractAuthenticationProcessingFilter

image-20211011075328253

所以要说UsernamePasswordAuthenticationFilter,它两个是分不开的。

1.UsernamePasswordAuthenticationFilter的创建

可以看到UsernamePasswordAuthenticationFilter有两个构造器:

image-20211011075603972

它默认使用的是哪一个呢?

我在AbstractAuthenticationProcessingFilter的构造器上打上了断点:(springboot启动初始化时就会创建这些过滤器)

image-20211011075916511

可以看到执行的是第一个构造,一个参数的:

image-20211011080156655

🏳️‍🌈🏳️‍🌈🏳️‍🌈🏳️‍🌈🏳️‍🌈🏳️‍🌈🏳️‍🌈🏳️‍🌈小结一下:

UsernamePasswordAuthenticationFilter,spring容器初始化时就会创建,并且默认使用的是请求匹配器为"/login"(即Spring Security的登录请求),"post"方式的构造器

2.attemptAuthentication()方法

这个方法是执行真正的身份认证的,在AbstractAuthenticationProcessingFilter中定义,子类必须实现这个方法!

public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException;

如果验证成功,应该返回一个验证用户的令牌;否则返回null并抛出AuthenticationException异常

让我们看看UsernamePasswordAuthenticationFilter中是如何实现的:

image-20211011082613971

首先,它验证了请求方法是不是post,(这个postOnly是它设置的一个变量,值是true),不是post就会抛出异常-------认证的方法不被支持

String username = obtainUsername(request); //从请求中拿到用户名

obtainUsername()如下😱,还是看看吧:

image-20211011083117155

可以看到在对用户名密码判断空后,就用用户输入的密码用户名生成要认证的token了

image-20211011083917756

下面我们就去看看这个UsernamePasswordAuthenticationToken是什么妖魔:

它有两个构造器:

image-20211011084148743

image-20211011084305342

可以注意到我们传入的username在security中叫principal

password在security中叫credentials;这是有必要知道的!

还有就是Authentication:它是接口,UsernamePasswordAuthenticationToken就是其实现类之一

第一个构造器,在代码中可以任意使用,因为它被设置为是不可信的

第二个构造函数供AuthenticationManager或者AuthenticationProvider的实现类使用,传入实现类就能生成可信的token

但是我们上面的代码不是调用的第一个构造函数吗?那应该生成的token是不可信的啊?别急,经过我不懈的追,终于找到了答案,它会在一系列判断操作之后,认为你身份没问题时,会重新调一次,而这次使用的就是第二个构造。

不要试图使用UsernamePasswordAuthenticationToken的setAuthenticated方法将它设置为true

image-20211011105237470

可以看到其父类(AbstractAuthenticationToken)的setAuthenticated方法才是真正设置可不可信的,而第一个构造方法使用的是UsernamePasswordAuthenticationToken重写过后的setAuthenticated,本质是调用super.setAuthenticated(false);

整个认证过程梳理:

1. 一号选手UsernamePasswordAuthenticationFilter

登录请求首先会来到这个过滤器,既然是过滤器,当然是执行 doFilter方法嘛。因为其父类AbstractAuthenticationProcessingFilter已经定义好了doFilter方法,只能看父类:(关键处我都打了断点)

先看部分:

image-20211011111943413

首先要执行的就是requiresAuthentication(request, response)方法,它有什么作用呢?点开看看

image-20211011112300836

原来就是匹配你是不是我要的登录请求,前面我们说过,UsernamePasswordAuthenticationFilter默认使用的构造器就是把一个请求匹配器(姑且这么叫吧)😆,只匹配"/login"的post请求

匹配完成之后,就该执行attemptAuthentication(request, response)方法了

image-20211011112834258

前面我们说过,这个方法必须被子类实现,它的作用就是完成身份验证,点进去看看:

image-20211011123309958

getAuthenticationManager()是获取AuthenticationManager的实现类ProviderManager

2.二号选手ProviderManager

authenticate(authRequest)方法就是验证身份的入口方法,需要将生成的验证令牌传进去,一系列验证之后,如果验证成功会把令牌中的Authenticated属性设置为true,前面我们说过,它被设置为false。

最后那行代码就很简单了:::执行ProviderManager的authenticate方法,点进去看看:(代码有点长)

@Override
	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;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}
			// If the parent AuthenticationManager was attempted and successful then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then it will
		// publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
		// parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}
		throw lastException;
	}

没事,看我这,

image-20211011130141201

首先,我们看看providers是个啥

image-20211011130304806

是一个元素类型为AuthenticationProvider的列表,并且默认是个空列表,是在实例化ProviderManager是给其赋值的,

接下来我们再看看AuthenticationProvider(顾名思义,验证的提供者),它是一个接口,下面是它的实现类:

image-20211011130652796

下面这行代码就是在匹配那个验证提供类可以验证toTest:

image-20211011131157534

上上上张图,toTest已经标注了,就是传进来的令牌类型,这里是UsernamePassworduthenticationToken,

好了,我们还需要弄明白一个东西,supports方法是干嘛的?

我就直接说了吧,supports方法是用来判断该对象表示的类或接口是否与指定的类参数表示的类或接口相同,或者是其超类或超接口(一个叫isAssignableFrom的本地方法判断的)。如果是,则返回true;否则返回false

所以这个循环的作用就很明显了,匹配到能够验证令牌的验证提供类:

其原文翻译如下:

尝试对传递的身份验证对象进行身份验证。

将连续尝试AuthenticationProvider列表,直到AuthenticationProvider指示它能够验证传递的身份验证对象的类型。然后将尝试使用该AuthenticationProvider进行身份验证。

如果多个AuthenticationProvider支持传递的身份验证对象,则第一个能够成功身份验证该身份验证对象的身份验证提供程序将确定结果,并覆盖早期支持AuthenticationProviders引发的任何可能的AuthenticationException。成功验证后,将不会尝试后续的AuthenticationProviders。如果任何支持AuthenticationProvider的身份验证均未成功,则最后引发的AuthenticationException将被重试。

经过循环得到:DaoAuthenticationProvider能够验证UsernamePassworduthenticationToken类型的验证信息token,

3.三号选手DaoAuthenticationProvider(主力部队)

说明: >>>不同的自定义配置会可能会使用不同的验证提供类,用户名密码验证是使用的DaoAuthenticationProvider,如果你使用的是其它验证方式请应变一下。。。

DaoAuthenticationProvider会做很多的验证,可以说它是真正的验证执行者!

下面就会进入到DaoAuthenticationProvider的authenticate方法(这里就是真正的验证了),由于这个方法在父类AbstractUserDetailsAuthenticationProvider中已被实现,它只是继承,所以我们就在父类中查看:

@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);
	}

这里我们就略看,重点:

image-20211011142549325

点开retrieveUser:

image-20211011143214474

image-20211011143242746

到这里用户就已经查到了,接下来就是检查:

image-20211011143443205

顾名思义:额外的检查,这个类用户可自定义,其代码如下:

image-20211011194900612

注意:passwordEncoder,是在创建DaoAuthenticationProvider时设置的,其默认使用的是BCryptPasswordEncoder(就不贴源码了,有兴趣的同学可以追一追),这是可以自定义的配置类中配置即可,比如我这里是使用的这个NoOpPasswordEncoder,是直接比较字符串:如下图

image-20211011195859588

接着往下走,这两个检查对应:

image-20211011143709328

image-20211011143813800

先看看DefaultPreAuthenticationChecks类要验证什么内容:

image-20211011144110391

再看看DefaultPostAuthenticationChecks

image-20211011144251707

如果验证都通过了会把这个用户的验证信息保存到缓存

image-20211011144526624

最后,执行身份验证成功的方法:

image-20211011144611373

image-20211011191441575

这个令牌构造器,是会将Authenticated设置为true的

再往下其实也没什么了:

image-20211011201849141

后续成功或者失败的操作好像大都和配置相关了,比如remberMe啊、成功处理啊、包括上图的sessionStrategy,大家有兴趣可以自行研究一下,在此不做深追了👵头发没了都

image-20211011202211096

4.四号选手SecurityContextPersistenceFilter

差点忘了。。。。

其主要代码:

image-20211011205732710

其大概意思,在我的理解:先从security上下文持有者(SecurityContextHolder)那里拿到内容,然后清空持有者的内容,并将取出的内容保存到security上下文仓库中(SecurityContextRepository)。

在请求之前从配置的SecurityContextRepository获得的信息填充SecurityContextHolder,并在请求完成并清除上下文持有者后将其存储回存储库。(官方解释)

总结:我画了个图,希望能帮助理解

image-20211011221014047

这是比较粗略的(很多比较重要的类都没有写到),下面我把这个过程中主要的调用关系以图的形式展示出来:

image-20211011223208803

因为好几个重要的方法都是在其父类就写好了的,比如AbstractAuthenticationProcessingFilter里就定义了整个认证的流程,它是认证的总管!!

image-20211011223541107

,子类只是继承有,并且父类中是只读的,大概是安全起见。

这样调试就比较麻烦了,跳来跳去的。。。。。。。。。。。

好了,以上就是SpringSecurity的认证过程详解了,希望对你有帮助。

作者对其理解不是很深,也是气不过没学懂它才写下这篇博客。难免会出现错误,希望您能指正!!!🌂🌂🌂🌂🌂🌂🌂🌂🌂🌂🌂🌂🌂,终于写完了。

  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Spring Security是一个在Java应用程序中提供身份验证和授权的框架。它通过使用各种身份验证和授权技术,帮助开发人员实现应用程序的安全性。 在Spring Security中,身份验证包括验证用户的身份以确保其是合法的,并且授权包括确定用户是否有权访问特定功能或资源。以下是Spring Security中的一些关键概念和用法: 1. 身份验证:Spring Security提供了许多身份验证机制,例如基于表单的身份验证、基于HTTP基本身份验证、基于LDAP的身份验证等。开发人员可以选择适合他们应用程序需求的身份验证机制。 2. 授权:Spring Security使用许可(Permission)和角色(Role)的概念来控制访问权限。可以使用特定的注解或编程方式将这些权限和角色应用到方法或URL上。 3. 认证和授权流程:Spring Security认证和授权过程中使用了一系列的过滤器和提供者。它们分别负责处理身份验证和授权的不同方面。开发人员可以根据需要定制这些组件来满足自己的应用程序需求。 4. AccessDecisionManager:这是Spring Security中的一个重要组件,用于决定用户是否有权限访问特定的资源或功能。开发人员可以实现自己的AccessDecisionManager来根据自己的逻辑进行权限决策。 5. UserDetails:在Spring Security中,用户信息通过UserDetails接口进行封装。开发人员可以根据自己的需求实现自定义的UserDetails接口,并提供用户的身份验证和授权信息。 6. 匿名认证Spring Security支持为匿名用户建立一个匿名Authentication对象。这样,无需再对匿名用户进行额外的验证,可以直接将其当作正常的Authentication对象来使用。 综上所述,Spring Security提供了全面的身份验证和授权机制来保护应用程序的安全性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

为了我的架构师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值