双 Token 三验证解决方案

news2024/11/15 9:10:37

更好的阅读体验 \huge{\color{red}{更好的阅读体验}} 更好的阅读体验


问题分析


以往的项目大部分解决方案为单 token:

  • 用户登录后,服务端颁发 jwt 令牌作为 token 返回
  • 每次请求,前端携带 token 访问,服务端解析 token 进行校验和鉴权

存在的问题:

  • 有效期设置问题:有效期设置需要对时间做平衡,不能太短也不能太长
  • 续期问题:一旦过期,用户必须重新登录,很难做无感刷新
  • 无状态问题:token 是无状态的,单 token 颁发后服务端无法主动使其失效

原理解析


这里引入双 token 机制:

  • accessToken:时间较短,一般为 5 分钟或者更短
  • refreshToken:时间较长,一般为 1 到 3 天

登录过程:

  • 用户携带用户名和密码登录
  • 服务端为其颁发 accessToken 和 refreshToken

三验证环节:

  • 一验证:前端请求携带 accessToken,验证是否过期,不过期放行,过期则进入第二个验证环节
  • 二验证:前端请求携带 refreshToken,验证是否过期,不过期进入第三个验证环节,过期则要求用户重新登录
  • 三验证:在 redis 种验证 refreshToken 是否存在,存在则颁发新的 accessToken 和 refreshToken 返回前端更新,将原来的 refreshToken 删除,再把新的 refreshToken 存入 redis

该机制的 UML 图如下:


最佳实践


生成 Token


基于 SpringCache 来操作 redis,利用 MD5 算法对 token 进行加密,防止其作为键的后缀存入时过长,导致”大KEY“的问题出现

public class CommonRedisConstants {
    public static class RedisKey {
        /**
         * refreshToken 前缀
         */
        public static final String REFRESH_TOKEN_PREFIX = "REFRESH_TOKEN_PREFIX_%s";
    }
}
@Resource
private StringRedisTemplate stringRedisTemplate;


// 生成 accessToken
private String createAccessToken(Map<String, Object> claims) {
    // 这里是利用 jjwt 编写的工具类方法,读者可以自行实现相关工具类
	return JwtUtils.generateAccessToken(claims);
}

// 生成 refreshToken 并存入 redis
private String createRefreshToken(Map<String, Object> claims) {
    String refreshToken = JwtUtils.generateRefreshToken(claims);
    // redisKey 的形式为固定前缀+md5转换的token
    String redisKey = String.format(CommonRedisConstants.RedisKey.REFRESH_TOKEN_PREFIX, MD5Util.generateMd5Str(refreshToken));
    // 设置有效期为 3 days
    this.stringRedisTemplate.opsForValue().set(redisKey, refreshToken, Duration.ofDays(3L));
    return refreshToken;
}

校验 Token


基于自定义注解和 Spring AOP 实现校验 token,并将解析后的信息存储到上下文

自定义的注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
}

AOP 切面:

@Aspect
@Component
@Slf4j
public class CurrentUserAspect {

    private final HttpServletRequest request;

    public CurrentUserAspect(HttpServletRequest request) {
        this.request = request;
    }

    @Before("@annotation(currentUser)")
    public void setUserContext(CurrentUser currentUser) {
        String token = request.getHeader("Authorization");
        if (token != null) {
            try {
                // 这里是利用 jjwt 编写的工具类方法,读者可以自行实现相关工具类
                Claims claims = JwtUtils.parseToken(token);
                // 这里是利用 ThreadLocal 存储用户信息到上下文,读者可以自行实现相关工具类
                UserContextUtil.set(claims);
            } catch (Exception e) {
                // token 解析失败后的逻辑
            }
        } else {
            // 请求头未携带 token 的逻辑
        }
    }

    // 方法执行完后释放资源,防止内存泄漏
    @After("@annotation(currentUser)")
    public void clearUserContext(CurrentUser currentUser) {
        UserContextUtil.clear();
    }

}

刷新 Token


前端调用刷新 token 后,服务端返回新的 accessToken 和 refreshToken:

