Java 项目实战: 交友项目 - JWT 概述:为什么要用 Token 换掉 Session

概述

交友项目是 App 端与服务端分离、服务端还要集群部署的项目,登录这件事就不能再照搬传统 Web 的 Session 那一套。这篇只讲清楚 JWT 是什么、为什么需要它、三段结构各自干什么、和 Session 比优劣在哪、什么时候该用什么时候不该用;具体怎么写工具类、怎么处理异常,留给同系列的《JWT 入门案例》篇。

纲要

  • 会话管理的起点:HTTP 无状态带来的"你是谁"问题
  • Session 方案怎么工作,以及它在多端、集群场景下的三条硬伤
  • Token 方案的核心思路:服务端不存会话,验签即信任
  • JWT 的三段结构:Header / Payload / Signature
  • 一段真实 token 的逐段拆解与结构表格
  • 验签流程(Mermaid 时序图):为什么改了 payload 就会验签失败
  • 对称签名(HS256/HS512)与非对称签名(RS256)的取舍
  • JWT 与 Session 的对比表格,以及本项目为什么选 JWT
  • JWT 的短板与应对:主动失效、体积、保密性、续期
  • JWT 在交友项目登录流程中的位置
  • 常见误解澄清

从"你是谁"这个问题说起

HTTP 协议本身是无状态的。这句话翻译成人话就是:服务端处理完一个请求就把你忘了,下一个请求进来,它根本不知道这是刚才那个人发的。可业务上又必须知道------查"我"的资料、发"我"的动态、看"我"关注了谁,都得先确定请求方是谁。

这套动作叫用户鉴权,拆开是两件事:判断有没有登录,判断有没有权限。前者是"你是谁",后者是"你能干什么"。这篇只涉及前者。

传统 Web 项目里,答案是 Session。流程不复杂:

  1. 用户提交账号密码,服务端校验通过;
  2. 服务端在内存或存储里建一份会话数据,key 是 JSESSIONID 之类的随机串,value 是用户身份;
  3. 这个 id 通过响应头的 Set-Cookie 写给浏览器;
  4. 浏览器后续请求自动带上 Cookie,服务端拿 id 一查,就知道是谁。

问题出在三个地方,而且是越往后越躲不掉。

服务端要存数据。 每个在线用户对应一条会话记录,用户量上去了,内存扛不住就得上 Redis 之类的集中存储。存是存下来了,但这意味着每次请求都要多一次存储查询------鉴权从一个纯计算动作变成了 IO 动作。

集群下会话不共享。 交友项目的服务端是集群部署的,两台三台机器一起对外提供服务。用户在 A 机器登录,会话记在 A 的内存里;下一次请求被负载均衡分到 B 机器,B 上根本没有这条会话,直接判定未登录。业界有几种解法:session 复制(机器之间互相同步,量大了同步开销很可观)、粘性会话(按 ip 或 cookie 把同一用户钉在同一台机器,机器宕了会话就丢)、独立 session 存储(把会话抽到 Redis,所有机器共享)。能用,但每一条都在给系统加复杂度。

跨端不友好。 交友项目是多端访问:iOS、Android、还有 Web 页面。Cookie 是浏览器体系里的东西,App 和小程序对它的支持并不一致,指望 Cookie 自动携带本身就不可靠。

三条痛点指向同一个根因:会话状态被放在了服务端。

Token 方案:把身份信息交给客户端

换一个思路。服务端不存会话了,登录成功后把"这个用户是谁"这段信息,编码成一个字符串交给客户端自己保管;客户端每次请求把这个字符串带上;服务端拿到之后不查库,而是验证这段信息有没有被动过手脚------验过了,就信任里面写的身份。

这个字符串就是 JWT,全称 JSON Web Token。它的定位很明确:用于对应用程序上的用户进行身份验证的标记。用了 JWT,就不需要再保存用户的 Cookie 或其它 session 数据。

