摘要:三持久化令牌源码解析现在壹哥已经带各位实现了基于持久化令牌方案的自动登录,你可能很好奇,内部到底是怎么进行实现的呢接下来请跟着我一起分析其实现原理和底层源码吧。
在上一章节中,一一哥 带各位基于散列加密方案实现了自动登录,并且给各位介绍了散列加密算法,其实还有另一种自动登录的实现方案,也就是基于持久化令牌方案来进行实现。接下来请跟 一一哥 学习这种方案该怎么实现吧。
有的小伙伴会问,既然我们要基于持久化令牌来实现自动登录,那啥是持久化令牌啊?所以 一一哥 先给大家做个概念解释。
所谓的持久化令牌的实现方案,其实在交互上与我们前面章节讲的散列加密方案一致,只不过是在用户勾选Remember-me之后,将生成的token令牌发送到用户浏览器,并在用户下次访问系统时读取该令牌进行认证。不同的是,持久化令牌方案采用了更加严谨的安全性设计,也就是安全性更好一些。
在持久化令牌方案中,最核心的是series和token两个值,这两个值都是用MD5散列计算生成的随机字符串。不同的是,series仅在用户使用密码重新登录时更新,而 token 会在每一个新的session会话中都重新生成。
我们前面已经学习了基于散列算法的自动登录方案了,为啥还要再学习持久化方案呢?肯定是因为它有独特之处吧。
首先,持久化令牌方案 避免了散列加密方案中,一个令牌可以同时在多端登录的问题,这是因为每个session会话都会引发token的更新,即每个token仅支持单实例登录。
其次,自动登录不会导致series变更,但每次自动登录都需要同时验证 series和 token两个值,所以这样的设计会更安全。因为当该令牌还未使用过自动登录就被盗取时,系统会在非法用户验证通过后刷新 token 值,此时在合法用户的浏览器中,该token值已经失效。当合法用户使用自动登录时,由于该series对应的 token 不同,系统可以推断该令牌可能已被盗用,从而做一些处理。例如,清理该用户的所有自动登录令牌,并通知该用户可能已被盗号等。
了解了持久化令牌的概念和优点之后,接下来就跟着 壹哥 进行代码实现吧。
在持久化令牌方案中,是要把令牌进行持久化保存的,那么把令牌持久化到哪里去呢?我们首选数据库!
所以我们首先创建一张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进行令牌信息的查询等操作。
请按之前的方式创建一个新的项目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; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Override protected 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(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
创建applicaiton.yml文件并在其中关联配置自己的数据库,我是在该数据库中创建了persistent_logins表。
spring: datasource: url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT username: root password: syc security: remember-me: key: yyg
创建一个项目入口类(代码略),然后把项目启动起来。
这时候,我们只需要在登录页面中输入 用户名和密码,勾选“记住我”功能之后,Spring Security就会生成一个持久化令牌,在这个令牌中就保存了当前登陆的用户信息,该令牌信息会被自动持久化存储到persistent_logins表中。
在我们的persistent_logins表中,你会看到保存的自动登录的用户信息。
这样我们就实现了基于持久化令牌的自动登录方案。
各位可以参考下图,结合前面的章节,自行实现。
现在 壹哥 已经带各位实现了基于持久化令牌方案的自动登录,你可能很好奇,Spring Security内部到底是怎么进行实现的呢?接下来请跟着我一起分析其实现原理和底层源码吧。
我先给大家分析一下持久化令牌的实现原理。
当我们经过自动登录之后,就可以在persistent_logins表的series和token字段中看到,分别保存了对应的信息。
在自动登录成功后,也会在remember-me中保存用户的cookie信息,我们可以利用Base64在线编解码工具对该cookie信息进行解码。Base64在线编解码工具直接百度即可找到。
我们看到解码出来的结果以 “:” 分割为前后两部分,冒号前面的部分是我们保存在数据库里的series字段的内容,冒号后面的部分是token字段存储的内容。
另外在persistent_logins表中还存储了token的过期时间,以后用户每次登陆成功后,都会通过用户名确认该令牌的身份,通过对比token可以得知该令牌是否有效。并且通过上一次自动登录的时间也可以知道该令牌是否已过期,并在完整校验通过之后生成新的token。
我给大家介绍完持久化令牌的基本实现原理后,再给各位剖析一下该方案的底层源码。
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字段内容。
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);}
JdbcTokenRepositoryImpl是对PersistentTokenRepository接口的具体实现,该实现类的实现方法其实很简单,就是定义了5个SQL语句,分别是建表语句,以及对持久化令牌表的增删改查操作的SQL语句,源码如下:
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository { // ~ 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 getTokenBySeries 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 createNewToken */ public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)"; /** The default SQL used by updateToken */ public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?"; /** The default SQL used by removeUserTokens */ public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?"; ......
然后就是利用Spring自带的JdbcTemplate来实现对“persistent_logins”表的增删改查操作。
我们再看看另一个带有记住我功能的持久化令牌服务类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 cookie throw new RememberMeAuthenticationException( "No persistent token found for series id: " + presentedSeries); } // We have a match for this user/series combination if (!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()); }
以上源码的实现逻辑如下:
- 首先从前端传来的 cookie 中解析出 series 和 token;
- 根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例;
- 如果查出来的 token 和前端传来的 token 不相同,说明账号可能被人盗用(别人用你的令牌登录之后,token 会变)。此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
- 接下来校验 token 是否过期;
- 构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token(这就是我们文章开头说的,新的会话都会对应一个新的 token);
- 将新的令牌重新添加到 cookie 中返回;
- 根据用户名查询用户信息,再走一波登录流程。
至此,我已经给大家详细讲解了基于散列加密方案和持久化令牌方案的自动化登录实现,这里我对两种方案做一个简单对比。
散列加密方案和持久化令牌方案,这两种方案都是把信息存储在cookie中,所以都有被盗取用户身份信息的可能性,当然持久化令牌方案的安全性更高一些。但是如果要你追求最安全的方式,那就尽量不要实现自动登录功能,所以我们要在用户体验和提高安全性之间选择平衡点。
如果我们一定要实现自动登录功能,可以限制以cookie身份登录时的部分执行权限,比如在修改密码、修改邮箱(防止找回密码)、查看隐私信息(如完整的手机号码、银行卡号等)时,我们可以进一步校验用户的登录密码,或者设置独立密码来做二次校验,以提高安全性。
我们上面虽然讲解了2种自动登录的实现方案,但是依然存在用户身份被盗用的问题,这个问题其实是很难完美解决。那么我们能做的,只能是当发生用户身份被盗用这样的事情时,将损失降低到最小。因此,我们采用二次校验来增强项目的安全性。
我们在上面项目的基础上,添加一个新的测试接口/remember。
@RestControllerpublic 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功能"; }}
在SecurityConfig类中对/remember/hello接口配置二次校验,主要是对该接口利用rememberMe()方法进行配置。
@EnableWebSecurity(debug = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${spring.security.remember-me.key}") private String rememberKey; @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; @Override protected 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(); } @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } }
当我们访问"/remember/hello"接口时,需要二次校验才能访问,即该接口需要开启remember-me功能才能访问:
至此,壹哥 就结合着源码和底层原理,给大家讲解了基于持久化令牌方案实现了自动登录,并且在本案例中给大家介绍了持久化令牌的概念及实现原理,你掌握的怎么样呢?请在评论区给 一一哥 留言,说说你的感受吧!下一篇文章中,壹哥 会给各位讲解 如何在Spring Security环境下实现注销登录,敬请期待哦!
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/122066.html
摘要:框架具有轻便,开源的优点,所以本译见构建用户管理微服务五使用令牌和来实现身份验证往期译见系列文章在账号分享中持续连载,敬请查看在往期译见系列的文章中,我们已经建立了业务逻辑数据访问层和前端控制器但是忽略了对身份进行验证。 重拾后端之Spring Boot(四):使用JWT和Spring Security保护REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:指南无论你正在构建什么,这些指南都旨在让你尽快提高工作效率使用团队推荐的最新项目版本和技术。使用进行消息传递了解如何将用作消息代理。安全架构的主题指南,这些位如何组合以及它们如何与交互。使用的主题指南以及如何为应用程序创建容器镜像。 Spring 指南 无论你正在构建什么,这些指南都旨在让你尽快提高工作效率 — 使用Spring团队推荐的最新Spring项目版本和技术。 入门指南 这些...
摘要:截至年月日,将网站标记为不安全。管理密码使用密码哈希以纯文本格式存储密码是最糟糕的事情之一。是中密码哈希的主要接口,如下所示提供了几种实现,最受欢迎的是和。 Spring Boot大大简化了Spring应用程序的开发。它的自动配置和启动依赖大大减少了开始一个应用所需的代码和配置量,如果你已经习惯了Spring和大量XML配置,Spring Boot无疑是一股清新的空气。 Spring ...
摘要:开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章系列处理登录请求前后端分离一使用完美处理权限问题前后端分离二使用完美处理权限问题前后端分离三中密码加盐与中异常统一处理 开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章! Spring Boo...
摘要:开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章系列处理登录请求前后端分离一使用完美处理权限问题前后端分离二使用完美处理权限问题前后端分离三中密码加盐与中异常统一处理 开公众号差不多两年了,有不少原创教程,当原创越来越多时,大家搜索起来就很不方便,因此做了一个索引帮助大家快速找到需要的文章! Spring Boo...
阅读 2318·2023-04-26 01:44
阅读 3257·2021-10-11 10:56
阅读 1877·2021-09-26 09:47
阅读 2273·2021-09-10 10:50
阅读 1194·2019-08-30 15:56
阅读 1933·2019-08-30 15:44
阅读 390·2019-08-29 11:14
阅读 3318·2019-08-26 11:56