SpringSecurity登录认证流程

2023-05-16

SpringSecurity登录认证流程

目录

SpringSecurity简介

springSecurity登录认证流程

一、Springsecurity简介

​ Spring Security是一个灵活和强大的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全,其中就是两方面的功能:认证和授权,认证意思是你是谁,授权意思是你能干什么。核心是通过一组过滤器,里面的每个过滤器去执行一种认证方式。

二、登录认证流程

​ 我们知道,登录的整体流程是:用户点击登录从前端发起请求,后端接受前端发送来的用户名和密码,然后去数据库里面查询是否存在此用户,如果存在,就放行,让用户进入系统,如果不存在用户或者密码错误,则提示用户信息错误。那么在springsecurity中,也是大致遵循这个思路,只不过做了很多校验。下面就让我们一一解读一下。
在这里插入图片描述

​ 获取到请求里传递来的用户名/密码之后,接下来就构造一个 UsernamePasswordAuthenticationToken 对象,传入 username 和 password,username 对应了 UsernamePasswordAuthenticationToken 中的 principal 属性,而 password 则对应了它的 credentials 属性。见下图

在这里插入图片描述

​ 我们获得UsernamePasswordAuthenticationToken 对象后,会去执行authenticationManager的authenticae方法。当我们点authenticationManager会看到,它其实是一个接口,里面就只有一个authenticae空方法

在这里插入图片描述

​ 那我们找到这个接口的实现类ProviderManager,里面有authenticae的实现方法,

在这里插入图片描述
在这里插入图片描述

  1. 首先获取到传进来的 authentication 的 Class,判断当前 provider 是否support该 authentication(标记1处)。

  2. 如果支持,则调用 provider 的 authenticate 方法(标记2处)开始做校验,校验完成后,会返回一个新的 Authentication。

  3. 这里的 provider 可能有多个,如果 provider 的 authenticate 方法没能正常返回一个 Authentication,则调用 provider 的 parent 的 authenticate 方法继续校验(标记3处)。

    最主要的流程,就是这样,在 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 {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// 从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 {
                //去数据库里面查有没有此用户名的用户,如果有,就返回此用户
				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);
            //检查用户密码对比是否正确
			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();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

上述代码就是去校验登录用户的各个状态。

  1. 首先从 Authentication 提取出登录用户名。
  2. 然后通过拿着 username 去调用 retrieveUser 方法去获取当前用户对象,这一步会调用我们自己在登录时候的写的 loadUserByUsername 方法,所以这里返回的 user 其实就是你的登录对象
  3. 接下来调用 preAuthenticationChecks.check 方法去检验 user 中的各个账户状态属性是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。
  4. additionalAuthenticationChecks 方法则是做密码比对的
  5. 最后在 postAuthenticationChecks.check 方法中检查密码是否过期。
  6. 最后,通过 createSuccessAuthentication 方法构建一个新的 UsernamePasswordAuthenticationToken。

​ 下面我们依次展开来说,

  1. 首先是retrieveUser函数,这个函数的实现在DaoAuthenticationProvider类里面,因为这个类继承了AbstractUserDetailsAuthenticationProvider类,故retrieveUser函数会去数据库里面查找有没有用户名为username的用户,DaoAuthenticationProvider里面的retrieveUser实现代码如下:
protected final UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
   prepareTimingAttackProtection();
   try {
       //this.getUserDetailsService()读取了框架中的UserDetailsService(这是个接口),进而去UserDetailsService里面执行loadUserByUsername方法,然而我们通常会写xxDetailsService来实现UserDetailsService接口,并注入到框架中,所以会去执行我们自己写的xxDetailsService里面的loadUserByUsername方法
      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);
   }
}

​ this.getUserDetailsService()读取了框架中的UserDetailsService(这是个接口),然后去UserDetailsService里面执行loadUserByUsername方法,然而我们通常会写xxDetailsService来实现UserDetailsService接口,并注入到框架中,所以会去执行我们自己写的xxDetailsService里面的loadUserByUsername方法。下面是我的接口实现类