这个转变带来两个直接结果:服务端省掉了会话存储和查询,集群里任何一台机器都能独立完成鉴权------因为它不需要知道别的机器上有什么。这就是所谓无状态。代价也在后面写着:服务端不再持有会话,也就没法"撤销"它,这部分留到短板那一节讲。

JWT 的三段结构

JWT 是一个由点号分隔的三段式字符串,形如 A.B.C:

  • A 段 Header:头部信息,声明 token 类型和签名算法;
  • B 段 Payload:载荷,真正存身份数据的地方;
  • C 段 Signature:签名,用于校验前两段有没有被篡改。

先看两段真实内容。Header 解出来是一个 JSON,通常只有两个字段:

json 复制代码
{
  "alg": "HS512",
  "typ": "JWT"
}

alg 是签名算法,typ 固定为 JWT。Payload 也是 JSON,存的是 claims(声明),交友项目里登录后塞进去的大致是这样:

json 复制代码
{
  "id": 1,
  "mobile": "13800138000",
  "exp": 1618390870
}

Payload 里的 claims 按用途分三类,这个分类值得记住:

类别 说明 例子
标准声明 JWT 规范预定义的字段,建议但不强制使用 exp 过期时间、iat 签发时间、iss 签发者、sub 主题
公共声明 使用者自定义,但需要在 IANA 注册以免冲突 一般业务项目用不到
私有声明 双方约定即可,最常用的一类 用户 id、手机号、昵称

exp 是标准声明里最关键的,服务端验签时会顺便检查它,过期直接判定失效------这是 JWT 唯一自带的"时效控制"手段。

Signature 是对前两段做的签名 ,计算方式是把 base64Url(header) + "." + base64Url(payload) 拼起来,用服务端的密钥按 alg 指定的算法算一个摘要。密钥只有服务端知道,所以客户端拿不出合法的第三段。

这里有一个初学者最大的误解,必须先钉死:Payload 只是做了 Base64Url 编码,不是加密。Base64Url 是一种编码方式,不是加密算法,任何人都能反解。

讲义里把 JWT 描述成"经过加密处理与校验处理的字符串",这里的"加密"是个不严谨的说法,需要纠正:JWT 前两段是可逆的编码,第三段才是签名。签名保证的是数据没被篡改,加密保证的才是数据不被读取,两回事。 这也是为什么敏感数据------密码、账户金额这类------绝对不能写进 JWT。

三段拆解汇总成一张表:

段 内容 编码方式 是否可读 作用
Header {"alg":"HS512","typ":"JWT"} Base64Url 可读 声明类型与签名算法
Payload claims JSON(如 id、mobile、exp) Base64Url 可读 承载身份信息
Signature 前两段加密钥算出的摘要 Base64Url(对摘要) 不可逆推密钥 防篡改

用一段真实 token 印证"可读不可改"

下面这段 token 是用 HS512 加密钥 mfxs123 构造的,跟工程里 JwtUtils 的算法和密钥口径一致:

text 复制代码
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibW9iaWxlIjoiMTM4MDAxMzgwMDAiLCJleHAiOjE2MTgzOTA4NzB9.YDLThmZquceSMNmvF2QBvFp9GwhoziNnm0aOa7kAhu_L9USgsGVslseXPgOWXndPxCLaA6mq1horWzRUos9xBw

把它按点号切开,标上位置:

text 复制代码
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9 . eyJpZCI6MSwibW9iaWxlIjoiMTM4MDAxMzgwMDAiLCJleHAiOjE2MTgzOTA4NzB9 . YDLThmZquceSMNmvF2QBvFp9GwhoziNnm0aOa7kAhu_L9USgsGVslseXPgOWXndPxCLaA6mq1horWzRUos9xBw
└────────── Header ──────────┘   └────────────────────── Payload ──────────────────────┘   └────────────────────────── Signature ──────────────────────────┘
{"alg":"HS512","typ":"JWT"}          {"id":1,"mobile":"13800138000","exp":1618390870}

