【深入浅出Spring Security(三)】默认登录认证的实现原理

news2024/11/23 0:48:36

Spring Security 默认登录认证的实现原理

  • 一、默认配置登录认证过程
  • 二、流程分析
    • 登录页面的由来
    • 表单登录认证过程(源码分析)
  • 三、UserDetailsService
    • Spring Security 中 UserDetailsService 的实现
    • 默认的 UserDetailsService 配置(源码分析)
    • 默认用户名和密码
  • 四、总结

一、默认配置登录认证过程

请添加图片描述

二、流程分析

由默认的 SecurityFilterChain 为例(即表单登录),向服务器请求 /hello 资源Spring Security 的流程分析如下:
在这里插入图片描述

  1. 请求 /hello 接口,在引入 Spring Security 之后会先经过一系列过滤器(一中请求的是 /test 接口);
  2. 在请求到达 FilterSecurityInterceptor 时,发现请求并未认证。请求被拦截下来,并抛出 AccessDeniedException 异常;
  3. 抛出 AccessDeniedException 的异常会被 ExceptionTranslationFilter 捕获,这个Filter中会去调用 LoginUrlAuthenticationEntryPoint#commence 方法给客户端返回 302(暂时重定向),要求客户端进行重定向到 /login 页面。
  4. 客户端发送 /login 请求;
  5. /login 请求再次当遇到 DefaultLoginPageGeneratingFilter 过滤器时,会返回登录页面。

登录页面的由来

下面是DefaultLoginPageGeneratingFilter 重写的doFilter方法,也可以解释默认配置下为什么会返回登录页,登录页就由下面的过滤器实现而来。

// DefaultLoginPageGeneratingFilter

	@Override
	public void doFilter(ServletRequest request,
	 ServletResponse response, 
	FilterChain chain)
			throws IOException, ServletException {
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

    private void doFilter(HttpServletRequest request, 
    HttpServletResponse response, 
    FilterChain chain) throws IOException, ServletException {
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);
        // 判断是否是登录请求、登录错误和注销确认
        // 不是的话给用户返回登录界面
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
            chain.doFilter(request, response);
        } else {
        // generateLoginPageHtml方法中有对页面登录代码进行了字符串拼接
        // 太长了,这里就不给出来了
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

表单登录认证过程(源码分析)

在重定向到登录页面后,会有个疑问,它是怎么校验的,怎么对用户名和密码进行认证的呢?

首先知道默认加载中是开启了表单认证的,在【深入浅出Spring Security(二)】Spring Security的实现原理 中小编指出了默认加载的过滤器中有一个UsernamePasswordAuthenticationFilter,它是来处理表单请求的,其实它是在调用 HttpSecurity 中的 formLogin 方法配置的过滤器的。

接下来分析一个 UsernamePasswordAuthenticationFilter 干了什么(它不是原生的过滤器,里面是attemptAuthetication进行过滤,而不是doFilter,参数与原生过滤器相比少了个chain):

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, 
	HttpServletResponse response)
			throws AuthenticationException {
			// 首先是判断是否是POST请求
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 获取用户名和密码
		// 这是通过获取表单输入框名为username的数据
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 这是获取表单输入框名为password的数据
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 在一中小编也说了,这是Security中的认证
		// 通过调用AuthenticationManager中的authenticate方法
		// 需要传递的参数的Authentication对象,当时是这样解释的
		return this.getAuthenticationManager()
		.authenticate(authRequest);
	}

在这里插入图片描述

