创见博客
什么是 JWT:从「为什么需要它」到「它到底怎么实现」
七崽爱吃小饼干2026/10/22阅读 0

先看一串东西:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxIiwibmFtZSI6ImxqbSIsImlhdCI6MTc5MTcxMDU2OCwiZXhwIjoxNzkxOTI2NTY4fQ.
iwJU5RFWL8eBAqjcXnxOduKL5dQZut8124YyMTW6rRM

这是登录成功后服务器发给你的一段 JWT。它看着像加密的乱码,其实中间那段是明文的 JSON——随便找个 JWT 解码工具贴进去,就能看到里面写着 name: "ljm"、什么时候签发、什么时候过期。

要理解 JWT,别从「它长什么样」开始,要从「它为什么存在」开始。JWT 是 HTTP 无状态这件事,逼出来的一个方案。

一、为什么需要 JWT:先看 Session 的麻烦

HTTP 天生无状态:每个请求都是独立的,服务器压根不记得你上一个请求干过什么。

可登录这件事天然是有状态的——你输密码登一次,之后每个请求服务器都得知道「这是同一个人」。怎么记住?

传统做法是 Session:

text
1. 你在服务器登录,服务器建一条 session 记录存起来
2. 服务器把 sessionId 塞进 Cookie 发给你
3. 你之后每个请求都带上这个 Cookie
4. 服务器拿 sessionId 去自己的存储里查:"哦,是这个人"

问题出在第 1 步和第 4 步:状态存在服务器上。这带来一串连锁反应:

  • 要存东西:内存扛不住,得上 Redis,又是一套要运维的服务。
  • 多机难同步:一个请求负载均衡到 B 机,结果 session 存在 A 机,用户状态没了。要么粘性会话,要么共享存储。
  • 跨域/跨端别扭:Cookie 受同源策略约束,App、小程序、第三方调用都不方便带。
  • 扩展受限:想横向加机器,得先解决 session 共享。

一句话:Session 把「你是谁」这张状态存在了服务器,服务器越多、端越杂,越难受。

JWT 的思路正好反过来。

二、JWT 是什么:把状态搬到客户端

JWT,全称 JSON Web Token。它的核心思想是:

text
别在服务器存「你是谁」,
把「你是谁」写进一张签名过的凭证里,交给客户端自己拿着。
服务器不存任何东西,收到凭证后自己验一下签名就知道真假。

这样服务器就「无状态」了——任何一台机器拿到 token,用同一个密钥验一下,都能确认你的身份,不需要共享存储。横向扩展变得轻而易举,跨域、跨端也不受 Cookie 那套限制。

这正是 JWT 解决的核心问题:在无状态的 HTTP 之上,用一张自证的凭证,恢复出「有状态」的身份识别。

三、JWT 的结构:三段,用点隔开

一个 JWT 就是三段 Base64URL 字符串,用 . 连起来:

text
Header . Payload . Signature
  header   payload   signature

第一段 Header,描述签名算法:

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

第二段 Payload,存放声明(claims),也就是真正的数据:

json
{
  "sub": "1",
  "name": "ljm",
  "role": "1",
  "iat": 1791710568,
  "exp": 1791926568
}

这里面分两类:

类型例子含义
标准声明iss签发者
sub主题(常见于用户 ID)
exp过期时间
iat签发时间
nbf生效时间(之前不可用)
jti唯一 ID(可用于防重放/黑名单)
aud接收方
自定义声明name / role想放什么放什么

第三段 Signature,是拿前两段 + 密钥算出来的签名,用来防篡改:

text
signature = 签名算法(
  base64url(header) + "." + base64url(payload),
  密钥
)

这里有个必须记住的点:JWT 是「签名」不是「加密」。 前两段只是 Base64URL 编码,任何人都能解开看,只是不能改(改了签名就对不上)。所以——

绝对不要把密码、身份证号这类敏感信息放进 payload。

四、它到底怎么实现的

拆完结构,实现就没什么魔法了。签名算法最常用两种:

  • HS256(对称):用同一个密钥签发、也用同一个密钥验证。简单,但验签方也能签发,适合只有自己用的场景。
  • RS256(非对称):私钥签发,公钥验证。验签方拿公钥只能验、不能签,适合「一个签发方、多个验证方」。

手动实现 HS256,其实就几行

javascript
const crypto = require('crypto')

const SECRET = process.env.SECRET_KEY
const b64 = s => Buffer.from(s).toString('base64url')

function sign(payload) {
  const header = b64(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))
  const body = b64(JSON.stringify({
    ...payload,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 3600,   // 1 小时后过期
  }))

  const data = `${header}.${body}`
  const sig = crypto.createHmac('sha256', SECRET).update(data).digest('base64url')

  return `${data}.${sig}`
}

验证就是把签名重算一遍,对上再检查过期:

