SpringSecurity(2)---记住我功能实现
SpringSecurity(2)---记住我功能实现
上一篇博客实现了认证+授权的基本功能,这里在这个基础上,添加一个 记住我的功能。
上一篇博客地址:SpringSecurity(1)---认证+授权代码实现
说明:上一遍博客的 用户数据 和 用户关联角色 的信息是在代码里写死的,这篇将从mysql数据库中读取。
一、数据库建表
这里建了三种表

一般权限表有四张或者五张,这里有关 角色关联资源表 没有创建,角色和资源的关系依旧在代码里写死。
建表sql
/*创建用户表*/
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*创建j角色表*/
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
/*创建用户关联角色表*/
CREATE TABLE `roles_user` (
`id` int NOT NULL AUTO_INCREMENT,
`rid` int DEFAULT '2',
`uid` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=133 DEFAULT CHARSET=utf8;
/*这里密码对应的明文 还是123456*/
INSERT INTO `user` (`id`, `username`, `nickname`, `password`, `enabled`)
VALUES
(1, '小小', '小小', 'e10adc3949ba59abbe56e057f20f883e', 1);
/*三种角色*/
INSERT INTO `roles` (`id`, `name`)
VALUES
(1, '校长'),
(2, '教师'),
(3, '学生');
/*小小用户关联了 教师和校长角色*/
INSERT INTO `roles_user` (`id`, `rid`, `uid`)
VALUES
(1, 2, 1),
(2, 3, 1);
说明:这里数据库只有一个用户
用户名 :小小
密码:123456
她所拥有的角色有两个 教师 和 学生。
二、Spring Security的记住我功能基本原理
概念 记住我在登陆的时候都会被用户勾选,因为它方便地帮助用户减少了输入用户名和密码的次数,用户一旦勾选记住我功能那么 当服务器重启后依旧可以不用登陆就可以访问。
Spring Security的“记住我”功能的基本原理流程图如下所示:

这里大致流程如下:
第一次登陆
用户请求的时候 remember-me参数为true 时,用户先进行 认证+授权过滤器。然后走记住我过滤器这里需要做两,这里主要做两件事。
1.将Token数据存入数据库 2.将token数据存入cookie中。
服务重启后
如果服务重启的话,那么之前的session信息已经不在了,但是cookie中的Token还是存在的。所以当用户重启后去访问需要认证的接口时,会先通过cookie中的Token
去数据库查询这条Token信息,如果存在那么在通过用户名去查询数据库获取当前用户的信息。
三、代码实现
因为上面项目已经完成了整个授权+认证的过程,那么这里就很简单添加一点点代码就可以了。
在WebSecurityConfig中添加一个Bean,配置完这个Bean就基本完成了 记住我 功能的开发,然后在将这个Bean设置到configure方法中即可。
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
上面的代码 tokenRepository.setCreateTableOnStartup(true) ;是自动创建Token存到数据库时候所需要的表,这行代码只能运行一次,如果重新启动数据库,
必须删除这行代码,否则将报错,因为在第一次启动的时候已经创建了表,不能重复创建。保险起见我们还是注释掉这段代码,手动建这张表。
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在配置里再加上这些就可以了。

四、测试
主要测试两个点地方,
1、当我登陆时选择记住我功能,看下数据库persistent_logins是否有一条token记录
2、当使用记住我功能后,关闭服务器在重启服务器,不再登陆直接访问需要认证的接口,看是否能够访问成功。
1、首次登陆

我们在看数据库token表

很明显新增了一条token数据。
2、重启服务器
这个时候我们重启服务器访问需要认证的接口

发现就算重启也不需要重启登陆就可以反问需要认证的接口。
五、源码分析
同样这里也分为两部分 1、第一次登陆源码流程。 2、重启后未认证再去访问需要认证的接口源码流程。
1、首次登陆源码流程
第一步
当用户发送登录请求的时候,首先到达的是UsernamePasswordAuthenticationFilter这个过滤器,然后执行attemptAuthentication方法的代码,代码如下图所示:
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());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
之后所走的流程就是 ProviderManager的authenticate方法 ,之后再走AbstractUserDetailsAuthenticationProvider的authenticate方法,再走DaoAuthenticationProvider的方法retrieveUser方法。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
//这里就走我们自定义的获取用户认证和授权信息的代码了
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
这样一来,认证的流程就已经走完了。那就要走记住我功能的过滤器了。
第二步
验证成功之后,将进入AbstractAuthenticationProcessingFilter 类的successfulAuthentication的方法中,首先将认证信息通过代码
SecurityContextHolder.getContext().setAuthentication(authResult);将认证信息存入到session中,紧接着这个方法中就调用了rememberMeServices的loginSuccess方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
//记住我
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
再走PersistentTokenBasedRememberMeServices的onLoginSuccess方法
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
//这里就是关键的两步 1、将token存入到数据库 2、将token存入cookie中
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
这个方法中调用了tokenRepository来创建Token并存到数据库中,且将Token写回到了Cookie中。到这里,基本的登录过程基本完成,生成了Token存到了数据库,
且写回到了Cookie中。
2、第二次访问
重启项目,这时候服务器端的session已经不存在了,但是第一次登录成功已经将Token写到了数据库和Cookie中,直接访问一个服务,并且不输入用户名和密码。
第一步
首先进入到了RememberMeAuthenticationFilter的doFilter方法中,这个方法首先检查在session中是否存在已经验证过的Authentication了,如果为空,就进行下面的
RememberMe的验证代码,比如调用rememberMeServices的autoLogin方法,代码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//走记住我流程
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
//省略不重要的代码
chain.doFilter(request, response);
} else {
chain.doFilter(request, response);
}
}
我们在看this.rememberMeServices.autoLogin(request, response)方法。最终实现在AbstractRememberMeServices的autoLogin方法中
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
//1、获取token
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
//这步是关键
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
} catch (CookieTheftException var6) {
this.cancelCookie(request, response);
throw var6;
}
this.cancelCookie(request, response);
return null;
}
}
}
我们在看 this.processAutoLoginCookie(cookieTokens, request, response);在PersistentTokenBasedRememberMeServices中实现,到这一步就已经很明白了
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
} else {
String presentedSeries = cookieTokens[0];
String presentedToken = cookieTokens[1];
//1、去token表中查询token
PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
if (token == null) {
throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
//2校验数据
} else 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."));
//3、查看token是否过期
} else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
}
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
try {
//4、更新这条token 没更新一次有效时间就都变成了之间设置的时间
this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
this.addCookie(newToken, request, response);
} catch (Exception var9) {
this.logger.error("Failed to update token: ", var9);
throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
}
//5、这里拿着用户名 就又获取当前用户的认证和授权信息
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
}
}
}
这样整个流程就完成了,我们可以看出源码的过程和上面图片展示的流程还是非常像的。
别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
攻我盾者,乃我内心之矛(18)
SpringSecurity(2)---记住我功能实现的更多相关文章
- SpringSecurity之记住我功能的实现
Spring security记住我基本原理: 登录的时候,请求发送给过滤器UsernamePasswordAuthenticationFilter,当该过滤器认证成功后,会调用RememberMeS ...
- SpringSecurity实现记住我功能
⒈表单添加 <form action="/authentication/form" method="post"> <table> < ...
- SpringBoot + Spring Security 学习笔记(四)记住我功能实现
记住我功能的基本原理 当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的 ...
- JavaWeb-SpringSecurity记住我功能
系列博文 项目已上传至guthub 传送门 JavaWeb-SpringSecurity初认识 传送门 JavaWeb-SpringSecurity在数据库中查询登陆用户 传送门 JavaWeb-Sp ...
- java实现记住密码功能(利用cookie)
<br> <input type="text" id="userName" name="userName" value=& ...
- 通过sharedpreferences实现记住密码功能
通过sharedpreferences实现记住密码功能
- jquery.cookie.js 操作cookie实现记住密码功能的实现代码
jquery.cookie.js操作cookie实现记住密码功能,很简单很强大,喜欢的朋友可以参考下. 复制代码代码如下: //初始化页面时验证是否记住了密码 $(document).ready( ...
- cookie记住密码功能
很多门户网站都提供了记住密码功能,虽然现在的浏览器都已经提供了相应的记住密码功能 效果就是你每次进入登录页面后就不需要再进行用户名和密码的输入: 记住密码功能基本都是使用cookie来进行实现的,因此 ...
- 【原创】js中利用cookie实现记住密码功能
在登录界面添加记住密码功能,我首先想到的是在java后台中调用cookie存放账号密码,大致如下: HttpServletRequest request HttpServletResponse res ...
随机推荐
- Amaze UI学习笔记——JS学习历程一
1.自定义事件 (1)一些组件提供了自定义事件,命名方式为{事件名称}.{组件名称}.amui,用户可以查看组件文档了解.使用这些事件,如: $('#myAlert').on('close.alert ...
- PL/SQL语言语法
一.前言 SQL全称是"结构化查询语言(Structured Query Language)",而PL/SQL是过程语言(Procedure Language),是对SQL的扩展. ...
- UVALive3720
题目大意:见刘汝佳<算法竞赛入门经典——训练指南>P173. 解题思路: 问题可以转化为求共有多少条过点阵中的点的斜线.其中必定包含左斜线和右斜线,由于点阵式对称的,所以我们只需求出左右斜 ...
- 蓝桥杯 试题 历届试题 发现环 并查集+dfs
问题描述 小明的实验室有N台电脑,编号1~N.原本这N台电脑之间有N-1条数据链接相连,恰好构成一个树形网络.在树形网络上,任意两台电脑之间有唯一的路径相连. 不过在最近一次维护网络时,管理员误操作使 ...
- Django创建应用以及路由的配置
Django简介:是一个开放源代码的web 应用框架,由python 写成.初次发布于2005年7月,并于2008年9月发布了第一个正式版本1.0. 文件说明:manage.py:一个命令行工具,可以 ...
- 阿里P9精心编写高并发设计手册,来看大厂是如何进行系统设计
在看这篇文章的应该都是IT圈的朋友吧,不知道你们有没有考虑过这样几件事: 淘宝双11的剁手狂欢为什么天猫没崩掉? 为什么滴滴打车高峰如何滴滴依旧可以平稳运行? 为什么疫情期间,钉钉能支撑那么多人同时上 ...
- php日志监控
<?php date_default_timezone_set('Asia/Shanghai'); $time = date('Y-m-d H:i:s',time()); //访问时间 $ip ...
- [Objective-C] 006_Protocol(协议)
学过java的同学都知道Interface(接口),那么在Objective-C中有没有接口呢?其实 Objective-C中用Protocol(协议)来实现的,在Objective-C具体怎么用,我 ...
- CSS选择器-类-ID-伪类
类选择器(Class selectors) 通过设置元素的 class 属性,可以为元素指定类名.类名由开发者自己指定. 文档中的多个元素可以拥有同一个类名. 在写样式表时,类选择器是以英文句号(.) ...
- 03 . 前端之JavaScipt
JavaScript概述 ECMAScript和JavaScript的关系 1996年11月,JavaScript的创造者–Netscape公司,决定将JavaScript提交给国际标准化组织ECMA ...