public class UserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private UserService userService;

//    @Autowired
//    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        User user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        return new LoginUser(user);
    }

//    public UserDetails createLoginUser(SysUuser user)
//    {
//        return new LoginUser(user, permissionService.getMenuPermission(user));
//    }
}

​ 主要是userService.selectUserByUserName(username)方法,会去调用相应的service层和mapper层查询数据库,这里不再讲述。如果存在用户就重新new一个LoginUser,这个LoginUser是UserDetails的实现类,故可以直接返回。

2.preAuthenticationChecks.check()函数,这个函数会去检查我们账户的状态信息,例如账户是否被禁用、账户是否被锁定、账户是否过期,具体代码如下:

public void check(UserDetails user) {
    //检查账户是否锁定
   if (!user.isAccountNonLocked()) {
      throw new LockedException(messages.getMessage(
            "AccountStatusUserDetailsChecker.locked", "User account is locked"));
   }
	//检查账户是否启用
   if (!user.isEnabled()) {
      throw new DisabledException(messages.getMessage(
            "AccountStatusUserDetailsChecker.disabled", "User is disabled"));
   }
	//检查账户是否过期
   if (!user.isAccountNonExpired()) {
      throw new AccountExpiredException(
            messages.getMessage("AccountStatusUserDetailsChecker.expired",
                  "User account has expired"));
   }
   if (!user.isCredentialsNonExpired()) {
      throw new CredentialsExpiredException(messages.getMessage(
            "AccountStatusUserDetailsChecker.credentialsExpired",
            "User credentials have expired"));
   }
}

默认情况下,在我的UserDetails接口实现类LoginUser里面,重写了这些属性值,默认返回true就好。
在这里插入图片描述

3.additionalAuthenticationChecks()函数,这个函数会去匹配密码,当然是拿登录用户传进来的明文密码去匹配数据库里面的加密后的密码,具体代码如下

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"));
   }
	//获得登录用户的明文密码
   String presentedPassword = authentication.getCredentials().toString();
	//将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配
   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"));
   }
}

首先获得登录用户的明文密码 ,然后将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配。匹配函数实现不做讲述。

最终通过createSuccessAuthentication(principalToReturn, authentication, user)方法,返回一个authentication,在这个方法里会去重新创建一个UsernamePasswordAuthenticationToken,将已认证状态标志注明。

在这里插入图片描述

将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配。匹配函数实现不做讲述。

