文章目录

  • 持久化令牌
    • 原理
    • 代码演示
    • 测试
    • 源码分析
  • 二次校验

在 上篇文章中,我们提到了 Spring Boot 自动登录存在的一些安全风险,在实际应用中,我们肯定要把这些安全风险降到最低,下面就来和大家聊一聊如何降低安全风险的问题。

降低安全风险,我主要从两个方面来给大家介绍:

  1. 持久化令牌方案
  2. 二次校验

持久化令牌

原理

要理解持久化令牌,一定要先搞明白自动登录的基本玩法,参考(Spring Security:自动登录)。

持久化令牌就是在基本的自动登录功能基础上,又增加了新的校验参数,来提高系统的安全性,这一些都是由开发者在后台完成的,对于用户来说,登录体验和普通的自动登录体验是一样的。

在持久化令牌中,新增了两个经过 MD5 散列函数计算的校验参数,一个是 series,另一个是 token。其中,series 只有当用户在使用用户名/密码登录时,才会生成或者更新,而 token 只要有新的会话,就会重新生成,这样就可以避免一个用户同时在多端登录,就像手机 QQ ,一个手机上登录了,就会踢掉另外一个手机的登录,这样用户就会很容易发现账户是否泄漏。

持久化令牌的具体处理类在 PersistentTokenBasedRememberMeServices 中,上篇文章我们讲到的自动化登录具体的处理类是在 TokenBasedRememberMeServices 中,它们有一个共同的父类:

而用来保存令牌的处理类则是 PersistentRememberMeToken,该类的定义也很简洁命令:

public class PersistentRememberMeToken {private final String username;private final String series;private final String tokenValue;private final Date date;//省略 getter
}

这里的 Date 表示上一次使用自动登录的时间。

代码演示

接下来,我通过代码来给大家演示一下持久化令牌的具体用法。

首先我们需要一张表来记录令牌信息,这张表我们可以完全自定义,也可以使用系统默认提供的 JDBC 来操作,如果使用默认的 JDBC,即 JdbcTokenRepositoryImpl,我们可以来分析一下该类的定义:

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implementsPersistentTokenRepository {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)";public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
}

根据这段 SQL 定义,我们就可以分析出来表的结构,这里给出一段 SQL 脚本:

CREATE TABLE `persistent_logins` (`username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,`series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

首先我们在数据库中准备好这张表。

既然要连接数据库,我们还需要准备 jdbc 和 mysql 依赖,如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>

然后修改 application.properties ,配置数据库连接信息:

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/javakf_test1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

接下来,我们修改 SecurityConfig,如下:

@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().rememberMe().key("javakf").tokenRepository(jdbcTokenRepository()).and().csrf().disable();
}

提供一个 JdbcTokenRepositoryImpl 实例,并给其配置 DataSource 数据源,最后通过 tokenRepository 将 JdbcTokenRepositoryImpl 实例纳入配置中。

OK,做完这一切,我们就可以测试了。

测试

我们还是先去访问 /hello 接口,此时会自动跳转到登录页面,然后我们执行登录操作,记得勾选上“记住我”这个选项,登录成功后,我们可以重启服务器、然后关闭浏览器再打开,再去访问 /hello 接口,发现依然能够访问到,说明我们的持久化令牌配置已经生效。

查看 remember-me 的令牌,如下:

这个令牌经过解析之后,格式如下:

gAnM2kjaP5tNZh3rHPR2OA%3D%3D:qVhKovcIayxvGNefBvKiHw%3D%3D

这其中,%3D 表示 =,所以上面的字符实际上可以翻译成下面这样:

gAnM2kjaP5tNZh3rHPR2OA==:qVhKovcIayxvGNefBvKiHw==

此时,查看数据库,我们发现之前的表中生成了一条记录:

数据库中的记录和我们看到的 remember-me 令牌解析后是一致的。

源码分析

这里的源码分析和上篇文章的流程基本一致,只不过实现类变了,也就是生成令牌/解析令牌的实现变了,所以这里我主要和大家展示不一样的地方,流程问题,大家可以参考上篇文章。

这次的实现类主要是:PersistentTokenBasedRememberMeServices,我们先来看里边几个和令牌生成相关的方法:

