【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

这篇具有很好参考价值的文章主要介绍了【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

之前我已经写过好几篇权限认证相关的文章了,有想复习的同学可以查看【身份权限认证合集】。今天我们来聊一下登陆页面中“记住我”这个看似简单实则复杂的小功能。

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

原理

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

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

基础版

搭建

初始化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.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</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禁用
			.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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

输入用户名密码登陆成功

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

源码分析

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

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

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

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

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

进阶版

集成

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

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

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
	httpSecurity
			.authorizeRequests()
			.anyRequest()
			//除上面外的所有请求全部需要鉴权认证
			.authenticated()
			.and()
			//开启 rememberMe 功能
			.rememberMe()
			.and()
			//登陆成功之后的跳转页面
			.formLogin().defaultSuccessUrl("/productInfo/index").permitAll()
			.and()
			//CSRF禁用
			.csrf().disable();
}

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

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

源码分析

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

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

疑问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的代码

//此处存放的是登陆的用户信息,可以理解为对应的cookie中的 JSESSIONID 
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) {
    //该方法的this.cookieName 的值为"remember-me",所以该处返回的是 cookie中remember-me的值
	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 {
        //对rememberMeCookie进行解码:
		String[] cookieTokens = decodeCookie(rememberMeCookie);
        //重点:执行TokenBasedRememberMeServices#processAutoLoginCookie下的 UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
        //就又回到我们自定义的 UserInfoServiceImpl 类中执行代码,返回user
		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

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

配置类
//在SecurityConfig的configure方法中增加一行
.rememberMe().tokenRepository(persistentTokenRepository());
    
//引入依赖,注入bean
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository(){
	JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
	tokenRepository.setDataSource(dataSource);
	return tokenRepository;
}
验证

重启项目,访问http://127.0.0.1:8080/login之后返回数据,查看表中数据,完美。

【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?

源码分析

前边的流程和升级版是相同的,区别就在于创建 token 之后是保存到 session 中还是持久化到数据库中,接下来我们从源码分析一波。

定位到AbstractRememberMeServices#loginSuccess中的 onLoginSuccess()方法,实际执行的是PersistentTokenBasedRememberMeServices#onLoginSuccess方法。

/**
 * 使用新的序列号创建新的永久登录令牌,并将数据存储在
 * 持久令牌存储库,并将相应的 cookie 添加到响应中。
 *
 */
@Override
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
		Authentication successfulAuthentication) {
	......
	try {
        //重点代码创建token并保存到数据库中
		this.tokenRepository.createNewToken(persistentToken);
		addCookie(persistentToken, request, response);
	}
	......
}

因为我们在配置类中定义的是JdbcTokenRepositoryImpl,所以进入改类的createNewToken方法。

@Override
public void createNewToken(PersistentRememberMeToken token) {
	getJdbcTemplate().update(this.insertTokenSql, token.getUsername(), token.getSeries(), token.getTokenValue(),
			token.getDate());
}

此时我们发现他就是做了插入数据库的操作,并且this.insertTokenSql

insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)

同时我们看到了熟悉的建表语句

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
			+ "token varchar(64) not null, last_used timestamp not null)

这样是不是就决解了上边的疑惑了呢。

执行完PersistentTokenBasedRememberMeServices#onLoginSuccess方法之后又进入到RememberMeAuthenticationFilter#doFilter()方法中结束。

有了持久化之后就不用担心服务重启了,接着我们重启服务,继续访问获取商品接口,成功返回商品信息。

鉴权

鉴权的逻辑也是和进阶版相似的,区别在于删除浏览器的 JSESSIONID 之后的逻辑。

定位到AbstractRememberMeServices#autoLogin中的UserDetails user = processAutoLoginCookie(cookieTokens, request, response);执行的是PersistentTokenBasedRememberMeServices#processAutoLoginCookie

//删减版代码
@Override
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
		HttpServletResponse response) {
	......
	String presentedSeries = cookieTokens[0];
	String presentedToken = cookieTokens[1];
	PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
	if (token == null) {
		throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
	}
	if (!presentedToken.equals(token.getTokenValue())) {
		this.tokenRepository.removeUserTokens(token.getUsername());
		throw new CookieTheftException(this.messages.getMessage(
				"PersistentTokenBasedRememberMeServices.cookieStolen",
				"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
	}
	if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
		throw new RememberMeAuthenticationException("Remember-me login has expired");
	}
	PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
			generateTokenData(), new Date());
	try {
		this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
		addCookie(newToken, request, response);
	}
	......
	return getUserDetailsService().loadUserByUsername(token.getUsername());
}
流程
  • 通过数据库中的 series 字段找到对应的记录;
  • 记录是否为空判断以及记录中的 token 是否和传入的相同;
  • 记录中的 last_used 加上默认的两周后是否大于当前时间,即是否 token 失效;
  • 更新该记录并将新生成的 token 放到 cookie 中;

后续的逻辑和进阶版一致。

扩展版