把整串贴到官方测试网站 jwt.io,右侧会直接展示三段解码结果:Header 一行、Payload 一行,都是明文可读的 JSON,只有 Signature 一栏显示 Invalid Signature 之类的提示------因为 jwt.io 不知道你的密钥是 mfxs123。在密钥框里填上 mfxs123,提示就变成签名有效。

这个演示想说明的就是"可读不可改":

  • 可读 ------不需要密钥,任何人 Base64Url 反解就能看到 id 是 1、手机号是多少。手改一下 Payload 里的 id,把它改成 999;
  • 不可改------Payload 一变,服务端用同样的密钥重新算出的签名就和第三段对不上,验签直接失败。而攻击者没有密钥,算不出能匹配的第三段。

所以 JWT 的安全边界很清楚:它保护的是完整性(没被改),不是机密性(不被看见)。

验签是怎么工作的

服务端拿到 token 后并不"解密"它,而是重新算一遍签名做比对。整个校验流程:
#mermaid-svg-8dT19vHnFtaT2bZE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8dT19vHnFtaT2bZE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8dT19vHnFtaT2bZE .error-icon{fill:#552222;}#mermaid-svg-8dT19vHnFtaT2bZE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8dT19vHnFtaT2bZE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8dT19vHnFtaT2bZE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8dT19vHnFtaT2bZE .marker.cross{stroke:#333333;}#mermaid-svg-8dT19vHnFtaT2bZE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8dT19vHnFtaT2bZE p{margin:0;}#mermaid-svg-8dT19vHnFtaT2bZE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8dT19vHnFtaT2bZE .cluster-label text{fill:#333;}#mermaid-svg-8dT19vHnFtaT2bZE .cluster-label span{color:#333;}#mermaid-svg-8dT19vHnFtaT2bZE .cluster-label span p{background-color:transparent;}#mermaid-svg-8dT19vHnFtaT2bZE .label text,#mermaid-svg-8dT19vHnFtaT2bZE span{fill:#333;color:#333;}#mermaid-svg-8dT19vHnFtaT2bZE .node rect,#mermaid-svg-8dT19vHnFtaT2bZE .node circle,#mermaid-svg-8dT19vHnFtaT2bZE .node ellipse,#mermaid-svg-8dT19vHnFtaT2bZE .node polygon,#mermaid-svg-8dT19vHnFtaT2bZE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8dT19vHnFtaT2bZE .rough-node .label text,#mermaid-svg-8dT19vHnFtaT2bZE .node .label text,#mermaid-svg-8dT19vHnFtaT2bZE .image-shape .label,#mermaid-svg-8dT19vHnFtaT2bZE .icon-shape .label{text-anchor:middle;}#mermaid-svg-8dT19vHnFtaT2bZE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8dT19vHnFtaT2bZE .rough-node .label,#mermaid-svg-8dT19vHnFtaT2bZE .node .label,#mermaid-svg-8dT19vHnFtaT2bZE .image-shape .label,#mermaid-svg-8dT19vHnFtaT2bZE .icon-shape .label{text-align:center;}#mermaid-svg-8dT19vHnFtaT2bZE .node.clickable{cursor:pointer;}#mermaid-svg-8dT19vHnFtaT2bZE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8dT19vHnFtaT2bZE .arrowheadPath{fill:#333333;}#mermaid-svg-8dT19vHnFtaT2bZE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8dT19vHnFtaT2bZE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8dT19vHnFtaT2bZE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8dT19vHnFtaT2bZE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8dT19vHnFtaT2bZE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8dT19vHnFtaT2bZE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8dT19vHnFtaT2bZE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8dT19vHnFtaT2bZE .cluster text{fill:#333;}#mermaid-svg-8dT19vHnFtaT2bZE .cluster span{color:#333;}#mermaid-svg-8dT19vHnFtaT2bZE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8dT19vHnFtaT2bZE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8dT19vHnFtaT2bZE rect.text{fill:none;stroke-width:0;}#mermaid-svg-8dT19vHnFtaT2bZE .icon-shape,#mermaid-svg-8dT19vHnFtaT2bZE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8dT19vHnFtaT2bZE .icon-shape p,#mermaid-svg-8dT19vHnFtaT2bZE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8dT19vHnFtaT2bZE .icon-shape .label rect,#mermaid-svg-8dT19vHnFtaT2bZE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8dT19vHnFtaT2bZE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8dT19vHnFtaT2bZE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8dT19vHnFtaT2bZE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
不一致
一致
已过期
未过期
客户端请求携带 token
token 是否为空或格式非法
判定无效 返回未登录
按点号切成三段 取出 header 与 payload
用服务端密钥 + header 中的 alg 重新计算签名
计算结果与第三段是否一致
签名不匹配 说明被篡改 判定无效
检查 exp 是否已过期
判定失效
信任 payload 中的 claims 取出用户 id
放行 进入业务逻辑