protected void onLoginSuccess(HttpServletRequest request,HttpServletResponse response, Authentication successfulAuthentication) {String username = successfulAuthentication.getName();PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(), generateTokenData(), new Date());tokenRepository.createNewToken(persistentToken);addCookie(persistentToken, request, response);
}
protected String generateSeriesData() {byte[] newSeries = new byte[seriesLength];random.nextBytes(newSeries);return new String(Base64.getEncoder().encode(newSeries));
}
protected String generateTokenData() {byte[] newToken = new byte[tokenLength];random.nextBytes(newToken);return new String(Base64.getEncoder().encode(newToken));
}
private void addCookie(PersistentRememberMeToken token, HttpServletRequest request,HttpServletResponse response) {setCookie(new String[] { token.getSeries(), token.getTokenValue() },getTokenValiditySeconds(), request, response);
}

可以看到:

  1. 在登录成功后,首先还是获取到用户名,即 username。
  2. 接下来构造一个 PersistentRememberMeToken 实例,generateSeriesData 和generateTokenData 方法分别用来获取 series 和 token,具体的生成过程实际上就是调用SecureRandom 生成随机数再进行 Base64 编码,不同于我们以前用的 Math.random 或者java.util.Random 这种伪随机数,SecureRandom则采用的是类似于密码学的随机数生成规则,其输出结果较难预测,适合在登录这样的场景下使用。
  3. 调用 tokenRepository 实例中的 createNewToken 方法,tokenRepository实际上就是我们一开始配置的 JdbcTokenRepositoryImpl,所以这行代码实际上就是将PersistentRememberMeToken 存入数据库中。
  4. 最后 addCookie,大家可以看到,就是添加了 series 和 token。

这是令牌生成的过程,还有令牌校验的过程,也在该类中,方法是:processAutoLoginCookie:

protected UserDetails processAutoLoginCookie(String[] cookieTokens,HttpServletRequest request, HttpServletResponse response) {final String presentedSeries = cookieTokens[0];final String presentedToken = cookieTokens[1];PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);if (!presentedToken.equals(token.getTokenValue())) {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");}PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),newToken.getDate());addCookie(newToken, request, response);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. 根据用户名查询用户信息,再走一波登录流程。

OK,这里和小伙伴们简单理了一下令牌生成和校验的过程,具体的流程,大家可以参考上篇文章。

二次校验

相比于上篇文章,持久化令牌的方式其实已经安全很多了,但是依然存在用户身份被盗用的问题,这个问题实际上很难完美解决,我们能做的,只能是当发生用户身份被盗用这样的事情时,将损失降低到最小。

因此,我们来看下另一种方案,就是二次校验。

二次校验这块,实现起来要稍微复杂一点,我先来和大家说说思路。

为了让用户使用方便,我们开通了自动登录功能,但是自动登录功能又带来了安全风险,一个规避的办法就是如果用户使用了自动登录功能,我们可以只让他做一些常规的不敏感操作,例如数据浏览、查看,但是不允许他做任何修改、删除操作,如果用户点击了修改、删除按钮,我们可以跳转回登录页面,让用户重新输入密码确认身份,然后再允许他执行敏感操作。

这个功能在 Shiro 中有一个比较方便的过滤器可以配置,Spring Security 当然也一样,例如我现在提供三个访问接口:

@RestController
public class HelloController {@GetMapping("/hello")public String hello() {return "hello";}@GetMapping("/admin")public String admin() {return "admin";}@GetMapping("/rememberme")public String rememberme() {return "rememberme";}}
  1. 第一个 /hello 接口,只要认证后就可以访问,无论是通过用户名密码认证还是通过自动登录认证,只要认证了,就可以访问。
  2. 第二个 /admin 接口,必须要用户名密码认证之后才能访问,如果用户是通过自动登录认证的,则必须重新输入用户名密码才能访问该接口。
  3. 第三个 /rememberme 接口,必须是通过自动登录认证后才能访问,如果用户是通过用户名/密码认证的,则无法访问该接口。

好了,我们来看下接口的访问要怎么配置:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/rememberme").rememberMe().antMatchers("/admin").fullyAuthenticated().anyRequest().authenticated().and().formLogin().and().rememberMe().key("javakf").tokenRepository(jdbcTokenRepository()).and().csrf().disable();
}

可以看到:

  1. /rememberme 接口是需要 rememberMe 才能访问。
  2. /admin 是需要 fullyAuthenticated,fullyAuthenticated 不同于authenticated,fullyAuthenticated 不包含自动登录的形式,而 authenticated包含自动登录的形式。
  3. 最后剩余的接口(/hello)都是 authenticated 就能访问。

OK,配置完成后,重启测试,测试过程我就不再赘述了。

代码托管:springsecurity_example_8

