前言

在上一章节中,一一哥 带各位基于散列加密方案实现了自动登录,并且给各位介绍了散列加密算法,其实还有另一种自动登录的实现方案,也就是基于持久化令牌方案来进行实现。接下来请跟 一一哥 学习这种方案该怎么实现吧。

一. 持久化令牌方案简介

1. 持久化令牌方案

有的小伙伴会问,既然我们要基于持久化令牌来实现自动登录,那啥是持久化令牌啊?所以 一一哥 先给大家做个概念解释。

所谓的持久化令牌的实现方案,其实在交互上与我们前面章节讲的散列加密方案一致,只不过是在用户勾选Remember-me之后,将生成的token令牌发送到用户浏览器,并在用户下次访问系统时读取该令牌进行认证。不同的是,持久化令牌方案采用了更加严谨的安全性设计,也就是安全性更好一些。

在持久化令牌方案中,最核心的是series和token两个值,这两个值都是用MD5散列计算生成的随机字符串。不同的是,series仅在用户使用密码重新登录时更新,而 token 会在每一个新的session会话中都重新生成

2. 持久化令牌方案优点

我们前面已经学习了基于散列算法的自动登录方案了,为啥还要再学习持久化方案呢?肯定是因为它有独特之处吧。

首先,持久化令牌方案 避免了散列加密方案中,一个令牌可以同时在多端登录的问题,这是因为每个session会话都会引发token的更新,即每个token仅支持单实例登录。

其次,自动登录不会导致series变更,但每次自动登录都需要同时验证 series和 token两个值,所以这样的设计会更安全。因为当该令牌还未使用过自动登录就被盗取时,系统会在非法用户验证通过后刷新 token 值,此时在合法用户的浏览器中,该token值已经失效。当合法用户使用自动登录时,由于该series对应的 token 不同,系统可以推断该令牌可能已被盗用,从而做一些处理。例如,清理该用户的所有自动登录令牌,并通知该用户可能已被盗号等。

了解了持久化令牌的概念和优点之后,接下来就跟着 壹哥 进行代码实现吧。

二. 持久化令牌方案的代码实现

1. 创建persistent_logins表

在持久化令牌方案中,是要把令牌进行持久化保存的,那么把令牌持久化到哪里去呢?我们首选数据库!

所以我们首先创建一张persistent_logins表,用来存储我们自动登录时生成的持久化令牌信息,该表SQL脚本如下:

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

在该表中,series是主键,可以根据series进行令牌信息的查询等操作。

2. 配置SecurityConfig类

请按之前的方式创建一个新的项目module,具体过程略。