@Data
@AllArgsConstructor
public class AdminLoginVO {
    private String accessToken;
    private String refreshToken;
}
public AdminLoginVO refreshLogin(String refreshToken) {
        // 校验 token 是否有效
        boolean isValidated = JwtUtils.validateToken(refreshToken);
        if (!isValidated) {
            // token 
        }

        /**
         * 校验 redis 里的 refreshToken 是否失效
         * 未失效:将 redis 里的 refreshToken 删除,重新颁发新的 accessToken 和 refreshToken
         * 已失效:重新登录
         */
        String redisKey = String.format(CommonRedisConstants.RedisKey.REFRESH_TOKEN_PREFIX, MD5Util.generateMd5Str(JwtUtils.preDecodeToken(refreshToken)));
        Boolean hasKey = this.stringRedisTemplate.hasKey(redisKey);
        if (ObjectUtil.notEqual(hasKey, Boolean.TRUE)) {
            // 原 token 过期或已经使用过的逻辑
        }
        // 删除原 token
        this.stringRedisTemplate.delete(redisKey);
        // 颁发新的 accessToken 和 refreshToken
        Claims claims = JwtUtils.parseToken(refreshToken);
        String accessToken = createAccessToken(claims);
        refreshToken = createRefreshToken(claims);
        return new AdminLoginVO(accessToken, refreshToken);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1970908.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

serial---- vulnhub打靶

1.新建虚拟机&#xff0c;虚拟硬盘使用vulnhub下载提供的虚拟硬盘文件 2.打开虚拟机&#xff0c;扫描网段&#xff0c;确定IP(或者arp -a) 3.发现没有robots.txt&#xff0c;以及一些常见admin,www.zip目录文件&#xff0c;尝试扫目录 拿另一个工具扫一下看看多了一个 4.发现备…

凸优化学习之旅

目录标题 专业名词MM算法CCP算法&#xff1a;代码说明 SCA算法&#xff1a;连续松弛梯度投影算法 分支定界搜索法凸问题辨别OA算法λ-representationADMM算法代码说明 BCD算法BCD&#xff08;Block Coordinate Descent&#xff09;代码示例与ADMM的区别总结 2024年5月6日15:15:…

2024 年 5 款顶级的免费和付费 PDF 编辑器个人评测

PDF 为企业、学校或一般用途提供了一种共享各种信息的便捷方式。您可以在笔记本电脑和智能手机上轻松查看 PDF 文档。但大多数图片查看器和 PDF 阅读器不允许您编辑 PDF。因此&#xff0c;当您想要修改 PDF 文件中的图像或文本时&#xff0c;您需要一个PDF 编辑器。 似乎没有太…

springboot高校勤工俭学平台-计算机毕业设计源码66824

摘 要 本研究基于Spring Boot企业框架&#xff0c;设计并实现了一款高校勤工俭学平台&#xff0c;包括首页、通知公告、新闻通知和岗位信息等功能模块。该平台旨在为高校学生提供便捷的勤工俭学信息发布与查询服务&#xff0c;促进校园内部劳动力资源的充分利用和高效管理。在研…

MSTR:智慧无处不在,可信任 AI 的崛起

|| 前言 商业智能&#xff08;BI&#xff09;的力量在于利用数据得出可行的洞察&#xff0c;从而做出更明智的数据驱动决策。从提升内部产品质量、运营和资源利用&#xff0c;到简化公共服务&#xff0c;BI应用非常多样化又具有影响力。 引入人工智能&#xff08;AI&#xff…

Vue+SpringBoot+SpringSecurity项目对于跨域的深度理解

随记&#xff08;可跳过&#xff09;&#xff1a;CodeMan在熬夜肝一周SpringSecurity学习的时候&#xff0c;总是报错&#xff0c;于是冥思苦想&#xff0c;选择了询问Ai&#xff0c;但是不论怎么设置权限&#xff0c;接口仍然无法按所设想的权限被调用&#xff0c;于是在今天的…

这“听说啊”的想法很美感

《澎湃新闻》今天在发表的长文《“第二个香港”即将诞生&#xff01;面积比香港大30倍&#xff0c;或成最大自由港》中称&#xff1a;“听说啊&#xff0c;2024年海南会在合适的时候启动全岛封关运作&#xff0c;这意味着海南要建成更自由、更开放的自由贸易港&#xff0c;咱们…

如何在 Debian 上安装运行极狐GitLab Runner?【二】

极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门面向中国程序员和企业提供企业级一体化 DevOps 平台&#xff0c;用来帮助用户实现需求管理、源代码托管、CI/CD、安全合规&#xff0c;而且所有的操作都是在一个平台上进行&#xff0c;省事省心省钱。可以一键安装极狐GitL…

亲子游戏 - 华为OD统一考试(D卷)

OD统一考试(D卷) 分值: 200分 题解: Java / Python / C++ 题目描述 宝宝和妈妈参加亲子游戏,在一个二维矩阵(N*N)的格子地图上,宝宝和妈妈抽签决定各自的位置,地图上每个格子有不同的糖果数量,部分格子有障碍物。 游戏规则是妈妈必须在最短的时间(每个单位时间只能走…

PythonPDF操作库之pdfminer使用详解

概要 在现代信息处理领域,PDF 文件是常见的文档格式之一。无论是在企业应用还是个人使用中,能够有效地提取和处理 PDF 文档内容是一项重要技能。pdfminer 是一个强大的 Python 库,专注于从 PDF 文件中提取文本和信息。本文将详细介绍 pdfminer 库,包括其安装方法、主要特性…

【JAVA设计模式】适配器模式——类适配器模式详解与案例分析

前言 在软件设计中&#xff0c;适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;旨在使不兼容的接口能够协同工作。它通过引入一个适配器类&#xff0c;帮助两个接口之间进行适配&#xff0c;使得它们能够互相操作。本文将详细介绍适配器模…

2006-2022年中国农村经营管理年报

2006-2022年中国农村经营管理年报 1、时间&#xff1a;2006-2022年 2、格式&#xff1a;2006-2014年为EXCEL&#xff0c;2015-2022年为PDF 3、说明&#xff1a;根据农村经营管理情况统计报表制度调查数据整理、编辑的。本资料系统收录了全国各省、自治区、直辖市农村集体经济…

Gartner发布2024年安全运营成熟度曲线:改变安全运营策略、能力和效果的23项技术发展趋势

安全运营技术和服务通过识别威胁、漏洞和暴露来保护 IT/OT 系统、云工作负载、应用程序和其他数字资产免受攻击。此技术成熟度曲线可帮助安全和风险管理领导者制定战略并提供安全运营能力和功能。 需要知道的 混合和远程工作实践不断发展&#xff0c;安全运营中心 (SOC) 团队支…

云计算 Logstash 配置管理

日志分析系统ELK 项目架构图 Logstash 是一个开源的、服务器端的数据收集引擎&#xff0c;与 Elasticsearch 和 Kibana 一起构成了 Elastic Stack&#xff08;之前称为 ELK Stack&#xff09;。Logstash 的主要功能是处理和转发数据&#xff0c;它可以从多种数据源收集数据&a…

Mamba+Transformer完美融合,效果炸裂!

因模型规模的扩展和需要处理的序列不断变长&#xff0c;transformer逐渐出现计算量激增、计算效率下降等问题,研究者们提出了Mamba—— 一种创新的线性时间序列建模方法&#xff0c;它结合了递归神经网络&#xff08;RNN&#xff09;和卷积神经网络&#xff08;CNN&#xff09;…

十七、Intellij IDEA2022.1.1下载、安装、激活

目录 &#x1f33b;&#x1f33b; 一、下载二、 安装三、激活 一、下载 官网下载地址 本地直接下载 目前Intellij IDEA的最新版本已经更新到了 2024.1.4&#xff0c;由于最新版本可能存在不稳定的问题&#xff0c;此处选择其他版本进行下载&#xff0c;此处以2022.1.1为例进行下…

Encoder-Decoder Model编码器-解码器模型

Encoder-Decoder编码器-解码器是一种深度学习模型&#xff0c;应用于图像处理、语音识别、自然语言处理等领域。主要由编码器和解码器两部分组成&#xff0c;这种结构能够处理序列到序列的任务。 编码器-解码器模型具备独特的双阶段处理&#xff0c;先对输入信息进行编码&#…

【C++】实验十五

题目&#xff1a; 1、求一元二次方程ax2bxc0的实根。如果方程没有实根&#xff0c;则利用异常处理处理机制输出有关警告信息 2、学校的人事部门保留了有关学生的部分数据&#xff08;学号、姓名、年龄、住址&#xff09;。教务部门也保留了学生的另一些数据&#xff08;学号、…

最新TomatoIDC开源虚拟主机销售系统源码/有插件系统模块+模版系统

源码简介&#xff1a; 最新TomatoIDC开源虚拟主机销售系统源码&#xff0c;它有一个方便扩展的插件和模版系统模块&#xff0c;使用实用。 TomatoIDC&#xff0c;一款遵循GPL3.0协议的开源虚拟主机销售系统&#xff0c;不仅有着可以轻松扩展的插件系统和模版系统&#xff0c;…

神奇的进度条!水缸进度动画效果怎么实现的?

最近看到一个非常有趣的动画效果&#xff1a;水波进度动画&#xff0c;想了一下实现思路&#xff0c;分享给大家~ 效果如下 图片 图片 基本组件代码 先把最基础的组件代码样式写出来&#xff0c;其实无非就是四个部分&#xff1a; 1、圆形水缸 2、水波 2、百分比数字 3、…