在这里插入代码片# Spring Boot 3.5 脚手架里的 JWT + Redis 双轨会话,双端 token 隔离我是这么设计的
起因
公司要把手里几个项目沉淀成一套脚手架,第一件事就是把登录鉴权重新捋一遍。管理后台、小程序、H5 三端共用同一个后端,最怕的就是 token 串端------管理端登录的 token 拿去调小程序接口居然也能通,这种事故出过一次就够呛。当时排查半天,发现就是后端对"这 token 是从哪个端来的"完全没有概念,谁都能调谁的接口。
所以这次做脚手架(qkl-boot),鉴权这块我直接按"双端隔离 + 双轨会话"来设计。
为什么是 JWT + Redis 双轨
纯 JWT 的好处是无状态,服务随便横向扩,但代价是没法主动踢人。员工离职了、用户改密码了,token 在过期前照样能用,这不合适。所以我的做法是:
- JWT 负责"证明身份",载荷里塞
userId、tenantId、terminal(端标识)、jti(会话唯一 ID) - Redis 负责"会话生命周期",登录、登出、踢人、改密都是操作 Redis
JWT 签发核心代码就这一段:
java
public String createAccessToken(Long userId, String tenantId, TerminalTypeEnum terminal, String jti) {
long expire = properties.getToken().getAccessExpireSecond();
Date now = new Date();
Date exp = new Date(now.getTime() + expire * 1000);
return Jwts.builder()
.id(jti)
.claim("userId", userId)
.claim("tenantId", tenantId)
.claim("terminal", terminal.name())
.issuedAt(now)
.expiration(exp)
.signWith(secretKey())
.compact();
}
注意那个 terminal,这是双端隔离的关键。每次请求进来先解析 JWT,看 terminal 是 ADMIN 还是 MOBILE,再决定这个接口允不允许访问。管理端接口只认 ADMIN 的 token,天然就把串端问题挡在这一层。
Access + Refresh 双轨
access token 有效期短(默认几十分钟),refresh token 有效期长(天级)。但这里有个细节我踩过坑:管理端和移动端的 refresh 时长不能一样。管理员可能连着几天用后台,refresh 给长一点;移动端用户量大、安全要求高,refresh 短一点,逼着用户定期重新登录。
java
long refreshTtl = user.getTerminal() == TerminalTypeEnum.ADMIN
? properties.getToken().getAdminRefreshExpireSecond()
: properties.getToken().getMobileRefreshExpireSecond();
Redis 会话隔离
签发 token 时同时往 Redis 写一份会话,key 里带上 terminal 和 jti:
java
String loginKey = loginKey(user.getTerminal(), jti);
Map<String, Object> session = new HashMap<>();
session.put("userId", user.getUserId());
session.put("tenantId", user.getTenantId());
session.put("username", user.getUsername());
// ...
redisTemplate.opsForValue().set(loginKey, JSONUtil.toJsonStr(session), Duration.ofSeconds(refreshTtl));
登出就是删这个 key,踢人就是删这个 key,改密就是把所有 jti 的 key 全扫出来删掉。jti 每次 refresh 都会换新的,旧的 access 即使没过期,因为 Redis 里对应的 session 没了,也照样失效。
踩坑记录
- JWT 密钥太短 :密钥长度少于 32 字节直接抛异常,别用
"123456"这种去签名,HMAC 密钥太短等于裸奔。我在JwtService里加了硬校验,密钥不够直接拒绝,而不是等线上出了安全事件再补。 - 时钟偏移 :解析 token 的时候加
clockSkewSeconds(30)。服务器时钟和签发端时钟稍微差一点,不带这个,明明没过期的 token 会随机报"已过期",排查起来特别玄学。 - Redis 挂了对鉴权的影响:鉴权依赖 Redis,Redis 挂了要能快速失败而不是拖死整个服务,Redis 连接池超时参数调小,快速报错比卡死好排查。
后台的工作台和用户管理长这样:


总结
双轨会话 + Redis 会话隔离这套组合,本质就是用 JWT 的"快"配合 Redis 的"可控"。核心就三点:载荷带 terminal 做端隔离、jti 做会话指纹、Redis 做生命周期管理。脚手架源码在 qkl-boot 仓库里,配套的 uniapp 移动端在 job-uniapp,有需要可以直接参考。
项目源码:https://gitee.com/gzqkl/qkl-boot · https://gitee.com/gzqkl/job-uniapp