关键在于签名算法的确定性:同样的输入加同样的密钥,算出来的摘要一定相同。攻击者哪怕只改动 Payload 里的一个字符,反解出来的 JSON 就变了,重算的签名必然对不上。这就是 JWT 防篡改的全部原理,没有更玄的东西。

签名算法按密钥的使用方式分成两大类,这也是选型时要做的第一个决定:

算法类型 代表 密钥特征 签发与验签 适用场景
对称签名 HS256 / HS512 同一个密钥 同一个密钥既签又验 单体应用、可信内网,签发方和验签方是同一个服务
非对称签名 RS256 一对公私钥 私钥签发,公钥验签 多系统共享身份、需要第三方或网关独立验签的场景

对称签名的优点是快、实现简单,缺点是密钥必须共享------任何需要验签的一方都得拿到密钥,而拿到密钥就意味着能伪造 token。非对称签名把"能验"和"能签"分开:资源服务只拿公钥,只能验不能签,密钥泄露的风险面小很多,代价是计算更慢、密钥管理更麻烦。交友项目内部用的是 HS512 对称方案,签发和验签都在自己项目里,够用。

JWT 与 Session 的对比

这张表是这篇最该带走的东西:

维度 Session JWT
状态存在哪 服务端(内存 / Redis) 客户端(由客户端保存和携带)
服务端存储压力 有,在线用户越多压力越大 无,服务端不落盘
分布式友好度 差,要靠 session 复制 / 粘性会话 / 独立存储解决 好,任何节点独立验签即可
能否主动失效 可以,删掉服务端那条会话就生效 不行,签发后在有效期内一直有效,这是最大短板
续期方式 服务端可控,滑动过期很自然 需要另发新 token 或引入 refresh token
跨端支持 依赖 Cookie,App / 小程序支持不一致 放在请求头里手动携带,各端统一
典型适用场景 传统 Web 后台、单机或强管控内网 移动端 App、前后端分离、微服务

先把结论说在前面:不是 JWT 一定更好。 如果项目就是一个传统 Web 后台,用户都在浏览器里,那 Session 反而省事------能主动踢人、续期简单、只传一个几十字节的 sessionId,JWT 那几条优势一条都用不上。技术选型看的从来是场景,不是流行度。

交友项目选 JWT,理由是可以逐条对上的:客户端是 App,Cookie 这条路不好走;前后端分离,接口是给多端共用的;服务端集群部署,会话共享要额外搭一套;后续走微服务,网关和各服务都要能独立验签,无状态的 token 天然合适。四条理由指向同一个答案。

JWT 的不足之处

把缺点摆清楚,比光说优点有用。这里每条都给应对方式:

