【Spring Security】的RememberMe功能流程与源码详解
文章目录
- 前言
- 原理
- 基础版
- 搭建
- 初始化sql
- 依赖引入
- 配置类
- 验证
- 源码分析
- 进阶版
- 集成
- 源码分析
- 疑问1
- 疑问2
- 鉴权
- 升级版
- 集成
- 初始化sql
- 配置类
- 验证
- 源码分析
- 鉴权
- 流程
- 扩展版
前言
之前我已经写过好几篇权限认证相关的文章了,有想复习的同学可以查看【身份权限认证合集】。今天我们来聊一下登陆页面中“记住我”这个看似简单实则复杂的小功能。
如图就是某网站登陆时的“记住我”选项,在实际开发登陆接口以前,我一直认为这个“记住我”就是把我的用户名和密码保存到浏览器的 cookie 中,当下次登陆时浏览器会自动显示我的用户名和密码,就不用我再次输入了。
直到我看了 Spring Security
中 Remember 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.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
输入用户名密码登陆成功
我们就可以拿着 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 功能.rememberMe().and()//登陆成功之后的跳转页面.formLogin().defaultSuccessUrl("/productInfo/index").permitAll().and()//CSRF禁用.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
的代码
//此处存放的是登陆的用户信息,可以理解为对应的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 类中执行代码,返回userUserDetails 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
不要问我为啥这样创建表,我会在下边告诉你
【Spring Security】的RememberMe功能流程与源码详解相关推荐
- 【换脸详细教程】手把手教你进行AI换脸:换脸流程及源码详解
目录 1. 换脸基本原理 2 人脸检测及可视化 3. 人脸轮廓点检测及可视化 4. 人脸图像变换--仿射变换 5. 生成遮罩并直接替换人脸 6. 人脸颜色校正 最近AI换脸貌似比较火爆,就稍微研究了一 ...
- Spring Security实现RememberMe功能以及原理探究
在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,下面,主要讲解如何使用Spring Security实现记住我这个功能以及深入源 ...
- Spring事务源码详解
一. 简介 事务: 事务是逻辑上的一组操作,要么都执行,要么都不执行,关于事务的基本知识可以看我的这篇文章:事务的基础知识 Spring事务: Spring 支持两种方式的事务管理:编程式事务管理.声 ...
- 【JAVA秘籍心法篇-Spring】Spring XML解析源码详解
[JAVA秘籍心法篇-Spring]Spring XML解析源码详解 所谓天下武功,无坚不摧,唯快不破.但有又太极拳法以快制慢,以柔克刚.武功外式有拳打脚踢,刀剑棍棒,又有内功易筋经九阳神功.所有外功 ...
- Linux 内核中RAID5源码详解之守护进程raid5d
Linux 内核中RAID5源码详解之守护进程raid5d 对于一个人,大脑支配着他的一举一动:对于一支部队,指挥中心控制着它的所有活动:同样,对于内核中的RAID5,也需要一个像大脑一样的东西来支配 ...
- 【 反向传播算法 Back-Propagation 数学推导以及源码详解 深度学习 Pytorch笔记 B站刘二大人(3/10)】
反向传播算法 Back-Propagation 数学推导以及源码详解 深度学习 Pytorch笔记 B站刘二大人(3/10) 数学推导 BP算法 BP神经网络可以说机器学习的最基础网络.对于普通的简单 ...
- 【 线性回归 Linear-Regression torch模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人(4/10)】
torch模块实现与源码详解 深度学习 Pytorch笔记 B站刘二大人 深度学习 Pytorch笔记 B站刘二大人(4/10) 介绍 至此开始,深度学习模型构建的预备知识已经完全准备完毕. 从本章开 ...
- Laravel5.5源码详解 -- Laravel-debugbar及使用elementUI-ajax的注意事项
Laravel5.5源码详解 – Laravel-debugbar 及使用elementUI - ajax的注意事项 关于laravel对中间件的处理,请参中间件考另文, Laravel5.5源码详解 ...
- Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解
Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解 目录 Android AR开发实践之七:OpenGLES相机预览背景绘制源码详解 一.OpenGL ES渲染管线 1.基本处 ...
最新文章
- python文件的基础操作
- Hinton新作!越大的自监督模型,半监督学习需要的标签越少
- python-模块入门二(模块循环导入,区分python文件的两种用途,模块搜索路径,软件开发的目录规范)...
- AntDesignPro一次添加多条数据的表单字数限制,并且把input框变为可变文本框
- Python分析5000+抖音大V,发现大家都喜欢这类视频
- Unity3D之Mecanim动画系统学习笔记(五):Animator Controller
- 泛型 java 总结_JAVA泛型总结
- C/C++连接MySQL数据库执行查询
- 欢迎界面java_Linux命令行欢迎界面美化
- 计算机普通话培训开班简报,普通话培训第四期简报.doc
- mxnet入门--第4篇
- H3C无线ap基本配置套路
- CF1153F Serval and Bonus Problem
- 请说说自己对鲁迅本人他作品的了解计算机,26 回忆鲁迅先生课堂实录及点评
- 【财经期刊FM-Radio|2020年11月03日】
- Openlayers 中code错误编码对应的问题
- 安全L1-AD.3-DNS代理原理及配置
- 邻接矩阵无向图的介绍
- 假如时光可以倒流……
- pycharm--设置working directory
热门文章
- 全国企业税收调查数据(2007-2016)
- Simulink S-Function的使用(以串口接收MPU6050六轴陀螺仪参数为实例)
- 用大数据为潮流赋能 淘宝热词如何打造“有温度的时尚”?
- JavaScript判断设备类型的实现
- ACL访问控制列表案例(7.15)
- 论文调研——23.2.28
- echart堆叠柱状图,顶部显示堆叠柱总数的技巧
- java导出Excel增加下拉框选项,解决小数据量和大数据量下拉框选项的问题
- word中运行Mathtype报错问题解决方案
- ANSYS 有限元分析 后处理 结点解与单元解