【SpringBoot】最佳实践——JWT结合Redis实现双Token无感刷新

JWT概览

JWT概念

JWT是全称是JSON WEB TOKEN,是一个开放标准,用于将各方数据信息作为JSON格式进行对象传递,可以对数据进行可选的数字加密,可使用RSAECDSA进行公钥/私钥签名。JWT最常见的使用场景就是缓存当前用户登录信息,当用户登录成功之后,拿到JWT,之后用户的每一个请求在请求头携带上Authorization字段来辨别区分请求的用户信息。且不需要额外的资源开销。

JWT组成部分

JWT通常由一个头部(Header)、一个负载(Payload)和一个签名(Signature)三部分组成,这三部分之间用点(.)分隔。所以,一个完整的JWT看起来像这样:

xxxxx.yyyyy.zzzzz

下面我们来详细解析每一部分:

头部(header)

头部用于描述令牌的元数据,通常包含令牌的类型(即JWT)和所使用的签名算法(如HMAC SHA256)。

  • typ:表示令牌的类型,JWT令牌统一写为"JWT"。
  • alg:表示签名使用的算法,例如HMAC SHA256或RSA。

头部信息会被进行Base64编码,形成JWT的第一部分。

{  "typ": "JWT",  "alg": "HS256"  
}

负载(payload)

负载包含了JWT的声明,即传递的数据,这些数据通常包括用户信息和其他相关数据。声明有三种类型:注册的声明、公共的声明和私有的声明。

  • 注册的声明:这是一组预定义的声明,它们不是强制的,但是推荐使用,以提供一组有用的、可互操作的声明。如:iat(签发时间)、exp(过期时间)、aud(接收方)、sub(用户唯一标识)、jti(JWT唯一标识)等。
  • 公共的声明:可以定义任何名称,但应避免与注册的声明名称冲突。
  • 私有的声明:是提供者和消费者之间共同定义的声明。

负载同样会被Base64编码,形成JWT的第二部分。

{  "sub": "1234567890",  "name": "John Doe",  "jti": "unique-jwt-id","admin": true  
}

签名(signature)

签名将头部和负载用指定的算法进行签名,验证JWT的真实性和完整性。当接收者收到JWT时,他们可以使用相同的算法和密钥(对于HMAC算法)或使用公钥(对于RSA或ECDSA算法)验证签名。如果两个签名匹配,那么JWT就是有效的。

签名的过程如下:

  • 先将Base64编码后的头部和负载数据用点号(.)连接起来。
  • 使用指定的签名算法(例如,HMAC SHA256、RSA、ECDSA)和密钥对连接后的字符串进行签名。
  • 将生成的签名部分进行Base64Url编码,形成JWT的第三部分。

签名部分也是经过Base64Url编码的,形成JWT的第三部分。

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

注意:虽然Base64Url编码不是加密方式,但它可以确保JWT的字符串格式是紧凑的,并且容易在URL、POST参数或HTTP头部中传输。

技术方案设计

单点登录(SSO)

  • 单点登录(Single Sign-On, SSO) 是一种身份认证机制,允许用户通过一次登录即可访问多个相互信任的应用系统,而无需重复输入认证信息。

双Token机制

  • AccessToken
    • 短期有效(如30分钟),用于接口访问。
    • 客户端每次请求API时携带。
    • 不持久化存储,仅通过签名验证合法性。
  • RefreshToken
    • 用于获取新的Access Token,有效期长(如3天)。
    • 仅在刷新令牌时传输,不直接访问业务API。
    • 必须持久化存储(如Redis),服务端可主动使其失效。
  • 签名算法:使用RSA非对称加密算法,减少内存占用,防止篡改,并方便后续拓展子系统。

无感刷新Token

  • 客户端将由于AccesssToken过期失败的请求存储起来,携带RefreshToken成功刷新Token后,将存储的失败请求重新发起,以此达到用户无感的体验。
  • 服务端根据RefreshToken解析出userId和deviceId后,去Redis中查询存储的RefreshToken并进行比对,成功后生成新的AT和RT并返回

多端会话管理

  • 同一账号在不同设备登录时,为每个设备生成独立的RefreshToken。
  • Redis中以 userId:deviceId为键存储RefreshToken,过期时间设置为RefreshToken的过期时间。

废弃令牌移除

  • Redis中以 blacklist:token 为键存储AccessToken黑名单,键值对的过期时间设置为AccessToken的剩余有效期。
  • 直接删除Redis中的RefreshToken。

最佳实践

总体流程

JWT工具类