最终通过createSuccessAuthentication(principalToReturn, authentication, user)方法,返回一个authentication,在这个方法里会去重新创建一个UsernamePasswordAuthenticationToken,将已认证状态标志注明。
在这里插入图片描述

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringSecurity登录认证流程 的相关文章

  • vector介绍和基本使用

    文章目录 一 vector介绍二 vector使用 1 constructor 2 iterator 3 capacity 4 Element access 5 Modifiers 三 vector迭代器失效问题 一 vector介绍 ve
  • PELCO(派尔高)协议解析及下载(转载)

    PELCO xff08 派尔高 xff09 协议解析及下载 沈雪瑜 在IBMS接口开发 中 xff0c 我们需要用到一些常用的协议 xff0c 而PELCO 派尔高 的监控器材在我国有很广泛的应用 PELCO有自己的传输控制协议 xff0c
  • STM32 HAL库 STM3…

    原文地址 xff1a STM32 HAL库 STM32CUBEMX KEIL TIM1 PWM 四路输出可调 一 作者 xff1a 用户2797410335 硬件 xff1a TM32F407VET6 8M晶振 xff0c JLINK JT
  • 海康设备网络SDK开发NET_DVR_GetDeviceConfig

    由于官方的例子中没有关于NET DVR GetDeviceConfig的示例 xff0c 在此记录一下 NET DVR GET FIELD DETECTION 获取区域入侵侦测配置 xff0c 避免其他小伙伴踩坑 这里只记录主要代码 xff
  • geoserver热图

    1 参考 GeoServer发布Heatmap wenglabs 博客园 Rendering Transformations GeoServer 2 21 x User Manual 2 下载 GeoServer 及wps插件 xff0c
  • arcgis的lyr样式转qgis的sld样式

    需求 xff1a arcgis样式lyr要发布到geoserver 先说最终技术路线 xff1a qgis安装slyr插件 xff0c lyr转成xml xff0c 再通过xml配图后导出sld 避免的操作 xff1a lyr直接导出sld
  • httpclient海康ISAPI透传

    可以使用海康SDK调用NET DVR STDXMLConfig进行透传 xff0c 但是这种方式仍然比较麻烦 SDK的透传其实就是http的包装 xff0c 可以完全撇开海康SDK xff0c 也就是通过http的方式获取或者设置 xff0
  • openlayers文字随线的方向

    在不使用ol ext时需要自己计算方向 效果 xff1a 核心代码 xff1a function styleArrow start end title var arrowLonLat 61 end 0 43 start 0 2 end 1
  • openlayer点沿线动画

    Marker Animation 核心代码 xff1a 开始动画 let lastTime 61 Date now let distance 61 0 function moveFeature event const speed 61 10
  • ol-ext沿线动画

    参考示例 ol ext Openlayers feature animation 核心代码 xff1a 核心代码 var anim controler function animateFeature if routeFeature anim
  • test

    package org my cameratest import java io File import java io FileOutputStream import java io IOException import org kobj
  • C#操作Excel做Chart并输出成图

    lt summary gt 创建Chart xff0c 并设置相关属性 最后按照固定路径输出成gif图 lt summary gt lt param name 61 34 saveDocPath 34 gt 保存图片路径 lt param
  • Arcgis分级时出现Too many unique values (> 65536).

    只需要把默认值改的更大一点就行了 解决方法 xff1a 在默认值后面多添加几个0 xff0c 嘿嘿 xff0c 这样就行了
  • LCD与LED液晶显示屏的区别

    什么是LCD LCD是液晶显示屏Liquid Crystal Display的全称 xff0c 主要有TFT UFB TFD STN等几种类型的液晶显示屏无法定位程序输入点于动态链接库上 笔记本液晶屏常用的是TFT TFT xff08 Th
  • arcgis中连接excel时出错

    在arcmap中直接右键jion就可以关联excel xff0c 但是这次想使用小工具组合成ModelBuilder xff0c 所以先使用Copy Rows工具 xff0c 但是总是出错 xff0c 结果是一个字符引起的错误 箭头所指的方
  • 招行闪电贷“您的额度已被暂停”

    打电话给招行 xff0c 招行解释说名下两笔贷款都违规了 一笔是借贷还旧贷 xff0c 一笔是转账记录的备注写了还某某某首付 需要专款专用 xff0c 的确都违规了 招行也说只能先还完当前所有贷款 xff0c 才能尝试恢复额度 xff0c
  • su 鉴定故障解决办法(转)

    su su root命令输入密码后出现鉴定故障错误 这是因为在安装linux系统时没有给root用户设置密码 xff0c 重新设置密码即可 1 设置root密码 sudo passwd root gt 如果没有登录密码 则提示输入新密码 4
  • 配置apache2.4+PHP8.0(转)

    转自 xff1a 配置apache2 4 43 PHP8 0 chicboy2 博客园 cnblogs com xff08 1 xff09 下载apache2 4 按照电脑版本下载压缩包 xff08 2 xff09 下载后解压缩到需要安装的
  • error: cast from pointer to smaller type ‘unsigned int‘ loses information

    在使用clang编译一处代码时报标题中的错误 原因 xff1a 64为机器上因为int为4字节 xff0c 指针统统为8字节 xff0c int无法容纳一个指针的值 解决 xff1a 将 unsigned int 强制类型转换修改成 xff
  • 0xC0000139: Entry Point Not Found

    MODULE hMod 61 LoadLibraryExA 34 D test dll 34 NULL LOAD WITH ALTERED SEARCH PATH if hMod 61 61 NULL MessageBoxA NULL 34

随机推荐