首先我们创建SucurityConfig配置类,在configure(HttpSecurity http)方法中通过tokenRepository()方法关联JdbcTokenRepositoryImpl,进而对persistent_logins表进行增删改查。

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Value("${spring.security.remember-me.key}")private String rememberKey;@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {//配置数据源JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").antMatchers("/user/**").hasRole("USER").antMatchers("/app/**").permitAll().anyRequest().authenticated().and().formLogin().permitAll().and()//开启记住我功能.rememberMe().userDetailsService(userDetailsService)//1.设置加密的key.key(rememberKey)//2.持久化令牌方案.tokenRepository(tokenRepository)//设置令牌有效期,为7天有效期.tokenValiditySeconds(60 * 60 * 24 * 7).and().csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}}

3. 配置application.yml文件

创建applicaiton.yml文件并在其中关联配置自己的数据库,我是在该数据库中创建了persistent_logins表。

spring:datasource:url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMTusername: rootpassword: sycsecurity:remember-me:key: yyg

4. 启动项目测试

创建一个项目入口类(代码略),然后把项目启动起来。

这时候,我们只需要在登录页面中输入 用户名和密码,勾选“记住我”功能之后,Spring Security就会生成一个持久化令牌,在这个令牌中就保存了当前登陆的用户信息,该令牌信息会被自动持久化存储到persistent_logins表中。

在我们的persistent_logins表中,你会看到保存的自动登录的用户信息。

这样我们就实现了基于持久化令牌的自动登录方案。

5. 代码结构

各位可以参考下图,结合前面的章节,自行实现。

三. 持久化令牌源码解析

现在 壹哥 已经带各位实现了基于持久化令牌方案的自动登录,你可能很好奇,Spring Security内部到底是怎么进行实现的呢?接下来请跟着我一起分析其实现原理和底层源码吧。

1. 持久化令牌的实现原理

我先给大家分析一下持久化令牌的实现原理。

当我们经过自动登录之后,就可以在persistent_logins表的series和token字段中看到,分别保存了对应的信息。

在自动登录成功后,也会在remember-me中保存用户的cookie信息,我们可以利用Base64在线编解码工具对该cookie信息进行解码。Base64在线编解码工具直接百度即可找到。

我们看到解码出来的结果以 “:” 分割为前后两部分,冒号前面的部分是我们保存在数据库里的series字段的内容,冒号后面的部分是token字段存储的内容。

另外在persistent_logins表中还存储了token的过期时间,以后用户每次登陆成功后,都会通过用户名确认该令牌的身份,通过对比token可以得知该令牌是否有效。并且通过上一次自动登录的时间也可以知道该令牌是否已过期,并在完整校验通过之后生成新的token。

2. PersistentRememberMeToken令牌类

我给大家介绍完持久化令牌的基本实现原理后,再给各位剖析一下该方案的底层源码。

Spring Security中是使用 PersistentRememberMeToken类来封装持久化令牌对象的,源码如下。

/*** @author Luke Taylor*/
public class PersistentRememberMeToken {private final String username;private final String series;private final String tokenValue;private final Date date;......getter & setter方法略
}

会发现在该源码中,有series和tokenValue字段,可以分别存储persistent_logins表中的series和token字段内容。

3. PersistentTokenRepository

Spring Security之所以可以实现令牌的持久化存储,主要是基于PersistentTokenRepository接口,该接口的父子类关系图如下:

在PersistentTokenRepository接口中,定义了对令牌进行增删改查的4个方法,源码定义如下:

public interface PersistentTokenRepository {void createNewToken(PersistentRememberMeToken token);void updateToken(String series, String tokenValue, Date lastUsed);PersistentRememberMeToken getTokenForSeries(String seriesId);void removeUserTokens(String username);}    

4. JdbcTokenRepositoryImpl实现类

JdbcTokenRepositoryImpl是对PersistentTokenRepository接口的具体实现,该实现类的实现方法其实很简单,就是定义了5个SQL语句,分别是建表语句,以及对持久化令牌表的增删改查操作的SQL语句,源码如下:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implementsPersistentTokenRepository {// ~ Static fields/initializers// =====================================================================================/** Default SQL for creating the database table to store the tokens */public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "+ "token varchar(64) not null, last_used timestamp not null)";/** The default SQL used by the <tt>getTokenBySeries</tt> query */public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";/** The default SQL used by <tt>createNewToken</tt> */public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";/** The default SQL used by <tt>updateToken</tt> */public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";/** The default SQL used by <tt>removeUserTokens</tt> */public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";......

然后就是利用Spring自带的JdbcTemplate来实现对“persistent_logins”表的增删改查操作。

5. PersistentTokenBasedRememberMeServices类

我们再看看另一个带有记住我功能的持久化令牌服务类PersistentTokenBasedRememberMeServices,在该类中有一个处理自动登录的重要方法processAutoLoginCookie(),源码如下:

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) + "'");}final String presentedSeries = cookieTokens[0];final String presentedToken = cookieTokens[1];PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);if (token == null) {// No series match, so we can't authenticate using this cookiethrow new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);}// We have a match for this user/series combinationif (!presentedToken.equals(token.getTokenValue())) {// Token doesn't match series value. Delete all logins for this user and throw// an exception to warn them.tokenRepository.removeUserTokens(token.getUsername());throw new CookieTheftException(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");}// Token also matches, so login is valid. Update the token value, keeping the// *same* series number.if (logger.isDebugEnabled()) {logger.debug("Refreshing persistent login token for user '"+ token.getUsername() + "', series '" + token.getSeries() + "'");}PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());try {tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),newToken.getDate());addCookie(newToken, request, response);}catch (Exception e) {logger.error("Failed to update token: ", e);throw new RememberMeAuthenticationException("Autologin failed due to data access problem");}return getUserDetailsService().loadUserByUsername(token.getUsername());}

以上源码的实现逻辑如下:

  1. 首先从前端传来的 cookie 中解析出 series 和 token;
  2. 根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;
  3. 如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
  4. 接下来校验 token 是否过期;
  5. 构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);
  6. 将新的令牌重新添加到 cookie 中返回;
  7. 根据用户名查询用户信息,再走一波登录流程。