这边经过调试进入到 authenticate 方法观察如何认证的,下面是调试的认证过程:

  1. 进入 authenticate 方法后会调用 ProviderManager 下的 authenticate 方法,它是重写 AuthenticationManager 的,第一次 providers 里只有 AnoymousAuthenticationProvider 对象,用来匿名认证的,最后会判断支不支持此认证,不支持换Provider;
    在这里插入图片描述在这里插入图片描述

  2. 此时匿名认证匹配不了,往下执行,由于parent 属性不为空,所以会调用 parent 的 authenticate 进行认证。(其parent也是一个ProviderManager对象,但其 providers 集合中有且存在 DaoAuthenticationProvider 认证对象)。
    在这里插入图片描述从这可以间接推出在 UsernamePasswordAuthenticationFilter 中的 AuthenticationManager对象 是通过以下构造方法得出来的。
    在这里插入图片描述

  3. 既然 provider.supports 方法匹配成功,那就让provider去验证,然后将验证后的结果集返回。
    在这里插入图片描述DaoAuthenticationProvider 中未重写 AuthenticationProvider 中的 authenticate 方法,由其抽象父类 AbstractUserDetailsAuthenticationProvider 实现的。核心方法通过retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);去获取UserDetails对象,然后结合一些其他参数去创Authentication对象将其返回。

AbstractUserDetailsAuthenticationProvider下的authenticate方法

	@Override
	public Authentication authenticate(Authentication authentication) 
	throws AuthenticationException {
// 断言 authentication 是否是UsernamePasswordAuthenticationToken对象
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// 获取一下用户名
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		// 从缓存中拿UserDetails 对象,显然没有,咱刚调试呢,哪来的缓存
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
		// 既然为空呢,就说明这不是从缓存中拿的,调为false
			cacheWasUsed = false;
			try {
			// 核心代码,获取UserDetails对象去
				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);
			// 这里是验证密码的,通过子类DaoAuthenticationProvider的这个方法对密码去进行验证
			// 传过去的参数是user(UserDetails对象)和authentication对象
			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);
	}
  1. 接下来就是核心方法 retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication) 的概述了,它是 DaoAuthenticationProvider 下的一个方法,用来返回 UserDetails 对象,即用户的详细信息,方便等等封装到认证信息 Authentication 中然后返回结果,判断是否认证成功。
// 一共两个参数,一个是用户名,一个是传过来的认证信息
	@Override
	protected final UserDetails retrieveUser(String username, 
	UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
		// 核心方法就是这个,通过UserDetatilsService中的loadUserByUsername方法去获取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);
		}
	}

我们可以看见默认配置下它是一个 InMemoryUserDetailsManager 对象,是一个基于内存的关于UserDetails 的操作对象。简单看看它里面的loadUserByUsername方法,写的也是非常简单,它这里面用户名不区分大小写
在这里插入图片描述

  1. 再说说密码验证,密码验证在3源码里指出了,在获取UserDetails对象user后,会调用子类的additionalAuthenticationChecks 方法进行密码验证。主要就是和输出框输入的密码和那个UserDetails对象中的密码进行比较,UserDetails 密码可以理解为是通过 PasswordEncoder 编码后的密码(密文),而输入框输入的是可以理解为是明文,可以简单这样先理解。然后通过 PasswordEncoder 去看看是否匹配。默认是 DelegatingPasswordEncoder 密码编码器;
    在这里插入图片描述

三、UserDetailsService

Spring Security 中 UserDetailsService 的实现

在这里插入图片描述

  • UserDetailsManager 在 UserDetailsService 的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在共 5 种方法。
  • JdbcDaoImpl 在 UserDetailsService 的基础上,通过 spring-jdbc 实现了从数据库中查询用户的方法。
  • InMemoryUserDetailsManager 实现了 UserDetailsManager 中关于用户的增删改查方法,不过都是基于内存的操作,数据并没有持久化。
  • JdbcUserDetailsManager 继承自 JdbcDaoImpl 同时又实现了 UserDetailsManager 接口,因此可以通过 JdbcUserDetailsManager 实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过 JdbcUserDetailsManager 有一个局限性,就是操作数据库中用户的 SQL 都是提前写好的,不够灵活,因此在实际开发中 JdbcUserDetailsManager 使用并不多。
  • CachingUserDetailsService 的特点是会将 UserDetailsService 缓存起来。
  • UserDetailsServiceDelegator 则是提供了 UserDetailsService 的懒加载功能。
  • ReactiveUserDetailsServiceAdapter 是 webflux-web-security 模块定义的 UserDetailsService 的实现。

