首页 > 编程语言 >【Spring Security】的RememberMe功能流程与源码详解

【Spring Security】的RememberMe功能流程与源码详解

时间:2023-07-13 19:25:45浏览次数:43  
标签:me Spring request RememberMe token 源码 cookie NULL response

相关课程

前言

今天我们来聊一下登陆页面中"记住我"这个看似简单实则复杂的小功能。

如图就是某网站登陆时的"记住我"选项,在实际开发登陆接口以前,我一直认为这个"记住我"就是把我的用户名和密码保存到浏览器的 cookie 中,当下次登陆时浏览器会自动显示我的用户名和密码,就不用我再次输入了。

直到我看了 Spring SecurityRemember Me 相关的源码,我才意识到之前的理解全错了,它的作用其实是让用户在关闭浏览器之后再次访问时不需要重新登陆。

原理

如果用户勾选了 "记住我" 选项, Spring Security 将在用户登录时创建一个持久的安全令牌,并将令牌存储在 cookie 中或者数据库中。当用户关闭浏览器并再次打开时,Spring Security 可以根据该令牌自动验证用户身份。

先来张图感受下,然后跟着阿Q从简单的 Spring Security 登陆样例开始慢慢搭建吧!

基础版

搭建

初始化sql


CREATE TABLE `sys_user_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

INSERT INTO sys_user_info
(id, username, password)
VALUES(1, 'cheetah', '$2a$10$N.zJIQtKLyFe62/.wL17Oue4YFXUYmbWICsMiB7c0Q.sF/yMn5i3q');

CREATE TABLE `product_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `price` decimal(10,4) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(1, '从你的全世界路过', 32.0000, '2020-11-21 21:26:12', '2021-03-27 22:17:39');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(2, '乔布斯传', 25.0000, '2020-11-21 21:26:42', '2021-03-27 22:17:42');
INSERT INTO product_info
(id, name, price, create_date, update_date)
VALUES(3, 'java开发', 87.0000, '2021-03-27 22:43:31', '2021-03-27 22:43:34');

依赖引入

<dependency>
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-securityartifactId>
dependency>

配置类

自定义 SecurityConfig 类继承 WebSecurityConfigurerAdapter 类,并实现里边的 configure(HttpSecurity httpSecurity)方法。


@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
	httpSecurity
			.authorizeRequests()
			.anyRequest()

			.authenticated()
			.and()

			.formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
			.and()

			.csrf().disable();
}

另外还需要指定认证对象的来源和密码加密方式

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoder());
}

@Bean
public BCryptPasswordEncoder passwordEncoder(){
	return new BCryptPasswordEncoder();
}

【阿Q说代码】后台回复" reme"获取项目源码。

验证

启动程序,浏览器打开 http://127.0.0.1:8080/login

输入用户名密码登陆成功

我们就可以拿着 JSESSIONID 去请求需要登陆的资源了。

; 源码分析

方框中的是类和方法名,方框外是类中的方法具体执行到的代码。

首先会按照图中箭头的方向来执行,最终会执行到我们自定义的实现了 UserDetailsService 接口的 UserInfoServiceImpl 类中的查询用户的方法 loadUserByUsername()

该流程如果不清楚的话记得复习《实战篇:Security+JWT组合拳 | 附源码》

当认证通过之后会在 SecurityContext中设置 Authentication对象

org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#successfulAuthentication中的方法 SecurityContextHolder.getContext().setAuthentication(authResult);

最后调用 onAuthenticationSuccess方法跳转链接。

; 进阶版

集成

接下来我们就要开始进入正题了,快速接入"记住我"功能。

在配置类 SecurityConfig 的 configure() 方法中加入 两行代码,如下所示

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
	httpSecurity
			.authorizeRequests()
			.anyRequest()

			.authenticated()
			.and()

			.rememberMe()
			.and()

			.formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
			.and()

			.csrf().disable();
}

重启应用页面上会出现单选框"Remember me on this computer"

可以查看下页面的属性,该单选框的名字为"remember-me"

点击登陆,在 cookie 中会出现一个属性为 remember-me 的值,在以后的每次发送请求都会携带这个值到后台

然后我们直接输入 http://127.0.0.1:8080/productInfo/getProductList获取产品信息

当我们把 cookie 中的 JSESSIONID 删除之后重新获取产品信息,发现会生成一个新的 JSESSIONID。

源码分析

认证通过的流程和基础版本一致,我们着重来分析身份认证通过之后,跳转链接之前的逻辑。

; 疑问1

图中1处为啥是 AbstractRememberMeServices 类呢?

我们发现在项目启动时,在类 AbstractAuthenticationFilterConfigurer 的 configure() 方法中有如下代码

RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
if (rememberMeServices != null) {
	this.authFilter.setRememberMeServices(rememberMeServices);
}

AbstractRememberMeServices 类型就是在此处设置完成的,是不是一目了然了?

疑问2

当代码执行到图中2和3处时

@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                               Authentication successfulAuthentication) {
    if (!rememberMeRequested(request, this.parameter)) {
        this.logger.debug("Remember-me login not requested.");
        return;
    }
    onLoginSuccess(request, response, successfulAuthentication);
}

因为我们勾选了"记住我",所以此时的值为"on",即 rememberMeRequested(request, this.parameter)返回 true,然后 加非返回 false,最后一步就是设置 cookie 的值。

鉴权

此处的讲解一定要对照着代码来看,要不然很容易错位,没有类标记的方法都属于 RememberMeAuthenticationFilter#doFilter

