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

这篇具有很好参考价值的文章主要介绍了【深入浅出Spring Security(三)】默认登录认证的实现原理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

二、流程分析

由默认的 SecurityFilterChain 为例(即表单登录),向服务器请求 /hello 资源Spring Security 的流程分析如下:
【深入浅出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);
	}

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

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

  1. 进入 authenticate 方法后会调用 ProviderManager 下的 authenticate 方法,它是重写 AuthenticationManager 的,第一次 providers 里只有 AnoymousAuthenticationProvider 对象,用来匿名认证的,最后会判断支不支持此认证,不支持换Provider;
    【深入浅出Spring Security(三)】默认登录认证的实现原理【深入浅出Spring Security(三)】默认登录认证的实现原理

  2. 此时匿名认证匹配不了,往下执行,由于parent 属性不为空,所以会调用 parent 的 authenticate 进行认证。(其parent也是一个ProviderManager对象,但其 providers 集合中有且存在 DaoAuthenticationProvider 认证对象)。
    【深入浅出Spring Security(三)】默认登录认证的实现原理从这可以间接推出在 UsernamePasswordAuthenticationFilter 中的 AuthenticationManager对象 是通过以下构造方法得出来的。
    【深入浅出Spring Security(三)】默认登录认证的实现原理

  3. 既然 provider.supports 方法匹配成功,那就让provider去验证,然后将验证后的结果集返回。
    【深入浅出Spring Security(三)】默认登录认证的实现原理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 的操作对象。【深入浅出Spring Security(三)】默认登录认证的实现原理简单看看它里面的loadUserByUsername方法,写的也是非常简单,它这里面用户名不区分大小写
【深入浅出Spring Security(三)】默认登录认证的实现原理

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

三、UserDetailsService

Spring Security 中 UserDetailsService 的实现

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

  • 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对象,它是一个静态内部类实例。
【深入浅出Spring Security(三)】默认登录认证的实现原理
看下面静态内部类User属性可以看见,其用户名name是"user",而密码则是一个UUID字符串,roles是一个list集合,可以指定多个。
【深入浅出Spring Security(三)】默认登录认证的实现原理注意:下面的 getter、setter 方法没有截取出来。

那可不可以自己配置用户名和密码呢?
当然是可以滴。
【深入浅出Spring Security(三)】默认登录认证的实现原理可以看见,SecurityProperties@ConfigurationProperties 注解修饰了(这里得知道SecurityProperties是由Spring容器管理的一个对象)。

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

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

比如我这么配置:

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

用户名、密码就被更改。

【深入浅出Spring Security(三)】默认登录认证的实现原理文章来源地址https://www.toymoban.com/news/detail-484753.html

四、总结

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

到了这里,关于【深入浅出Spring Security(三)】默认登录认证的实现原理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【深入浅出Spring Security(一)】Spring Security的整体架构

    这篇博客所述主要是在读《 深入浅出Spring Security 》途中所做的笔记(之前有学Spring Security,但了解的比较浅,所以想着看这本书深入一点点,这都是因为上次一个bug调了我几天) 这本书的 pdf 网盘链接可通过微信扫下方公众号私信\\\"深入浅出Spring Security\\\"即可获取。 在 Spring

    2024年02月06日
    浏览(88)
  • 【深入浅出 Spring Security(七)】RememberMe的实现原理详讲

    先看看最简单用法的默认页面效果变化。 SecurityConfig 配置类 测试 TestController 代码 以下是给出的默认的登录页面。 观察页面源代码可以发现,比原先没配置 RememberMe 之前多了个 name 为 remember-me 的 checkbox 选项。 如果我们勾选了它并且登录成功后,当我们关闭掉当前浏览器,

    2024年02月09日
    浏览(38)
  • 【深入浅出 Spring Security(十一)】授权原理分析和持久化URL权限管理

    在 【深入浅出Spring Security(一)】Spring Security的整体架构 中小编解释过授权所用的三大组件,在此再解释说明一下(三大组件具体指:ConfigAttribute、AccessDecisionManager(决策管理器)、AccessDecisionVoter(决策投票器)) ConfigAttribute 在 Spring Security 中,用户请求一个资源(通常是

    2024年02月10日
    浏览(55)
  • 【C++深入浅出】类和对象中篇(六种默认成员函数、运算符重载)

    目录 一. 前言  二. 默认成员函数 三. 构造函数 3.1 概念 3.2 特性 四. 析构函数 4.1 概念 4.2 特性 五. 拷贝构造函数 5.1 概念 5.2 特性 六. 运算符重载 6.1 引入 6.2 概念 6.3 注意事项 6.4 重载示例 6.5 赋值运算符重载 6.6 前置++和后置++运算符重载 七. const成员函数 7.1 问题引入 7.2 定义

    2024年02月09日
    浏览(56)
  • 深入浅出Spring AOP

    第1章:引言 大家好,我是小黑,咱们今天要聊的是Java中Spring框架的AOP(面向切面编程)。对于程序员来说,理解AOP对于掌握Spring框架来说是超级关键的。它像是魔法一样,能让咱们在不改变原有代码的情况下,给程序增加各种功能。 AOP不仅仅是一个编程范式,它更是一种思

    2024年01月20日
    浏览(58)
  • 深入浅出 Spring:核心概念和基本用法详解

    个人主页:17_Kevin-CSDN博客 收录专栏;《Java》 在 Java 企业级应用开发中,Spring 框架已经成为了事实上的标准。它提供了一种轻量级的解决方案,使得开发者能够更轻松地构建灵活、可扩展的应用程序。在本文中,我们将探讨 Spring 框架的一些核心概念和基本用法,以此更好地

    2024年03月20日
    浏览(55)
  • Spring5深入浅出篇:Spring与工厂设计模式简介

    轻量级 JavaEE的解决⽅案 spring实际上就是对原有设计模式的一种高度封装和整合 整合设计模式 工厂设计模式 什么是工厂设计模式 当UserServiceImpl发生变化是会影响到userService等相关联的类,在线上环境不利于维护

    2024年01月18日
    浏览(55)
  • Spring5深入浅出篇:bean的生命周期

    指的是⼀个对象创建、存活、消亡的⼀个完整过程 由Spring负责对象的创建、存活、销毁,了解⽣命周期,有利于我们使⽤好Spring为我们创建的对象 创建阶段 Spring⼯⼚何时创建对象 当bean标签中增加scope=\\\"singleton\\\"时,当你创建对象所有的引用都是第一个对象的内存地址;sigleton:只

    2024年04月12日
    浏览(42)
  • 【深入浅出Spring原理及实战】「源码调试分析」深入源码探索Spring底层框架的的refresh方法所出现的问题和异常

    阅读Spring官方文档,了解Spring框架的基本概念和使用方法。 下载Spring源码,可以从官网或者GitHub上获取。 阅读Spring源码的入口类,了解Spring框架的启动过程和核心组件的加载顺序。 阅读Spring源码中的注释和文档,了解每个类和方法的作用和用法。 调试Spring源码,可以通过

    2023年04月23日
    浏览(49)
  • Spring高手之路14——深入浅出:SPI机制在JDK与Spring Boot中的应用

       SPI ( Service Provider Interface ) 是一种服务发现机制,它允许第三方提供者为核心库或主框架提供实现或扩展。这种设计允许核心库/框架在不修改自身代码的情况下,通过第三方实现来增强功能。 JDK原生的SPI : 定义和发现 : JDK 的 SPI 主要通过在 META-INF/services/ 目录下放置

    2024年02月09日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包