短板 具体表现 应对方式
无法主动失效 用户登出、改密码、被封号,旧 token 在到期前依然有效 缩短有效期 + 服务端维护黑名单 / 白名单,或引入版本号校验
体积比 sessionId 大 sessionId 通常几十字节,一个带若干 claims 的 token 上百字节起步,每次请求都带 Payload 只放必要字段,别把整份用户对象塞进去
Payload 不保密 Base64Url 编码可被任何人解开 敏感信息一律不放,绝不放密码、金额、身份证号
泄露即被冒充 token 被截获,持有者就是"合法用户" 全链路走 HTTPS,配合短有效期把窗口压小
续期要额外设计 有效期到了就只能重新登录,直接给长有效期又等于放大风险 双 token 方案:短效 access token + 长效 refresh token,仅 refresh 接口可续
服务端仍要做事 有人误以为用了 JWT 服务端什么都不用管 验签、过期检查、必要时查黑名单,一个都不能省

针对"无法主动失效"再补一句:业界常见的折中是黑名单------登出时把该 token 的标识塞进 Redis,验签通过后再查一次黑名单;或者干脆把有效期设得很短(比如 30 分钟),用"过期自然失效"替代"主动失效"。两种方案都不是银弹,黑名单等于又把状态放回了服务端,只是放得比 Session 少得多。这个取舍本身也说明了一件事:无状态不是免费的。

JWT 在交友项目登录流程里的位置

把 JWT 放回到整个登录链路里,它处在"签发"和"校验"两个点上:
服务端 客户端 App 服务端 客户端 App #mermaid-svg-OYVSAe8RKHYGMCNQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OYVSAe8RKHYGMCNQ .error-icon{fill:#552222;}#mermaid-svg-OYVSAe8RKHYGMCNQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OYVSAe8RKHYGMCNQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OYVSAe8RKHYGMCNQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OYVSAe8RKHYGMCNQ .marker.cross{stroke:#333333;}#mermaid-svg-OYVSAe8RKHYGMCNQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OYVSAe8RKHYGMCNQ p{margin:0;}#mermaid-svg-OYVSAe8RKHYGMCNQ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OYVSAe8RKHYGMCNQ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OYVSAe8RKHYGMCNQ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-OYVSAe8RKHYGMCNQ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-OYVSAe8RKHYGMCNQ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-OYVSAe8RKHYGMCNQ .sequenceNumber{fill:white;}#mermaid-svg-OYVSAe8RKHYGMCNQ #sequencenumber{fill:#333;}#mermaid-svg-OYVSAe8RKHYGMCNQ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-OYVSAe8RKHYGMCNQ .messageText{fill:#333;stroke:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OYVSAe8RKHYGMCNQ .labelText,#mermaid-svg-OYVSAe8RKHYGMCNQ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .loopText,#mermaid-svg-OYVSAe8RKHYGMCNQ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-OYVSAe8RKHYGMCNQ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-OYVSAe8RKHYGMCNQ .noteText,#mermaid-svg-OYVSAe8RKHYGMCNQ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-OYVSAe8RKHYGMCNQ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OYVSAe8RKHYGMCNQ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OYVSAe8RKHYGMCNQ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-OYVSAe8RKHYGMCNQ .actorPopupMenu{position:absolute;}#mermaid-svg-OYVSAe8RKHYGMCNQ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-OYVSAe8RKHYGMCNQ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-OYVSAe8RKHYGMCNQ .actor-man circle,#mermaid-svg-OYVSAe8RKHYGMCNQ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-OYVSAe8RKHYGMCNQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 校验通过 校验不通过 提交手机号 + 验证码 校验验证码,查用户,不存在则自动注册 用用户 id / 手机号构造 claims,签发 token 响应 token 本地保存 token 后续请求,请求头携带 token 拦截器取出 token,验签 + 检查 exp 从 claims 取出用户 id,放行 返回业务数据 返回 401 未登录 / 鉴权失败

服务端这边要做的事其实就两件:登录成功时颁发 token,往里面写用户信息(注意不写敏感内容);数据接口校验 token,通过 token 取回用户信息。 剩下的活都在客户端:保存 token,每次请求记得带上。

第二步"从请求头取 token、验签、塞进上下文"这套统一动作,通常放在拦截器里做------那是后面章节的内容,这里只负责指出 JWT 在链路中的位置。