// JWT工具类
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.access.expiration}")private long accessExpiration;@Value("${jwt.refresh.expiration}")private long refreshExpiration;public String generateAccessToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + accessExpiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}public String generateRefreshToken(String username) {return Jwts.builder().setSubject(username).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + refreshExpiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}public boolean validateToken(String token) {try {Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {return false;}}public String getUsernameFromToken(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();}
}

Redis服务类

// Redis服务类
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;@Service
public class RedisService {private final StringRedisTemplate redisTemplate;public RedisService(StringRedisTemplate redisTemplate) {this.redisTemplate = redisTemplate;}public void saveRefreshToken(String refreshToken, String username) {redisTemplate.opsForValue().set("refresh_token:" + refreshToken, username, 7, TimeUnit.DAYS);}public boolean isRefreshTokenValid(String refreshToken) {return redisTemplate.hasKey("refresh_token:" + refreshToken);}public void deleteRefreshToken(String refreshToken) {redisTemplate.delete("refresh_token:" + refreshToken);}public void addToBlacklist(String accessToken, long expirationMs) {redisTemplate.opsForValue().set("blacklist:" + accessToken, "invalid", expirationMs, TimeUnit.MILLISECONDS);}public boolean isInBlacklist(String accessToken) {return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + accessToken));}
}

Filter过滤器

// JWT过滤器
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;public class JwtFilter extends OncePerRequestFilter {private final JwtUtil jwtUtil;private final RedisService redisService;public JwtFilter(JwtUtil jwtUtil, RedisService redisService) {this.jwtUtil = jwtUtil;this.redisService = redisService;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = resolveToken(request);if (token == null) {filterChain.doFilter(request, response);return;}if (redisService.isInBlacklist(token)) {sendError(response, "Token invalid");return;}if (jwtUtil.validateToken(token)) {String username = jwtUtil.getUsernameFromToken(token);UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, null);SecurityContextHolder.getContext().setAuthentication(authentication);filterChain.doFilter(request, response);} else {sendError(response, "Token expired or invalid");}}private String resolveToken(HttpServletRequest request) {String bearerToken = request.getHeader("Authorization");if (bearerToken != null && bearerToken.startsWith("Bearer ")) {return bearerToken.substring(7);}return null;}private void sendError(HttpServletResponse response, String message) throws IOException {response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write(message);response.getWriter().flush();}
}

Controller类

// 控制器类
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;@RestController
public class AuthController {private final JwtUtil jwtUtil;private final RedisService redisService;public AuthController(JwtUtil jwtUtil, RedisService redisService) {this.jwtUtil = jwtUtil;this.redisService = redisService;}@PostMapping("/login")public ResponseEntity<?> login(@RequestBody LoginRequest request) {// 这里应添加用户认证逻辑(如数据库验证)String username = request.getUsername();String accessToken = jwtUtil.generateAccessToken(username);String refreshToken = jwtUtil.generateRefreshToken(username);redisService.saveRefreshToken(refreshToken, username);return ResponseEntity.ok(new TokenResponse(accessToken, refreshToken));}@PostMapping("/refresh")public ResponseEntity<?> refreshToken(@RequestBody RefreshRequest request) {String refreshToken = request.getRefreshToken();if (!redisService.isRefreshTokenValid(refreshToken)) {return ResponseEntity.status(401).body("Invalid refresh token");}String username = jwtUtil.getUsernameFromToken(refreshToken);String newAccessToken = jwtUtil.generateAccessToken(username);String newRefreshToken = jwtUtil.generateRefreshToken(username);// 替换旧refreshTokenredisService.deleteRefreshToken(refreshToken);redisService.saveRefreshToken(newRefreshToken, username);// 将旧accessToken加入黑名单(可选)// long expiration = jwtUtil.getExpirationFromToken(refreshToken).getTime() - System.currentTimeMillis();// redisService.addToBlacklist(refreshToken, expiration);return ResponseEntity.ok(new TokenResponse(newAccessToken, newRefreshToken));}// DTO类private static class LoginRequest {private String username;private String password;// getters/setters}private static class RefreshRequest {private String refreshToken;// getters/setters}private static class TokenResponse {private final String accessToken;private final String refreshToken;// constructor/getters}
}

问题解析

相比单Token的优势

  • 高安全性:用户请求仅携带过期时间较短的AccessToken,即使令牌泄露,风险时间窗口也较小;用户仅在请求刷新Token时携带RefreshToken
  • 长会话:RefreshToken一般设置较长的过期时间,只要RT不过期用户就无需重复登录

引入Redis的作用