默认的 UserDetailsService 配置(源码分析)

关于UserDetailsService的默认配置在UserDetailsServiceAutoConfiguration自动配置类中。(由于代码很长,这里只提取核心部分)

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },
		type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
				"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
				"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {

	@Bean
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
			// 这里是从SecurityProperties中获取User对象(这里的User对象是SecurityProperties的静态内部类)
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		// 然后创建InMemoryUserDetailsManager对象返回
		// 交给Spring容器管理
		return new InMemoryUserDetailsManager(User.withUsername(user.getName())
			.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
			.roles(StringUtils.toStringArray(roles))
			.build());
	}
	}

观察 UserDetailsServiceAutoConfiguration 上的注解 @ConditionalOnMissingBean ,联想到啥?自动化配置 SecurityFilterChain 遇到过。
上面配置意思的,要想使用默认配置,得先满足容器中不含 AuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolver实例这个条件。

默认用户名和密码

从上面自动化配置 UserDetailsService 中,我们也发现了使用的User对象是从 SecurityProperties 中获取的,那咱看一下是怎么个 User 对象吧。

首先是调用的 getUser 去获取的,而这个user 就一直接 new 的一个User对象,它是一个静态内部类实例。

看下面静态内部类User属性可以看见,其用户名name是"user",而密码则是一个UUID字符串,roles是一个list集合,可以指定多个。
注意:下面的 getter、setter 方法没有截取出来。

那可不可以自己配置用户名和密码呢?
当然是可以滴。
在这里插入图片描述可以看见,SecurityProperties@ConfigurationProperties 注解修饰了(这里得知道SecurityProperties是由Spring容器管理的一个对象)。

而 @ConfigurationProperties 注解是通过 setter 注入的方式,将配置文件配置的值,映射到被该注解修饰的对象中。

所以我们可以在配置文件中进行自己的配置,可以配置自己的用户名和密码。

比如我这么配置:

# application.yml
spring:
  security:
    user:
      name: xxx
      password: 123

用户名、密码就被更改。

请添加图片描述

四、总结

  • AuthenticationManager、ProviderManager、AuthenticationProvider关系。
    在这里插入图片描述
  • 得知道 DaoAuthenticationProvider retrieveUser 方法和 additionalAuthenticationChecks 方法(这俩方法分别应用了UserDetailsService和PasswordEncoder对象)。UsernamePasswordAuthenticationFilter 最后也是去通过 ProviderManager 中的 authenticate 去认证,最后还是调到 DaoAuthenticationProvider 的父类 AbstractUserDetailsAuthenticationProvider 的 authenticate 去认证,我们得清楚这个流程和这些类、方法,方便后期需要以及调试可用
  • 我们可以通过去实现 UserDetailsService 接口(自定义UserDetailsService),然后将实现类实例交给 Spring 容器管理,这样就不会用默认实现了,而是用我们的自定义实现。
  • UserDetails 是用户的详情对象,里面封装了用户名、密码、权限等信息。也是 UserDetailsService 的返回值,这些都是可以自定义的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/589563.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【送书福利-第七期】《分布式中间件核心原理与RocketMQ最佳实践》

大家好&#xff0c;我是洲洲&#xff0c;欢迎关注&#xff0c;一个爱听周杰伦的程序员。关注公众号【程序员洲洲】即可获得10G学习资料、面试笔记、大厂独家学习体系路线等…还可以加入技术交流群欢迎大家在CSDN后台私信我&#xff01; 本文目录 一、前言二、内容介绍三、作者介…

孪生诱捕网络在欺骗防御领域的应用

随着以数字化、网络化和智能化为特征的信息化浪潮的蓬勃兴起&#xff0c;信息已经成为重要的战略资源与重要生产要素&#xff0c;在国家的发展和人们的生产生活中起到至关重要的作用。信息化在给人们带来便利的同时&#xff0c;网络信息安全问题也日益凸显。经过多年的网络安全…