当直接调用 http://127.0.0.1:8080/productInfo/index接口时,会走 RememberMeAuthenticationFilter#doFilter的代码


if (SecurityContextHolder.getContext().getAuthentication() != null) {
	this.logger.debug(LogMessage
			.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
					+ SecurityContextHolder.getContext().getAuthentication() + "'"));
	chain.doFilter(request, response);
	return;
}

因为 SecurityContextHolder.getContext().getAuthentication()中有用户信息,所以直接返回商品信息。

当删掉 JSESSIONID 后重新发起请求,发现 SecurityContextHolder.getContext().getAuthentication()为 null ,即用户未登录,会往下走 Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);代码,即自动登陆的逻辑

@Override
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {

	String rememberMeCookie = extractRememberMeCookie(request);
	if (rememberMeCookie == null) {
		return null;
	}
	this.logger.debug("Remember-me cookie detected");
	if (rememberMeCookie.length() == 0) {
		this.logger.debug("Cookie was empty");
		cancelCookie(request, response);
		return null;
	}
	try {

		String[] cookieTokens = decodeCookie(rememberMeCookie);

		UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
		this.userDetailsChecker.check(user);
		this.logger.debug("Remember-me cookie accepted");
		return createSuccessfulAuthentication(request, user);
	}
	catch (CookieTheftException ex) {
		cancelCookie(request, response);
		throw ex;
	}
	catch (UsernameNotFoundException ex) {
		this.logger.debug("Remember-me login was valid but corresponding user not found.", ex);
	}
	catch (InvalidCookieException ex) {
		this.logger.debug("Invalid remember-me cookie: " + ex.getMessage());
	}
	catch (AccountStatusException ex) {
		this.logger.debug("Invalid UserDetails: " + ex.getMessage());
	}
	catch (RememberMeAuthenticationException ex) {
		this.logger.debug(ex.getMessage());
	}
	cancelCookie(request, response);
	return null;
}

执行完之后接着执行 RememberMeAuthenticationFilter#doFilter中的 rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);

当执行到 ProviderManager#authenticate中的 result = provider.authenticate(authentication);时,会走RememberMeAuthenticationProvider 中的方法返回 Authentication 对象。

SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);将登录成功信息保存到 SecurityContextHolder 对象中,然后返回商品信息。

升级版

如果记录在服务器 session 中的 token 因为服务重启而失效,就会导致前端用户明明勾选了"记住我"的功能,但是仍然提示需要登陆。

这就需要我们对 session 中的 token 做持久化处理,接下来我们就对他进行升级。

集成

初始化sql

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL COMMENT '用户名',
  `series` varchar(64) NOT NULL COMMENT '主键',
  `token` varchar(64) NOT NULL COMMENT 'token',
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后一次使用的时间',
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

不要问我为啥这样创建表,我会在下边告诉你

标签:me,Spring,request,RememberMe,token,源码,cookie,NULL,response
From: https://www.cnblogs.com/3shu/p/17551855.html

相关文章

  • 实战:单点登录的两种实现方式,附源码
    相关课程最近工作有点忙,好久没更新文章了,正好这两天在整理单点登陆相关的文档,今天趁着小孩睡着了......
  • Spring 方法命名为啥好多用post ?
    参考:JLSPreIncrementExpressionPostIncrementExpressioninta=0;a++;//post++a;//pre示例:@Testpublicvoidtest(){inti=0;System.out.println(i++);System.out.println(++i);} 意思是执行了方法之后,入参会发生改......
  • spring bean 的属性为 java.util.Properties 时如何初始化该属性
       publicclassFooBean{privatejava.util.Propertiesattr;publicjava.util.PropertiesgetAttr(){returnattr;}publicvoidsetAttr(java.util.Propertiesattr){this.attr=attr;}} <beanid=......
  • SpEL (Spring Expression Language)
    https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html 6.1 IntroductionTheSpringExpressionLanguage(SpELforshort)isapowerfulexpressionlanguagethatsupportsqueryingandmanipulatinganobjectgraphatruntime.T......
  • spring 静态变量方式加载properties 文件(支持profile)
     foo-test.properties(测试环境)foo-pro.properties(生产环境)需要根据spring.profiles.active切换 importjava.io.IOException;importjava.util.Properties;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.beans.facto......
  • Spring及IOC
    Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架) IoC容器控制反转IoC(InversionofControl),是一种设计思想,DI(依赖注入)是实现IoC的一种方法没有IoC的程序中,我们使用面向对象编程,对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控......
  • SpringMVC入门案例
                ......
  • spring纯注解开发模式
    1、IOC的注解:1.1@Component【重点】:相当于<bean>标签:把对象交给spring管理,spring会帮助我们创建对象。@controller,@Service,@Repository与@Component的作用完全一致,但更加详细化。@Controller:用于web层的bean;@Service:用于service层的bean;@Repository:用于dao层的bean;1.2其他......
  • spring cloud Eureka 注册中心
    SpringCloud是一组框架和工具集,用于快速构建分布式系统,为微服务架构提供了全套技术支持。其中,注册中心是SpringCloud微服务架构中的一个重要组件,它提供了服务注册和发现的功能,是构建分布式系统的基础。本文将介绍SpringCloud中的Eureka注册中心,并给出相应的示例说明。Eureka注......
  • StarRocks Segment源码阅读笔记--SegmentIterator创建
    StarRocks中要读取Segment中的数据,需要先创建SegmentIteratorStatusOr<ChunkIteratorPtr>Segment::_new_iterator(constSchema&schema,constSegmentReadOptions&read_options){DCHECK(read_options.stats!=nullptr);//tryingtoprunethecurrentse......