循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?

想必很多人接触了spring security 后都想知道,登录流程是怎么样的?

今天悬弧带大家扒了登录流程源码的外衣,透过光溜溜的源码分析spring security的登录流程

回顾

我们在《循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调》探索了自定义登录页面时,发现提交的默认登录用户名和密码参数是被写死在代码类UsernamePasswordAuthenticationFilter中的,也可以通过代码配置修改。既然登录提交的用户名和密码在UsernamePasswordAuthenticationFilter类中定义的,那对应的登录验证,想必也是在这里面验证的了

探索登录流程

探索UsernamePasswordAuthenticationFilter 的attemptAuthentication 验证方法

UsernamePasswordAuthenticationFilter类是一个过滤器,代码不多,不难发现,attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 就是验证登录的接口,在《循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调》的项目基础上,打个断点,走一遍登录流程,发现果然和猜想想没错
在这里插入图片描述
不难发现

  • 首先验证登录接口是否是POST请求,如果不是,直接抛异常,当然,这个是默认的,如果希望支持get请求,需要重写过滤器UsernamePasswordAuthenticationFilter,将postOnly属性设置为FALSE
  • 通过 obtainUsername 和 obtainPassword 方法提取出请求里边的用户名/密码出来,提取方式就是 request.getParameter ,这也是为什么 Spring Security 中默认的表单登录要通过 key/value 的形式传递参数,而不能传递 JSON 参数,如果像传递 JSON 参数,修改这里的逻辑
    在这里插入图片描述
  • 使用登录提交的用户名和密码,构造UsernamePasswordAuthenticationToken 对象,构造的对象isAuthenticated() 将返回 false.
    在这里插入图片描述
  • 设置客户端信息,如客户端IP,sessionId等
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

  • 通过this.getAuthenticationManager().authenticate(authRequest) 验证用户登录,验证用户名,密码,用户状态等,如果都OK则登录成功,否则登录失败,具体过程往下看

探索AuthenticationManager#authenticate(authRequest) 验证过程

在前面的 attemptAuthentication 方法中,该方法的最后一步开始做验证了,首先要获取到一个 AuthenticationManager,这里拿到的是 ProviderManager ,所以接下来我们就进入到 ProviderManager 的 authenticate 方法中,这个方法比较长,我把关键的贴出来分析

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
		Class<? extends Authentication> toTest = authentication.getClass();
		for (AuthenticationProvider provider : getProviders()) {
    
    
			if (!provider.supports(toTest)) {
    
    
				continue;
			}
			try {
    
    
				result = provider.authenticate(authentication);
				if (result != null) {
    
    
					copyDetails(authentication, result);
					break;
				}
			}
		}
		if (result == null && this.parent != null) {
    
    
			try {
    
    
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
    
    }
			catch (AuthenticationException ex) {
    
    
				parentException = ex;
				lastException = ex;
			}
		}
		...
		throw lastException;
	}

几乎所有的认证逻辑都在这里了
分析下:

  • 首先获取 authentication 的 Class,判断当前 provider 是否支持该 authentication。如果支持,则调用 provider 的 authenticate 方法开始做校验,校验完成后,会返回一个新的 Authentication。一会来和大家捋这个方法的具体逻辑。
  • 这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验。
  • copyDetails 方法则用来把旧的 Token 的 details 属性拷贝到新的 Token 中来。
  • 接下来会调用 eraseCredentials 方法擦除凭证信息,也就是你的密码,这个擦除方法比较简单,就是将 Token 中的 credentials 属性置空。
  • 最后通过 publishAuthenticationSuccess 方法将登录成功的事件广播出去。

大致的流程,就是上面这样,在 for 循环中,第一次拿到的 provider 是一个 AnonymousAuthenticationProvider,这个 provider 不支持 UsernamePasswordAuthenticationToken,也就是会直接在 provider.supports 方法中返回 false,结束 for 循环,然后会进入到下一个 if 中,直接调用 parent 的 authenticate 方法进行校验。而 parent 就是 ProviderManager,所以会再次回到这个 authenticate 方法中。再次回到 authenticate 方法中,provider 也变成了 DaoAuthenticationProvider,这个 provider 是支持 UsernamePasswordAuthenticationToken 的,所以会顺利进入到该类的 authenticate 方法去执行,而 DaoAuthenticationProvider 继承自 AbstractUserDetailsAuthenticationProvider 并且没有重写 authenticate 方法,所以 我们最终来到 AbstractUserDetailsAuthenticationProvider#authenticate 方法中:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    
		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"));
			}
		}
		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);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
    
    
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

这里的逻辑就比较简单了:

  • 首先从 Authentication 提取出登录用户名。

  • 然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象
    在这里插入图片描述

  • 接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。

  • additionalAuthenticationChecks 方法则是做密码比对的,好多小伙伴好奇 Spring Security 的密码加密之后,是如何进行比较的,看这里就懂了。
    在这里插入图片描述

  • 在 postAuthenticationChecks.check 方法中检查密码是否过期。
    在这里插入图片描述

  • 有一个 forcePrincipalAsString 属性,这个是是否强制将 Authentication 中的 principal 属性设置为字符串,默认为 false,一般不用改,就用 false,这样在后期获取当前用户信息的时候反而方便很多。

  • 最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken。
    好了,那么登录的校验流程就算完成了
    在这里插入图片描述

用户登录后的信息是如何保存的?

要去找登录的用户信息,得先来解决一个问题,就是上面我们说了这么多UsernamePasswordAuthenticationFilter #attemptAuthentication 登录验证过程,但attemptAuthentication 是在哪里触发的?

其实不用多想,登录验证肯定是通过拦截器或者说过滤器触发的,UsernamePasswordAuthenticationFilter 本来就是一个过滤器,但其上面还有父类 AbstractAuthenticationProcessingFilter ,很多时候当我们想要在 Spring Security 自定义一个登录验证码或者将登录参数改为 JSON 的时候,我们都需自定义过滤器继承自 AbstractAuthenticationProcessingFilter ,毫无疑问,UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 类的 doFilter 方法中被触发的:
在这里插入图片描述
从上面的代码中,当 attemptAuthentication 方法被调用时,实际上就是触发了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,当登录抛出异常的时候,unsuccessfulAuthentication 方法会被调用,而当登录成功的时候,successfulAuthentication 方法则会被调用,那我们就来看一看 successfulAuthentication 方法:

在这里插入图片描述
可以看到很重要的一行代码,就是 SecurityContextHolder.getContext().setAuthentication(authResult); ,登录成功的用户信息被保存在这里,也就是说,在任何地方,如果我们想获取用户登录信息,都可以从 SecurityContextHolder.getContext() 中获取到,想修改,也可以在这里修改。

最后大家还看到有一个 successHandler.onAuthenticationSuccess,这就是我们在 SecurityConfig 中配置登录成功回调方法,就是在这里被触发的

分析了一遍源码,发现也就那样

猜你喜欢

转载自blog.csdn.net/huangxuanheng/article/details/119082778