当前位置: 首页 > news >正文

JWT原理分析

JWT 认证完整链路——从登录到退出的每一步,都有一个真实项目在跑

我做过一个政务系统的认证模块,Java 从零实现了一套 JWT 认证——双 Token、黑名单、Cookie 和 Header 双通道提取、用户信息缓存、权限鉴权。这篇文章拆开这个模块的完整源码,从 Token 结构到每条请求走过的每一步,每一步都给出来源和原因。

文章目录

  • JWT 认证完整链路——从登录到退出的每一步,都有一个真实项目在跑
    • 一、为什么不用 Session
    • 二、Token 结构——三段 Base64 拼起来的不只是一串字
    • 三、生成 Token——两个方法,两种生命周期
    • 四、refreshToken 换发——Token 时效的权衡
    • 五、验证 Token——一行 parse 背后的三步
    • 六、完整请求链路——一条请求从进来到出去的 7 步
    • 七、黑名单——无状态的代价和补救
    • 八、Token 的两个通道——Header 和 Cookie 各自的用途
    • 九、认证和鉴权是两件事
    • 十、配置
    • 十一、结语

一、为什么不用 Session

Session 是服务端状态。用户登录后,服务端在内存或 Redis 里存一份session_id → 用户信息的映射,浏览器通过 Cookie 回传 session_id。这个模型在单服务器时代够用。

但政务系统有几个特点:

  • 多节点部署——如果走 Session,要么做 sticky session(绑定到固定节点,那台挂了全掉),要么做 session 共享(引入 Redis 集中存储,单点故障风险)
  • Cookie 跨域限制——Cookie 的 domain 绑定了一个域名。如果系统有多个子域名(app1.gov.cnapp2.gov.cn),同域名下的 Cookie 不能跨子域共享——而 Session 依赖 Cookie
  • 前后端分离——前端可能是独立的 Vue/React 应用,跟后端不同域。Session 的 Cookie 在同源策略下传不到后端

JWT 解决了这三个问题:Token 是自包含的——用户信息签名后放在 Token 里,服务端不存状态。任何节点拿到 Token 用同一个密钥验签就能还原用户身份。Token 通过 Authorization Header 传,不受 Cookie 同源策略限制。

代价:Token 一旦签发就无法主动失效(不额外存储状态的话)。这个代价引出了接下来的"双 Token + 黑名单"设计。


二、Token 结构——三段 Base64 拼起来的不只是一串字

一个 JWT Token 长这样:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiJ9.HN_sWbXG-8_zpHjz5-TGKl06VfnKnCpN6A9TknR8VFs

用句号拆开是三段:

Header → eyJhbGciOiJIUzI1NiJ9 Payload → eyJzdWIiOiJhZG1pbiJ9 Sig → HN_sWbXG-8_zpHjz5-TGKl06VfnKnCpN6A9TknR8VFs

Base64 解码前两段:

Header:

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

Payload:

{"sub":"admin","psnId":"admin","psnName":"管理员","unitId":"U001","type":"access","iss":"browise","iat":1781683991,"exp":1781691191}

Signature 的计算方式:

HMAC-SHA256( Base64(Header) + "." + Base64(Payload), secret )

关键理解:Payload 是明文 Base64,任何人都能解出来看。Token 的安全性不靠加密——靠签名。没有 secret 的人可以读到 Payload 里的psnId=admin——但他改不了。他如果把psnId改成superadmin然后重算 Base64——签名就对不上了——服务端一验就拒绝。

所以 JWT 的设计哲学是:Payload 可以公开看,但不能改。


三、生成 Token——两个方法,两种生命周期

登录成功后生成两个 Token。

accessToken(2 小时):

// 来源: JwtUtil.javapublicStringgenerateAccessToken(StringpsnId,StringpsnName,StringunitId){Map<String,Object>claims=newHashMap<>();claims.put("psnId",psnId);claims.put("psnName",psnName);claims.put("unitId",unitId);claims.put("type","access");// 标记类型returnJwts.builder().setClaims(claims).setSubject(psnId).setIssuer("browise").setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+7200000))// 2小时.signWith(SignatureAlgorithm.HS256,secret).compact();}

