爆破专栏丨Spring Security系列教程之基于持久化令牌方案实现自动登录_spring security账户登录暴破

最后

现在其实从大厂招聘需求可见,在招聘要求上有高并发经验优先,包括很多朋友之前都是做传统行业或者外包项目,一直在小公司,技术搞的比较简单,没有怎么搞过分布式系统,但是现在互联网公司一般都是做分布式系统。

所以说,如果你想进大厂,想脱离传统行业,这些技术知识都是你必备的,下面自己手打了一份Java并发体系思维导图,希望对你有所帮助。

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

创建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


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 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 <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 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());
 }


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

  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";
    }



# 面试准备+复习分享:

> 为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

![秋招|美团java一面二面HR面面经,分享攒攒人品](https://img-blog.csdnimg.cn/img_convert/3a9c1113f63e13dc899bad6d1e2a9787.webp?x-oss-process=image/format,png)



> **本文已被[CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)收录**

**[需要这份系统化的资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

:

> 为了应付面试也刷了很多的面试题与资料,现在就分享给有需要的读者朋友,资料我只截取出来一部分哦

[外链图片转存中...(img-pewaUWaQ-1715692828743)]



> **本文已被[CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)收录**

**[需要这份系统化的资料的朋友,可以点击这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值