工程里承载这两个动作的是 JwtUtils,它在 mf-commons 模块里,被各服务共用:

text 复制代码
mf
├── mf-commons                        # 公共模块,放跨服务复用的工具
│   └── src/main/java/com/mf/commons/utils
│       └── JwtUtils.java                 # 签发 getToken / 验签 verifyToken / 取 claims getClaims
├── mf-app-server                     # 面向 App 的服务,登录接口在这里签发 token
├── mf-dubbo-interface                # Dubbo 接口定义
└── mf-dubbo-service                  # Dubbo 服务实现

JwtUtils 是一个静态工具类,有效期常量 TOKEN_TIME_OUT = 3_600(单位秒,即 1 小时),签名算法 HS512,密钥是硬编码在类里的 mfxs123。这是课程练手工程的现状------密钥硬编码、有效期写死,作为教学演示没问题,真要上线都得外置到配置文件并区分环境。密钥怎么放、异常怎么分开处理,正是《JWT 入门案例》要展开的部分。

常见误解澄清

几个反复被问到、也反复被写错的点:

误解 实际情况
JWT 是加密的,别人看不到内容 前两段只是 Base64Url 编码,明文可读;第三段是签名。签名保完整性,加密才保机密性
用了 JWT 就绝对安全了 token 泄露就能被冒充。必须配 HTTPS,并且有效期不能设太长
JWT 能主动登出 不能,服务端没有会话可删。要靠短有效期、黑名单或 token 版本号来补
用了 JWT 服务端就不用管了 验签、过期检查、按需查黑名单,都是服务端必须做的
Payload 越大越好,多放点省得查库 Payload 每次请求都要传输,膨胀直接增加带宽和解析开销;只放标识类字段
把密码 / 金额放进 Payload 更省事 Payload 可被任何人解开,这等于把敏感数据写在明信片上

总结

JWT 要解决的问题很朴素:HTTP 无状态,服务端怎么知道请求来自哪个用户。Session 用"服务端存一份、客户端拿 id"解决,在多端访问和集群部署的场景下暴露了存储压力、会话不共享、Cookie 不通用三条硬伤。JWT 换个方向,把身份信息编码成字符串交给客户端,服务端改成"验签即信任",从而做到无状态。

结构上它是 Header.Payload.Signature 三段:Header 声明算法,Payload 承载 claims,Signature 用密钥对前两段签名防篡改。记住一句话就够用------Payload 是可读的,签名是不可改的,这两件事别混。

选型上它不是银弹。主动失效做不了、体积比 sessionId 大、payload 不保密、续期要额外设计,这些都是真实成本。交友项目选它,是因为 App 多端 + 前后端分离 + 集群 + 微服务这四条正好踩在 JWT 的舒适区里。

原理到这就够了,接下来是动手:用 io.jsonwebtoken:jjwt 把生成、解析、验签的工具类写出来,把过期和被篡改两类异常分别触发一次看看效果。

相关推荐
SL_staff1 小时前
低代码选型避坑指南:为什么‘能拖拽’不等于‘能交付’?——基于JVS八大引擎的四维交付能力评估框架
java·低代码·spring cloud
李福春2 小时前
技术思考问题6:技术演讲该晒成功架构,还是交代失败与取舍?
java·开发语言·架构·computer use·深圳同盟·腾讯云架构师技术同盟·反演思维
用户7771298509122 小时前
equals和==的区别
java
旋律4142 小时前
对封装、继承、多态三个概念理解
java
code2cat2 小时前
【随笔】LLM裁判如何打分:先写评分标准,再用人工案例校准
java·人工智能·llm·agent·评估·langsmith
SL_staff2 小时前
计息规则不再等IT:JVS-Rules函数计算器在金融计息场景的技术实践
java·git·全栈
古法老登瞎折腾2 小时前
大数据智能运维实战(二):准备一个可复现的运维实验环境
java·大数据·运维·spring ai·mcp