refreshToken(7 天):

publicStringgenerateRefreshToken(StringpsnId){Map<String,Object>claims=newHashMap<>();claims.put("psnId",psnId);claims.put("type","refresh");// 标记类型——和 access 区分returnJwts.builder().setClaims(claims).setSubject(psnId).setIssuer("browise").setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+604800000))// 7天.signWith(SignatureAlgorithm.HS256,secret).compact();}

两个 Token 的区别不只是过期时间——type字段在验证时用来区分:type=refresh的 Token 不能当 accessToken 用——它只能用来换新 Token。如果你把 refreshToken 放进 Authorization Header 调业务接口——Filter 层检查 type 不是 “access” 直接拒绝。


四、refreshToken 换发——Token 时效的权衡

为什么需要两个 Token?

accessToken 只有 2 小时,因为它是高频传输的——每次请求都带着。如果 accessToken 有效期太长(比如 7 天),一旦泄露,攻击者有 7 天时间可以任意调用接口——而 JWT 本身不存储状态,你没法主动让它失效。

但 2 小时太短——用户在一天之内可能被强制登出好几次,体验极差。所以用一个 7 天的 refreshToken 来解决:accessToken 过期后,前端拿 refreshToken 去/auth/refresh换一个新的 accessToken——不用重新登录。

accessToken 过期(2小时) │ ├─ 前端请求 /api/xxx → 401 │ ├─ 前端拦截 401,拿 refreshToken 调 /auth/refresh │ ├─ 验证 refreshToken 签名 + 有效期 │ ├─ 检查 refreshToken 是否在黑名单(logout 时加入) │ ├─ 通过 → 生成新的 accessToken 返回 │ └─ 不通过 → 前端跳转登录页 │ └─ 前端拿到新 accessToken → 重试原请求

refreshToken 是 httpOnly 的 Cookie——JS 不能读。这样即使前端被 XSS 攻击拿到了 accessToken,攻击者也只能用 2 小时。要长期控制需要拿到刷新能力——但 refreshToken 在 httpOnly Cookie 里,XSS 拿不到。


五、验证 Token——一行 parse 背后的三步

// 来源: JwtUtil.javapublicClaimsparseToken(Stringtoken){returnJwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}

parseClaimsJws背后做了三件事:

  1. 用 secret 重算签名——和 Token 里的 Signature 段比对。不匹配 → 抛出SignatureException——Token 被篡改过
  2. 检查 exp——当前时间 > exp → 抛出ExpiredJwtException——Token 已过期
  3. 解析 Payload——验证通过后返回Claims对象——也就是 Payload 里的所有字段

三步在一行代码里完成。如果任何一步失败——Filter 直接返回 401。


六、完整请求链路——一条请求从进来到出去的 7 步

JwtAuthFilter 拦截所有请求(除了登录和公开接口)。每条请求走 7 步:

HTTP 请求到达 │ ├─ ① 提取 Token │ ├─ Authorization: Bearer xxx → 截取 "Bearer " 之后的部分 │ └─ Cookie: BROWISE_AT=xxx → 从 Cookie 取值(备用通道) │ ├─ ② 验证 Token │ ├─ parseToken(token) → 签名校验 + 过期检查 │ └─ 解析 Claims → 拿到 psnId, type, iat │ ├─ ③ 黑名单检查(jwtBlacklist) │ ├─ isInvalid(psnId, iat) → 该用户此时刻之前的 Token 全失效 │ └─ isInvalidByToken(token) → 精确失效(logout 单设备) │ ├─ ④ 加载用户信息(userProfileCache) │ ├─ 查 OP_PERSON → psnName, unitId, unitName │ ├─ 查 SYS_USER.STATUS → 账号是否启用 │ ├─ 查 SYS_USER_ROLE → 角色列表 │ └─ 缓存 1 小时,TTL 内直接返回不查库 │ ├─ ⑤ 注入当前用户(CurrentUser.set(profile)) │ └─ ThreadLocal → 请求线程内全局可访问 │ ├─ ⑥ 账号状态检查 │ └─ !profile.isEnabled() → 403 "账号已禁用" │ └─ ⑦ 权限检查(menuAuthProvider.canAccess(path)) ├─ 路径在 allowIdList 白名单 → 跳过鉴权 └─ 路径需匹配用户角色的菜单权限 → 不匹配返回 403

第三步的黑名单是最容易被忽略的设计——它解决了"Token 已经签发了但需要让它失效"的问题。


七、黑名单——无状态的代价和补救

JWT 不存状态。但业务上需要主动失效——用户修改密码、管理员禁用账号、用户主动登出——这些场景要求"已经签发的 Token 立刻不能用了"。

这个系统里黑名单分两层:

第一层:用户级失效(按签发时间)

当用户修改密码或管理员重置密码时——把这个用户的(psnId, 当前时间戳)写入黑名单。此后验证逻辑这样判断:

publicbooleanisInvalid(StringpsnId,longiat){LongcutoffTime=blacklist.get(psnId);// 从 Map 取该用户的最早失效时间if(cutoffTime==null)returnfalse;// 不在黑名单 → 正常returniat<=cutoffTime;// Token 签发时间 ≤ 失效时间 → 已失效}

如果用户在 12:00 改了密码——写入blacklist["admin"] = 12:00。之后所有iat ≤ 12:00的 Token 全部作废——不管你是手机登录的还是浏览器登录的。因为改了密码意味着所有历史登录都不可信。

第二层:Token 级失效(登出单设备)

用户点击"退出登录"——把当前这个 Token 写入精确黑名单:

publicbooleanisInvalidByToken(Stringtoken){returntokenBlacklist.contains(token);}

logout 只失效当前设备——手机端的 Token 不受影响。

黑名单的存储选择:

在演示环境里黑名单是内存 Map——服务重启后黑名单清空。生产环境换成 Redis——设置 key 的 TTL 等于 Token 的剩余有效时间——Token 过期后 key 自动删除,不用手动清理。


八、Token 的两个通道——Header 和 Cookie 各自的用途

Token 的传输用了两个通道:

通道Token 类型场景为什么
Authorization HeaderaccessTokenAJAX 请求不受 Cookie 同源限制,跨域可用
httpOnly CookieaccessToken页面跳转、form 提交浏览器自动携带,不用前端手动加
httpOnly CookierefreshToken刷新接口JS 不可读——XSS 攻击拿不到

两个通道的原因是——header 方式需要前端在每个请求里手动加Authorization: Bearer xxx。但有些场景(比如表单 POST 提交、<a>标签跳转)前端控制不了 header。这时候 Cookie 作为备用通道——浏览器自动附在请求上——Filter 层两个地方都查,有任意一个就继续验证。


九、认证和鉴权是两件事

很多刚接触权限系统的开发者把"认证"和"鉴权"混为一谈。这个模块里它们是两个独立的步骤:

认证(Authentication)= 你是谁 → ①②③④⑤:提取 Token → 验签 → 黑名单 → 加载用户 → 注入上下文 鉴权(Authorization)= 你能做什么 → ⑦:拿到用户角色和当前请求路径 → 匹配 → 放行或拒绝

为什么分开?因为有些接口不需要鉴权——只要你是登录用户就可以访问(比如查看自己的个人信息)。有些接口只需要认证不需要角色判断——比如全员可见的公告。


十、配置

browise:auth:enabled:truejwt:secret:browise-demo-local-dev-secret-key-must-be-32charsaccess-token-expire-ms:7200000# 2小时refresh-token-expire-ms:604800000# 7天issuer:browiseexclude-paths:-/auth/**-/captcha-/login

secret在生产环境不应该写在配置文件里。放在环境变量或密钥管理服务中注入——${JWT_SECRET}


十一、结语

JWT 认证不是一个"引入一个库、写两行代码"的事情。它的核心设计决策——双 Token、黑名单、双通道传输、认证鉴权分离——都是在回答同一个问题:“不存状态的 Token 怎么安全地用?”

accessToken 短有效防止泄露窗口、refreshToken 长有效避免频繁登录、httpOnly Cookie 防 XSS、黑名单补上"无状态"的漏洞、用户缓存省掉每次查库——这五个设计合在一起才是一个生产可用的 JWT 认证模块。


✅ 亮点:从"为什么不用 Session"的自然动机出发,按照请求链路逐层拆开 JWT 认证的完整实现——双 Token 生命周期、黑名单两级失效、Header/Cookie 双通道传输、认证鉴权分离——每层都给出了真实源码和为什么选这个设计的原因。适合正在实现或重构认证模块的开发者,也适合面试中"你们系统怎么做登录的"的系统级回答。扩展方向:OAuth2 授权码模式在政务系统中的应用、多系统单点登录(SSO)的 Token 共享方案、JWT + Redis 组合实现有状态 Token 的折中方案。

http://www.jsqmd.com/news/1290538/

相关文章:

  • 2026 年新发布:内蒙古靠谱的吉祥石象定制厂家哪家可靠,出门前摆这物件,竟能帮你避开装修踩坑?很多人都想不到 - 行业严选官
  • 粉笔直播课适合考前三天冲刺突破吗
  • 2026年口碑好的汽车零部件厂家排行一览 - 起跑123
  • 2026保姆级免费抠图工具教程:电脑本地、手机APP、在线网页全方法手把手教学 - 工具软件使用方法推荐
  • 增压泵源头厂家口碑单如何挑选合适配套? - 热点品牌推荐
  • 2026年7月陕西省安康市电信单宽带怎么选_避坑指南 - 找卡家园
  • 2026院校食堂南昌拌粉全料供应商排行一览 - 起跑123
  • 全屋定制安装供应商联系方式查询与落地指南 - 热点品牌推荐
  • AI提示词黄金模板库(覆盖12大行业+8类任务):2024最新实战验证版,仅开放72小时
  • 2026年河北哪里能寻到正宗手工月饼相关推荐 - 起跑123
  • 2026 年新消息:许昌口碑好的泥浆用膨润土加工厂哪家可靠,打桩钻井时,你悄悄用的那玩意儿,竟能让泥浆的质量翻3倍?-亿通膨润土 - 行业推荐官[官方】--
  • 2026 青岛黄金回收与珠宝保养服务商测评榜 正规门店 + 精修服务全维度优选,青岛珠宝保养维修、城阳正规金店回收价格、翠缘珠宝黄金回收 - 优企甄选
  • 长沙会议酒店场地2026年度实用梳理指南 - 起跑123
  • 2026年7月山东省烟台市电信单宽带怎么选_新手避坑指南 - 找卡家园
  • 平凉踏步板生产厂家怎么选才更省心? - 热点品牌推荐
  • java restful Java RESTful被JSON卡脖子?解析行,计算拉胯,分组汇总全靠硬编码
  • 2026年江苏正规脑功能检测热门公司排行梳理 - 起跑123
  • MyBatis+MyBatis-Plus 全套 CRUD 数据库实战
  • 2026模具制造五轴联动加工中心品牌排行一览 - 起跑123
  • 2026年国内环保无甲醛实木家具品牌排行一览 - 起跑123
  • 大厂面试,自进化 agent 正在成为主流!
  • 2026年国内分体灯杆靠谱生产合作方推荐指南 - 起跑123
  • 氯化钙厂家有哪些2026年源头直供怎么选更稳妥 - 品牌优推
  • 2026年7月最新长沙封窗机构一览 工艺口碑维度排行 - 起跑123
  • Java编码utf-8的坑,踩得我头破血流
  • 2026 五金加工场景下钣金喷涂一体化厂家哪家好 - 起跑123
  • 2026保姆级图片换背景教程:手机电脑免费工具+Photoshop完整操作步骤 - 工具软件使用方法推荐
  • 为什么97.3%的提示词迭代失败?——批判性反馈缺失引发的反馈衰减链(附NASA式根因分析模板)
  • 宁夏锚具生产商怎么选才靠谱?看这几点就对了 - 热点品牌推荐
  • 冲孔网工厂推荐几家?按这几点选不踩坑 - 热点品牌推荐