javascript
function verify(token) {
  const [header, body, sig] = token.split('.')
  if (!header || !body || !sig) throw new Error('格式错误')

  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${header}.${body}`)
    .digest('base64url')

  // 用恒定时间比较,防止时序攻击
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    throw new Error('签名不匹配,被篡改过')
  }

  const payload = JSON.parse(
    Buffer.from(body, 'base64url').toString()
  )

  if (payload.exp < Math.floor(Date.now() / 1000)) {
    throw new Error('已过期')
  }

  return payload
}

实际项目里没人手写,用库就行(Node 的 jsonwebtoken、前端的 jose):

javascript
const jwt = require('jsonwebtoken')

const token = jwt.sign({ sub: '1', name: 'ljm' }, SECRET, { expiresIn: '1h' })

const payload = jwt.verify(token, SECRET)  // 签名错或过期会直接抛异常

签名的价值在于「防篡改」:攻击者能读到 payload,也能改,但只要改了内容,第三段签名立刻对不上,服务器一验就露馅。他没有密钥,伪造不出新签名。

五、JWT 的坑:无状态是优点,也是软肋

1. 没法主动撤销

Session 想让你「登出」,服务器删掉那条记录就行。但 JWT 服务器不存东西,签发出去就收不回来了——你没法让一张还没过期的 token 立刻失效。

这就是「登出/封号」的痛点。补救办法通常是黑名单:把要作废的 token(或它的 jti)存进 Redis,每次验签再查一遍黑名单。但这么一搞,服务端又变成有状态了——JWT 的爽点被吃掉一半。

所以实践里靠缩短有效期来缓解:token 活得越短,作废窗口越小。

2. 过期时间的两难

  • 有效期太长:泄露了危害大,且撤销不了。
  • 有效期太短:用户几分钟就被踢出去重新登录。

答案是双 token 方案,见下一节。

3. 存哪里都是权衡

存储位置风险
localStorageJS 能读到 → XSS 一打一个准
httpOnly CookieJS 读不到,但浏览器会自动带上 → CSRF 风险

没有完美选项,只能配 SameSite、CSRF token、CSP 等一起用。

4. 算法相关的攻击

  • alg: none:早期一些库不校验算法,攻击者把 alg 改成 none、删掉签名,就绕过了验证。现代库默认拒绝。
  • 算法混淆:你把系统配成 RS256(公钥验签),攻击者把 header 改成 HS256,然后拿公开的公钥当 HMAC 密钥去签,有些实现会验过。所以要在服务端写死允许的算法,别信 token 里的 alg。

5. 密钥要够硬

HS256 的密钥一旦泄露,谁都能伪造任意用户的 token。别用弱密钥,别提交到仓库,该轮换就轮换。

六、和 Session 怎么选

维度SessionJWT
状态存哪服务端客户端
服务端存储需要(内存/Redis)不需要
横向扩展麻烦(要共享)天然支持
跨域/跨端受限(Cookie)友好
主动登出容易难(要黑名单)
携带数据只带一个 ID可带用户信息,省一次查询
体积小较大(每请求都带)

判断口诀:

text
单体应用、要随时踢人下线        → Session
多服务、多端、无状态横向扩展需求  → JWT
既要扩展又要能撤销              → JWT + 短有效期 + 黑名单

七、实战里的典型方案:双 Token

现实中最常见的是 Access Token + Refresh Token 组合,解决「既要安全又要体验」的矛盾:

text
Access Token   有效期短(如 15 分钟)  用来访问接口,泄露了窗口也小
Refresh Token  有效期长(如 7 天)     专门用来换新的 Access Token

流程:

text
1. 登录 → 服务器发 access + refresh 两张 token
2. 每次请求带 access
3. access 过期 → 客户端拿 refresh 去 /refresh 换一张新的 access
4. refresh 也过期(或用户登出)→ 重新登录

Refresh Token 一般存得更保险(httpOnly Cookie),并且服务端会存一份——这样需要封号时,删掉这个 refresh 记录,用户最多再用 access 撑十几分钟就彻底登不上了。用短有效期把「无状态」的安全债压到可接受的范围。

小结

  • JWT 是为了解决「HTTP 无状态下怎么记住登录」而生的:不把状态存服务器,而是把身份写进一张签名凭证交给客户端。
  • 结构是 Header.Payload.Signature 三段 Base64URL,Payload 是明文 JSON,签名只防篡改、不防偷看,敏感信息别放。
  • 签名原理很简单:用密钥对前两段算 HMAC,验证时重算比对;HS256 对称、RS256 非对称。
  • 最大优点是无状态、易扩展、跨端友好;最大软肋是难撤销、易受 XSS/CSRF/算法攻击。
  • 实践中用 短有效期 Access Token + 长有效期 Refresh Token 平衡安全和体验,必要时加黑名单。
  • 一句话:Session 把身份存在服务器,JWT 把身份签给客户端;前者好管,后者好扩,选哪个取决于你更怕哪种麻烦。
评论
0/100