【Linux】Linux 文件系统与设备文件

Ref: 《Linux设备驱动开发详解&#xff1a;基于最新的Linux4.0内核》中的第5章《Linux 文件系统与设备文件》 基于Linux 5.10 本文结合源码和实例分析了Linux 文件系统与设备文件&#xff0c;主要介绍文件系统的构成和发展&#xff0c;以及三种虚拟文件系统debugfs procfs sysf…

《微服务实战》 第二十七章 CAS

前言 本章节介绍CAS 1、CAS的概念 CAS的全称为&#xff1a;CompareAndSwap,直译为对比和交换。 CAS实际是普遍处理器都支持的一条指令&#xff0c;这条指令通过判断当前内存值V、旧的预期值A、即将更新的值B是否相等来对比并设置新值&#xff0c;从而实现变量的原子性。 Syn…

二进制安装Kubernetes(K8s)集群---从零安装教程(带证书)

一、实验环境 1、安装说明 selinux iptables off 官方网站&#xff1a;https://kubernetes.io/zh-cn/docs/home 主机名IP系统版本安装服务master0110.10.10.21rhel7.5nginx、etcd、api-server、scheduler、controller-manager、kubelet、proxymaster0210.10.10.22rhel7.5nginx、…

【细读Spring Boot源码】Spring如何获取一个Bean?BeanFactory的getBean方法

前言 在读refresh(context);时&#xff0c;finishBeanFactoryInitialization(beanFactory);中beanFactory.preInstantiateSingletons();用于实例化剩余所有的non-lazy-init的单例。这里包含了前置处理器和后置处理器 这里面就用到了本文主题getBean(beanName); 下面具体看看这…

4.数据结构期末复习之树

1.树的概念(一对多)(要求会写遍历序列) 1)n>个节点,n0时为空树2)仅有一个根节点3)左右节点互不相交,子节点一对多2.树的特点 1.子树之间没有关系 2.节点不属于多个子树(一个节点只能在一颗子树里面)3.没有回路(就是成环)4.有层次性3.术语 1.节点的度:拥有子树的个数…

ArcGIS教程——ArcGIS快速入门

实例数据&#xff1a;https://pan.baidu.com/s/184wwCmWrJdb-qjxsT614EQ 密码&#xff1a;dowv ArcGIS for Desktop是一套完整的专业GIS应用程序&#xff0c;包含有ArcMap、ArcCatalog、ArcToolbox、ArcScense、ArcGlobe和Model Builder等。其中ArcMap、ArcCatalog、ArcToolbo…

Unreal Niagara粒子入门3 - 根据模型顶点发射粒子

在一些游戏中经常会出现根据模型顶点位置发射粒子的情况&#xff0c;这次就来实现一下该效果&#xff1a; 1.基础创建操作 首先从空粒子发射器开始创建&#xff0c;右键NiagaraSystem->Create empty system&#xff0c;打开粒子系统后再右键Add empty emitter&#xff1a…

【智能软件安全】上海道宁为您带来智能软件安全平台——​Veracode,帮助您全面地保护您构建和管理地应用程序

Veracode可以全面地 保护您构建和管理地应用程序 在现代软件 开发生命周期的 每个阶段不断发现并修复缺陷 Veracode通过 建立一种在安全和开发团队之间 架起桥梁并授权 开发人员成为 安全倡导者的积极文化 从一开始就防止常见的安全漏洞 开发商介绍 Veracode成立于20…

腾讯云8核16G服务器18M带宽CPU性能可支撑多少人同时在线?

腾讯云8核16G轻量服务器CPU性能如何&#xff1f;18M带宽支持多少人在线&#xff1f;轻量应用服务器具有100%CPU性能&#xff0c;18M带宽下载速度2304KB/秒&#xff0c;折合2.25M/s&#xff0c;系统盘为270GB SSD盘&#xff0c;月流量3500GB&#xff0c;折合每天116.6GB流量&…

