SpringSecurity认证流程详解【深入源码】

本文详细阐述了SpringSecurity的认证流程,包括请求拦截、封装对象传递给AuthenticationManager、密码校验以及用户信息加载。重点介绍了additionalAuthenticationChecks和loadUserByUsername方法,以及如何自定义验证逻辑。
摘要由CSDN通过智能技术生成

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

关于SpringSecurity这方面网上的资源不是很多,而SpringSecurity流程比较复杂,参考了部分文章和自己看源码写了一篇笔记


提示:以下是本篇文章正文内容,下面案例可供参考

一、SpringSecurity是什么?

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。简单来说,就是一个使用了大量过滤器的安全认证框架

二、执行流程

大致流程:在这里插入图片描述

1.拦截请求

在认证阶段最重要的一个过滤器是UsernamePasswordAuthenticationFilter,通过他的构造方法可以拦截到所有路径为login,请求方法为post的请求

在这里插入图片描述
而这个方法最终请求到UsernamePasswordAuthenticationFilter的父类AbstractAuthenticationProcessingFilter(抽象类)中的一个方法
在这里插入图片描述
这个方法的底层调用了RequestMatcher进行路劲的对比
在这里插入图片描述

在这里插入图片描述

2.封装对象并传递至AuthenticationManager

路径比对成功后在AbstractAuthenticationProcessingFilter的doFilter方法(过滤器逻辑实现方法)中会调用attemptAuthentication方法,而该方法则是security认证中最重要逻辑
在这里插入图片描述
源码:

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		/**
		obtainUsername和obtainPassword都是在请求中根据参数名(request.getParameter)
		获取到账户和密码
		*/

		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();
		/**
		将账户和密码封装为Authentication对象,里面有两个重要参数
		Principal:相当于账户
		Credential:相当于密码
		**/
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

		return this.getAuthenticationManager().authenticate(authRequest);
	}

经过这个方法后,会将账户和密码封装为一个UsernamePasswordAuthenticationToken对象,最后把这个对象交给AuthenticationManager,并调用他的authenticate进行身份认证

3.加载用户信息及密码校验

经过封装后并传递至AuthenticationManager后,AuthenticationManager会调用他的服务提供者AbstractUserDetailsAuthenticationProvider
在这里插入图片描述
这里有几个重要的方法,主要的逻辑在AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider中实现


  • additionalAuthenticationChecks:进行密码比对
  • authenticate:身份认证主要方法
  • retrieveUser:获取用户

3.1 additionalAuthenticationChecks

additionalAuthenticationChecks执行的是密码比对,他有两个参数

  • UserDetails:数据库中查询出来的用户数据封装而成
  • authentication:前端传过来的参数经过attemptAuthentication封装而成的Authentication对象
    如果想实现自己的密码校验逻辑可以继承并重写这个方法
    源码
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
		//authentication.getCredentials()用于获取前端传来的密码
		String presentedPassword = authentication.getCredentials().toString();
		//使用passwordEncoder中的matches方法进行密码比对,密码在数据库中是加密存储
		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

3.2 retrieveUser

retrieveUser是用于获取数据库中的用户,他的主要执行逻辑在DetailsService中的loadUserByUsername方法中,在一般项目中我们是继承UserDetailsService并重写他的loadUserByUsername方法,将数据库中的User对象封装成security中的UserDetails对象,该对象将用于additionalAuthenticationChecks方法进行密码对比
源码:

	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
		//主要逻辑,获取数据库中的用户并封装为UserDetails
			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) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

3.3 authenticate

以上两个方法的逻辑都是在AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider中实现的,而进行认证的主体部分是在AbstractUserDetailsAuthenticationProvider中的authenticate方法实现
源码解读:

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		//从authentication中获取用户名
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();
		//该表示用于判断是否将用户信息存入了缓存
		boolean cacheWasUsed = true;
		//先从缓存中尝试获取用户信息
		UserDetails user = this.userCache.getUserFromCache(username);
		//缓存中没有用户信息
		if (user == null) {
			cacheWasUsed = false;

			try {
			/**
			使用`AbstractUserDetailsAuthenticationProvider`
			的子类`DaoAuthenticationProvider`中实现的retrieveUser方法加载数据库中的用户
			主要逻辑在改方法中的loadUserByUsername方法
			**/
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}
			//该用户不能为空
			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
		//密码比对前检查用户信息
			preAuthenticationChecks.check(user);
			/**
			使用`AbstractUserDetailsAuthenticationProvider`
			的子类`DaoAuthenticationProvider`中实现的
			additionalAuthenticationChecks方法进行密码比对
			可重写`DaoAuthenticationProvider`中的
			additionalAuthenticationChecks方法实现自己的密码比对逻辑
			**/
			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;
			}
		}
		//对比后检查
		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}
		//对比成功返回该Authentication对象
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

4.认证成功后的操作

至此认证成功,AbstractAuthenticationProcessingFilter中的attemptAuthentication(此方法的逻辑在AbstractAuthenticationProcessingFilter的子类UsernamePasswordAuthenticationFilte中实现)执行完毕
在这里插入图片描述
校验成功后调用AbstractAuthenticationProcessingFilter中successfulAuthentication方法,此方法的作用是将用户信息存入security的上下文中(基于ThreadLoacl实现)方便后续调用
在这里插入图片描述successfulAuthentication方法解读:

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

三、总结

在security中最重要的方法莫过于DaoAuthenticationProvider中的additionalAuthenticationChecks方法(密码比对)和UserDetailsService中的loadUserByUsername方法(加载用户信息),要想完成自己的逻辑只需重写这些比较重要的方法即可

  • 23
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值