四. 两种自动登录实现方案对比

至此,我已经给大家详细讲解了基于散列加密方案和持久化令牌方案的自动化登录实现,这里我对两种方案做一个简单对比。

散列加密方案和持久化令牌方案,这两种方案都是把信息存储在cookie中,所以都有被盗取用户身份信息的可能性,当然持久化令牌方案的安全性更高一些。但是如果要你追求最安全的方式,那就尽量不要实现自动登录功能,所以我们要在用户体验和提高安全性之间选择平衡点。

如果我们一定要实现自动登录功能,可以限制以cookie身份登录时的部分执行权限比如在修改密码、修改邮箱(防止找回密码)、查看隐私信息(如完整的手机号码、银行卡号等)时,我们可以进一步校验用户的登录密码,或者设置独立密码来做二次校验,以提高安全性

五. 二次校验功能的实现

我们上面虽然讲解了2种自动登录的实现方案,但是依然存在用户身份被盗用的问题,这个问题其实是很难完美解决。那么我们能做的,只能是当发生用户身份被盗用这样的事情时,将损失降低到最小。因此,我们采用二次校验来增强项目的安全性。

1. 定义新的测试接口

我们在上面项目的基础上,添加一个新的测试接口/remember。

@RestController
public class UserController {@GetMapping("/admin/hello")public String helloAdmin() {return "hello, admin";}@GetMapping("/user/hello")public String helloUser() {return "hello, user";}@GetMapping("/visitor/hello")public String helloVisitor() {return "hello, visitor";}@GetMapping("/remember/hello")public String remember() {return "hello, remember-me功能";}}

2. 配置二次校验权限

在SecurityConfig类中对/remember/hello接口配置二次校验,主要是对该接口利用rememberMe()方法进行配置。

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Value("${spring.security.remember-me.key}")private String rememberKey;@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);http.authorizeRequests().antMatchers("/admin/**").fullyAuthenticated()//.hasRole("ADMIN").antMatchers("/user/**").hasRole("USER")//需要开启remember-me功能才能访问.antMatchers("/remember/**").rememberMe().antMatchers("/visitor/**").permitAll().anyRequest().authenticated().and().formLogin().permitAll().and()//开启记住我功能.rememberMe().userDetailsService(userDetailsService)//1.散列加密方案.key(rememberKey)//2.持久化令牌方案.tokenRepository(tokenRepository)//7天有效期.tokenValiditySeconds(60 * 60 * 24 * 7).and().csrf().disable();}@Beanpublic PasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}}

当我们访问"/remember/hello"接口时,需要二次校验才能访问,即该接口需要开启remember-me功能才能访问:

至此,壹哥 就结合着源码和底层原理,给大家讲解了基于持久化令牌方案实现了自动登录,并且在本案例中给大家介绍了持久化令牌的概念及实现原理,你掌握的怎么样呢?请在评论区给 一一哥 留言,说说你的感受吧!下一篇文章中,壹哥 会给各位讲解 如何在Spring Security环境下实现注销登录,敬请期待哦!