【CocosCreator入门】CocosCreator组件 | TiledMap(地图)组件

Cocos Creator是一款流行的游戏开发引擎&#xff0c;具有丰富的组件和工具&#xff0c;其中TiledMap组件可以帮助开发者快速创建、加载和渲染地图。 目录 一、组件介绍 二、组件属性 三、脚本控制 3.1加载地图 3.2渲染地图 四、详细说明 五、关闭裁剪 六、节点遮挡 一、…

Linux中与进程间通信相关的内核数据结构

【摘要】本文详细讲述了Linux内核中与进程间通信概念相关的内核数据结构及其内在联系。 九、进程间通信(IPC)相关数据结构 9.1 ipc_namespace 从内核版本2.6.19开始&#xff0c;IPC机制已经能够意识到命名空间的存在&#xff0c;但管理IPC命名空间比较简单&#xff0c;因为它…

VR教育:让教育“可视化”,开启元宇宙教学之路

放眼世界&#xff0c;有不少高等教育学校都已经开始了元宇宙教学之路&#xff0c;为了从根本上解决目前课堂教学中存在的问题&#xff0c;进一步提高课堂教学质量&#xff0c;VR教育就可以很好地完善这些方面。 传统教育并不能让学生很好地沉浸在真实知识环境中&#xff0c;在一…

连锁门店运营管理系统有哪些功能?该如何选购?

连锁门店运营管理过程中&#xff0c;面临诸多难题&#xff0c;比如不同门店分布在不同地区&#xff0c;管理分散&#xff1b;各门店的人员管理、绩效考核、销售数据等工作进行困难&#xff1b;很难保证产品和服务的标准化管控。 连锁店只有不断适应市场变化&#xff0c;趁早选择…

不用运算符的加法运算

一.不用运算符的加法 1.题目描述 设计一个函数把两个数字相加。不得使用 或者其他算术运算符。 力扣:力扣 2.问题分析 1.知识预备 注意:下面所有的结论的进位是考虑二进制的进位,因为我们使用的位运算符,是针对二进制进行的. 结论一:在不考虑进位的情况下&#xff0c;其无…

【group by】mysql分组查询的案例和原理

【group by】mysql分组查询的案例和原理 【一】group by的使用场景【二】group by的基本语法【1】基本语法【2】常用的聚合函数&#xff08;1&#xff09;max函数&#xff1a;取出分组中的最大值&#xff08;2&#xff09;avg函数&#xff1a;取出分组中的平均值&#xff08;3&…

斩获大奖!「智办事绩效」荣获钉钉年度奖项-「含钉量新锐奖」

近日&#xff0c;钉钉发布「含钉量年度奖」榜单&#xff0c;「智办事绩效」凭借与钉钉的深度融合斩获钉钉年度奖项-「含钉量新锐奖」。 ​作为钉钉优质合作伙伴&#xff0c;智办事绩效致力于用数字化、智能化的产品与专业的服务&#xff0c;解决传统绩效管理、人才培养痛点&…

【JOSEF约瑟 JDL-5200A 电流继电器 过负荷或短路启动元件 导轨安装】

名称&#xff1a;电流继电器&#xff1b;品牌&#xff1a;JOSEF约瑟&#xff1b;型号&#xff1a;JDL-5200A触点容量&#xff1a;250V2A&#xff1b;返回时间&#xff1a;≤35ms&#xff1b;整定范围&#xff1a;0.03-19.9A&#xff1b;特点&#xff1a;返回系数高、安装方便。…

前端vue面试题

四、路由 1. Vue-Router 的懒加载如何实现 非懒加载&#xff1a; import List from /components/list.vue const router new VueRouter({routes: [{ path: /list, component: List }] })&#xff08;1&#xff09;方案一(常用)&#xff1a;使用箭头函数import动态加载 co…