Spring Security:自动登录(降低安全风险)相关推荐

  1. Spring Security MVC登录注销示例教程

    Spring Security MVC登录注销示例教程 今天我们将了解Spring Security Login Example.在阅读这篇文章之前,请先阅读我在"Spring 4 Secu ...

  2. Spring Security默认登录页面

    使用Spring Security作为权限管理模块的小伙伴们一定醉心于其极少的配置即可满足权限管理需求,以及比springMVC更简洁的filter配置. 在刚开始技术验证的demo阶段相信很多人试过 ...

  3. spring Security 重复登录配置无效的问题

    关于spring Security重复登录的配置,百度一大堆,我这里就不啰嗦了. 今天碰到 按照网上的配置,但是 感觉配置无效,同一用户还是可以登录,不知道为什么,开始以为是自己配置的又问题.再三确认 ...

  4. 关闭Spring security的登录验证

    目的关闭Spring security 默认登录页 Springboot 2.x关闭需要在启动类上排除SecurityAutoConfiguration和ManagementWebSecurityAu ...

  5. Spring Security:自定义登录页面

    本文来说下Spring Security中如何自定义登录页面 文章目录 准备工作 自定义登录界面 本文小结 准备工作 添加模板引擎 这里使用了thymeleaf模板引擎,在pom.xml进行添加: & ...

  6. spring security+jwt 登录认证

    spring security+jwt 登录认证 1.综述 2.版本与环境 3.架构 4.数据库认证逻辑图 5.案例 security+jwt 5.1引入依赖 5.2新建工具类 5.2新建组件类 5. ...

  7. Spring Security自定义登录验证及登录返回结果

    Spring Security自定义登录验证及登录返回结果 一.功能描述 二.处理逻辑 简单流程 自定义UserDetails 自定义UserDetailsDAO 自定义UserDetailsServ ...

  8. (二)Spring Security自定义登录成功或失败处理器

    目录 一:创建登录成功处理器 二:创建登录失败处理器 三:添加处理器 三. 项目地址 我们接着上一章 Spring Security最简单的搭建,进行开发 LoginSuccessHandler 和L ...

  9. spring security导致登录后从https跳转至http解决方案

    1. 项目为spring boot项目,由原来的http连接更换为https连接,因项目中配置的了spring security,登录被spring security拦截重定向后会跳转到http 解决 ...

  10. Spring Security自定义登录验证,验证码,动态管理uri访问权限,Thymeleaf,限制密码强度、过期、错误密码锁定超时自动解锁、禁用历史密码、新密码和现密码差异要求编辑距离

    在本教程中,我将指导您如何编写代码,以使用具有基于表单的身份验证的Spring安全API来保护Spring Boot应用程序中的网页.用户详细信息存储在MySQL数据库中,并使用春季JDBC连接到数据 ...

最新文章

  1. python文件输入和输出
  2. 汇编语言随笔(3)-条件转移指令和标志寄存器
  3. 第3周实践项目4 -顺序表的应用 删除顺序表中元素为x的值
  4. 如何快速研究透一篇 Paper 并提出有价值的问题?
  5. devstack 安装trove newtone
  6. Ubuntu12.04安装中文字体,解决导出图片乱码
  7. jQuery插件-json2.js
  8. C++ Primer Plus 第一章 预备知识
  9. LeetCode-94. 二叉树的中序遍历
  10. 简述osi参考模型各层主要功能_OSI参考模型各层主要功能
  11. Python 各种运算符 布尔运算 迭代器
  12. U3D-Assetbundle加载
  13. [加密]SSL/TLS原理详解
  14. 深度学习如何入门?知乎
  15. python如何输入特殊符号_python特殊符号转义
  16. Vue3使用echarts教程
  17. 豆瓣电影Top250数据爬取、数据分析及数据可视化
  18. Eclipse Neon EGit Integration gives Exception 401 Authorization Required
  19. MATLAB阶段性方程组,[转载]matlab 解方程组
  20. 大话深度残差网络(DRN)ResNet网络原理

热门文章

  1. java从本地下载pdf文件_java下载PDF文件
  2. linux设备驱动之 i2c设备驱动 at24c08驱动程序分析【全部地址的操作】
  3. C++向mysql批量插入数据
  4. python免费教程发布页
  5. java excel 导入试题
  6. stm32F103 模拟I2C mpu6050收到数据全为0,或者地址为209,104,0x68,0xD0的一些解决办法总结
  7. 用c语言双向循环链表,C语言实现双向循环链表
  8. IBM人工智能进入法律行业:推世界首位AI律师ROSS
  9. 2022PMP考试敏捷知识点(3)
  10. 七款顶级HTML5编辑器带你飞