Spring Security系列教程16--基于持久化令牌方案实现自动登录相关推荐

  1. Spring Security系列教程03--创建SpringSecurity项目

    前言 在上一章节中,一一哥 已经带大家认识了Spring Security,对其基本概念已有所了解,但是作为一个合格的程序员,最关键的肯定还是得动起手来,所以从本篇文章开始,我就带大家搭建第一个Spr ...

  2. Spring Security系列教程11--Spring Security核心API讲解

    前言 经过前面几个章节的学习,一一哥 带大家实现了基于内存和数据库模型的认证与授权,尤其是基于自定义的数据库模型更是可以帮助我们进行灵活开发.但是前面章节的内容,属于让我们达到了 "会用&q ...

  3. Spring Security系列教程-Spring Security核心API讲解

    前言 经过前面几个章节的学习,一一哥 带大家实现了基于内存和数据库模型的认证与授权,尤其是基于自定义的数据库模型更是可以帮助我们进行灵活开发.但是前面章节的内容,属于让我们达到了 "会用&q ...

  4. Spring Security系列教程21--会话管理之实现集群会话

    前言 现在我们已经掌握了如何防御会话固定攻击,处理会话过期,对会话进行并发控制等,但是这些会话处理手段都是针对单机环境下的,在现在的大型项目中,很多时候都是采用分布式开发方案.一旦涉及到分布式方案,就 ...

  5. Spring Security系列教程18--会话管理之防御固定会话攻击

    前言 在前面几个章节中,一一哥 带各位学习了如何实现基于数据库进行认证授权,如何给登录界面添加图形验证码,如何进行自动登录和注销登录,那么Spring Security的功能难道只有这些吗?肯定不是的 ...

  6. 千锋教育威哥学Java——爆破专栏丨Spring Security系列教程之解决Spring Security环境中的跨域问题

    前言 上一章节中,一一哥 给各位讲解了同源策略和跨域问题,以及跨域问题的解决方案,在本篇文章中,我会带大家进行代码实现,看看在Spring Security环境中如何解决跨域问题. 需要更多教程,微信 ...

  7. Spring Security系列教程解决Spring Security环境中的跨域问题

    原创:千锋一一哥 前言 上一章节中,一一哥 给各位讲解了同源策略和跨域问题,以及跨域问题的解决方案,在本篇文章中,我会带大家进行代码实现,看看在Spring Security环境中如何解决跨域问题. ...

  8. 爆破专栏丨Spring Security系列教程之实现CAS单点登录上篇-概述

    作者:千锋一一哥 前言 从本章节开始,一一哥 会给各位讲解一个很常见也很重要的知识点,就是单点登录!现在的大型分布式项目,基本都会考虑实现单点登录,而且现在网上也有很多单点登录的实现方案.开源项目,但 ...

  9. java培训爆破专栏之Spring Security系列教程之实现CAS单点登录上篇-概述

    作者:千锋一一哥 前言 从本章节开始,一一哥 会给各位讲解一个很常见也很重要的知识点,就是单点登录!现在的大型分布式项目,基本都会考虑实现单点登录,而且现在网上也有很多单点登录的实现方案.开源项目,但 ...

  10. Spring Security 示例教程

    Spring Security 示例教程 Spring Security提供了在Web应用程序中执行身份验证和授权的方法.我们可以在任何基于servlet的Web应用程序中使用spring secur ...

最新文章

  1. 2012年技术图书大盘点
  2. 使用MyEclipse开发第一个Web程序
  3. 将PS/2接口鼠标改造成USB接口鼠标
  4. ATSS EfficientDet
  5. gradle 项目运行主类报错
  6. THUPCCTSAPIO2019:Far Away
  7. Android软键盘弹出时,覆盖布局,不是把布局顶上去的解决方法
  8. org.neo4j.kernel.StoreLockException: Store and its lock file has been locked by another process
  9. Segment Model.
  10. 安装Oracle 11g 出现交换空间不够
  11. ACM学习历程—HDU5668 Circle(数论)
  12. 程序员35岁之后的职业发展
  13. 日常笔记系列:java汉字判断
  14. mysql 5.5.27_MySQL-5.5.27安装图解
  15. VSCode折叠所有区域代码快捷键
  16. IO虚拟化 - virtio-blk前端驱动分析【转】
  17. 河海大学计算机专业戴慧凤,特色宿舍 - 河海大学学生工作处.doc
  18. 【NeurIPS2022】阿里提出基于离散化对抗训练的鲁棒视觉新基准
  19. 关于网上很不好找的幼儿识图APP
  20. Linux 入门第一节

热门文章

  1. 搜索下载免费操作系统(转)
  2. 数显之家快讯:【SHIO世硕心语】做为职业经理人,你想成为车轮,还是发动机,还是方向盘?
  3. macos可以升级到指定版本吗_从Mac os High Sierra 升级到 Mac os Majove指定系统版本
  4. python对数正态分布函数_Python对数正态分布函数,python,中,的
  5. SAS PROC FORMAT
  6. Educational Codeforces Round 97 (Rated for Div. 2)
  7. python操作浏览器滚动条_python selenium webdriver处理浏览器滚动条
  8. 老米之家域名投资是什么?域名怎么购买?域名的购买方式?
  9. Laravel多表连接,多个查询(Eloquent)
  10. 关于Pascal和二项式系数