看到这有的小伙伴肯定会问了,如果我不用默认的登录页面,想用自己的登录页需要注意些什么呢?

首先要注意的就是“记住我”勾选框参数名必须为“remember-me”。如果你想自定义的话也是可以的,需要将自定义的名字例如:remember-me-new 配置到配置类中。

.rememberMe().rememberMeParameter("remember-me-new")

token 的有效期也是可以自定义的,例如设置有效期为2天

.rememberMe().tokenValiditySeconds(2*24*60*60)

我们还可以自定义保存在浏览器中的 cookie 的名称

.rememberMe().rememberMeCookieName("remember-me-cookie")

后台回复“reme”获取源码吧!

本文到这里就结束了,看看我为了写这篇文章又日渐稀少的头发,我忍不住哭出声来。可能只有给我点赞,才能平复我的心情吧。

好看的皮囊千篇一律,有趣的灵魂万里挑一,让我们在冷漠的城市里相互温暖,我是阿Q,我们下期再见!文章来源地址https://www.toymoban.com/news/detail-478624.html

到了这里,关于【Spring Security】的RememberMe功能流程与源码详解,基础-进阶-升级-扩展,你学会了吗?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • spring security @EnableWebSecurity自动配置DaoAuthenticationProvider流程

    spring-security:6.2.1 满足下列情况时,spring-security会自动配置DaoAuthenticationProvider 使用@EnableWebSecurity 注册UserDetailsServiceBean 没有注册其他AuthenticationProvider类型的Bean 没有通过http.authenticationProvider配置 org.springframework.security.config.annotation.web.configuration.EnableWebSecurity org.springframework.s

    2024年01月18日
    浏览(36)
  • Spring Security--守护你的功能权限

    首先,让我们明确一下什么是Spring Security以及前后端分离路径拦截器。Spring Security是一个基于Spring框架的安全框架,它提供了一系列的安全服务,包括但不限于认证、授权、加密和会话管理等。而前后端分离路径拦截器是指在前后端分离的情况下,根据用户角色和权限对请求

    2024年02月06日
    浏览(46)
  • 【业务功能篇57】Springboot + Spring Security 权限管理 【上篇】

    4.1.1 权限管理的意义 后台管理系统中,通常需要控制不同的登录用户可以操作的内容。权限管理用于管理系统资源,分配用户菜单、资源权限,以及验证用户是否有访问资源权限。 4.1.2 RBAC权限设计模型 ACL介绍 ACL(Access Control List):访问控制列表 用户 - 权限 ACL 模型,权限

    2024年02月15日
    浏览(42)
  • Spring Security详解

    Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括 用户认证(Authentication) 和 用户授权(Authorization) 两个部分。 用户认证指的是验证某个用户是否为系统中的合法主

    2024年02月11日
    浏览(41)
  • Spring Security实现详解

    一、WebSecurityConfigurerAdapter 总配置类: 1、介绍:配置类 2、主要方法: (1)configure(HttpSecurity http)  可以看到 WebSecurityConfigurerAdapter 已经默认声明了一些安全特性:   ① 验证所有用户请求;   ② 允许用户使用表单登录进行身份验证(Spring Security 提供了一个简单的表单登

    2024年01月19日
    浏览(34)
  • Spring Security 框架详解

    Spring Security是一款基于Spring框架的认证和授权框架,提供了一系列控制访问和保护应用程序的功能,同时也支持基于角色和权限的访问控制,加密密码,CSRF防范,会话管理等多种功能。Spring Security可以轻松地与其他Spring框架,如Spring Boot和Spring MVC进行集成使用。 本文将会对

    2024年02月04日
    浏览(40)
  • Spring Security 详解

    在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。特别是在spring boot项目中加入spring security更是十分简单。本篇我们介绍spring security,以及spring se

    2024年02月13日
    浏览(28)
  • 详解Spring Security

    目录 1.概述 2.登录 2.1.默认用户 2.2.自定义用户 2.3.加密 2.4.绕过加密 2.5.怎么传递用户信息 2.6.记住我 3.登出 4.使用数据库 4.1.jdbcAuthentication 4.2.userDetailsService 5.自定义处理器 6.更多细粒度的控制 7.原理简述 Spring Security是一个基于Spring框架的安全性框架,它提供了一系列的API和

    2023年04月24日
    浏览(28)
  • Spring Security基础学习

    一、SpringSecurity框架简介 二、SpringSecurity入门案例 三、SpringSecurity Web权限方案 四、SpringSecurity微服务权限方案 五、SpringSecurity原理总结

    2024年02月20日
    浏览(29)
  • 【Spring Security系列】Spring Security 过滤器详解与基于JDBC的认证实现

    上文说到,Spring Security它是一个强大的和高度可定制的身份验证和访问控制框架。它提供了一套丰富的功能,用于保护基于Spring的应用程序。 上文又说到,在Spring Security中,过滤器(Filter)是一个重要的组件,用于处理身份验证、授权和其他安全相关的任务。 Spring Security 的

    2024年04月22日
    浏览(42)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包