  • 方便状态管理:如果不存在Redis,用户登出后只能等待Token过期才能被动失效,增加Token暴露风险;通过在Redis中引入黑名单blacklist,可以使得Token主动失效
  • 多端会话管理:通过以 userId:deviceId为键存储不同设备的Token,实现同用户多端登录。通过删除对应设备的键并加上黑名单,可以主动剔出对应设备
  • 分布式一致性:若使用本地内存存储 RT,在分布式多节点架构中,各节点无法共享 RT 状态,导致用户在一个节点退出后,其他节点仍认为 RT 有效。Redis作为集中式存储,确保所有服务节点访问同一份 RT 数据,状态一致。

保证Token安全性

  • 存储安全性:AT存于内存或 SessionStorage(页面关闭失效),而RT通过 HttpOnly; Secure; SameSite=Strict Cookie 存储(XSS攻击无效)。
  • 传输安全性:开启HTTPS,防止中间人攻击(篡改、伪造和窃听);AT通过 Authorization: Bearer {token} 请求头传递,避免 URL 参数(防日志泄露),而RT通过 Cookie(标记 HttpOnly; Secure)传输。

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

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

相关文章

面试系列|蚂蚁金服技术面【1】

哈喽&#xff0c;大家好&#xff01;今天分享一下蚂蚁金服的 Java 后端开发岗位真实社招面经&#xff0c;复盘面试过程中踩过的坑&#xff0c;整理面试过程中提到的知识点&#xff0c;希望能给正在准备面试的你一些参考和启发&#xff0c;希望对你有帮助&#xff0c;愿你能够获…

eBPF 实时捕获键盘输入

eBPF 实时捕获键盘输入 本文将带你一步步实现一个基于eBPF kprobe的键盘记录功能&#xff0c;通过Go语言配合libbpfgo&#xff0c;你将学会如何无损地监控系统键盘输入&#xff0c;并从中获取实时数据&#xff0c;进一步提高系统安全和监控能力。 1. 说明 本文属于专栏 Go语言…

APB-清华联合腾讯等机构推出的分布式长上下文推理框架

APB (Accelerating Distributed Long-Context Inference by Passing Compressed Context Blocks acrossGPUs)是清华大学等机构联合提出的分布式长上下文推理框架。通过稀疏注意力机制和序列并行推理方式&#xff0c;有效解决了大模型处理长文本时的效率瓶颈。APB采用更小的Anch…

数据库分库分表介绍

分库分表是解决数据库性能瓶颈的常用技术手段&#xff0c;主要用于应对数据量过大、读写压力过高的问题。通过将数据分散到多个数据库或表中&#xff0c;可以提高系统的扩展性和性能。 1. 分库分表的核心概念 &#xff08;1&#xff09;分库 定义&#xff1a;将数据分散到多个…

#mapreduce打包#maven:could not resolve dependencies for project

打包报错&#xff1a; #报错信息&#xff1a; [ERROR] Failed to execute goal on project mapreduce_teacher1: Could not resolve dependencies for project org.example:mapreduce_teacher1:jar:1.0-SNAPSHOT: Failed to collect dependencies at org.apache.hive:hive-exe…

Rabit

之前发过rabit了&#xff0c;所以这里不再赘述&#xff0c;讲讲原理 在线Rabbit加密 | Rabbit解密- 在线工具 (sojson.com) rabbit加密原理 Rabbit加密算法是一种流密码算法&#xff0c;由Daniel J. Bernstein设计&#xff0c;并被广泛用于多种加密和安全通信应用中。它的设…

【A2DP】深入解读A2DP中通用访问配置文件(GAP)的互操作性要求

目录 一、模式支持要求 1.1 发现模式 1.2 连接模式 1.3 绑定模式 1.4 模式间依赖关系总结 1.5 注意事项 1.6 协议设计深层逻辑 二、安全机制&#xff08;Security Aspects&#xff09; 三、空闲模式操作&#xff08;Idle Mode Procedures&#xff09; 3.1 支持要求 …

模型蒸馏系列——开源项目

推荐项目&#xff1a;MiniMind&#xff08;低成本全流程训练框架&#xff09; GitHub&#xff1a;https://github.com/jingyaogong/minimind 核心特性&#xff1a;完整实现从数据清洗到模型部署的全流程&#xff0c;支持单卡低成本训练&#xff0c;代码全透明&#xff0c;适合…

【软考-架构】13.1、软件架构概述-构件技术

✨资料&文章更新✨ GitHub地址&#xff1a;https://github.com/tyronczt/system_architect 文章目录 ✨【重点】系统架构设计软件架构概述软件架构设计与生命周期构件&#x1f31f;软件架构风格数据流风格调用/返回风格独立构件风格虚拟机风格仓库风格闭环控制风格C2体系结…

《Android启动侦探团:追踪Launcher启动的“最后一公里”》

1. 开机仪式的“黑屏悬案” 当Android设备完成开机动画后&#xff0c;某些产品会陷入诡异的“黑屏时刻”——仿佛系统在玩捉迷藏。此时&#xff0c;**Launcher&#xff08;桌面&#xff09;**就是躲猫猫的主角。我们的任务&#xff1a;揪出Launcher何时完成启动&#xff0c;终…

Redis事务与管道

Redis事务 可以一次执行多个命令&#xff0c;本质是一组命令的集合。一个事务中的所有命令都会序列化&#xff0c;按顺序地串行执行而不会被其他命令插入&#xff0c;不许加塞。 一个队列中&#xff0c;一次性、顺序性、排他性的执行一系列命令。 Redis事务VS数据库事务 常用…

掌握这些 UI 交互设计原则,提升产品易用性

在当今数字化时代&#xff0c;用户对于产品的体验要求越来越高&#xff0c;UI 交互设计成为决定产品成败的关键因素之一。一个易用的产品能够让用户轻松、高效地完成各种操作&#xff0c;而实现这一目标的核心在于遵循一系列科学合理的 UI 交互设计原则。本文将详细阐述简洁性、…

Alembic 实战指南:快速入门到FastAPI 集成

一、快速开始 1.1 简介 Alembic 是一个基于 SQLAlchemy 的数据库迁移工具&#xff0c;主要用于管理数据库模式&#xff08;Schema&#xff09;的变更&#xff0c;例如新增表、修改字段、删除索引等&#xff0c;确保数据库结构与应用程序的 ORM 模型保持一致。 Alembic 通过版…

LRU(最近最少使用)算法实现

核心思想与基本思路 LRU&#xff08;Least Recently Used&#xff09;算法是一种缓存淘汰策略&#xff0c;其核心思想是淘汰最近最少使用的数据。 最近使用原则&#xff1a;最近被访问的数据在未来被访问的概率更高&#xff0c;因此应保留在缓存中。淘汰机制&#xff1a;当缓…

现在有分段、句子数量可能不一致的中英文文本,如何用python实现中英文对照翻译(即每行英文对应相应的中文)

以下是处理分段且中英文句子数量可能不一致的文本的Python实现方案&#xff0c;包含分句、翻译和对齐功能&#xff1a; from googletrans import Translator import redef split_paragraphs(text):"""按空行分割段落并清洗"""return [p.strip()…

C语言每日一练——day_8

引言 针对初学者&#xff0c;每日练习几个题&#xff0c;快速上手C语言。第八天。&#xff08;连续更新中&#xff09; 采用在线OJ的形式 什么是在线OJ&#xff1f; 在线判题系统&#xff08;英语&#xff1a;Online Judge&#xff0c;缩写OJ&#xff09;是一种在编程竞赛中用…

基础知识《Redis解析》

Redis 详细解析与介绍 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的高性能键值对&#xff08;Key-Value&#xff09;数据库&#xff0c;支持多种数据结构&#xff08;如字符串、哈希、列表、集合等&#xff09;&#xff0c;广泛应用于缓存、消息队列、…

区跨链知识和概念

1、以太坊 Geth 源码解析 Geth&#xff08;Go Ethereum&#xff09;是以太坊官方提供的 Go 语言实现的客户端&#xff0c;广泛用于以太坊全节点运行、挖矿、DApp 开发等。理解 Geth 的源码有助于掌握以太坊区块链底层逻辑&#xff0c;如区块同步、EVM 执行、P2P 交互等。 2、…

Vue 计算属性与 Data 属性同名问题深度解析

文章目录 1. 问题背景与核心概念1.1 Vue 响应式系统架构1.2 核心概念定义 2. 同名问题的技术分析2.1 同名场景示例2.2 问题发生机制 3. 底层原理剖析3.1 Vue 初始化流程3.2 响应式系统关键代码 4. 问题解决方案4.1 最佳实践建议4.2 错误处理机制 5. 性能影响分析5.1 递归调用性…

Mybatis——基础操作、动态SQL

目录 一.基础操作 1.删除 2.新增 3.更新 4.查询 5.XML映射文件 二、动态SQL 1.<if> 2.<where> 3.<set> 4.<foreach> 5.<sql> 6.<include> 一.基础操作 1.删除 参数占位符&#xff1a; 注意&#xff1a; #{...}相比于${...}…