如果你这段时间在写登录、权限、接口鉴权相关的功能大概率已经被一个词刷过屏JWT。登录成功之后接口返回一长串看起来乱七八糟的字符串前端把它存起来后面每次请求都往请求头里带后端校验通过才放行。这套玩法用起来门槛很低但大多数人并不清楚它底层真正是什么也不明白为什么把 Token 里的某个字段改掉之后服务端一下子就不认账了。这篇是 JWT 学习系列的第一篇我先把“JWT 是什么”这件事彻底讲透。我会从一个最普通的登录需求讲起再看它内部到底长什么样签名在保护什么最后用一段不依赖任何框架的最小代码演示生成和校验过程。适合刚接触 JWT、被网上一堆“无状态”“自包含”“防篡改”这些词绕晕的同学。看完这篇你至少能跟别人解释清楚 JWT 和 Session 到底差在哪以及在什么场景下该怎么选。1. JWT 到底是什么先把它从“黑话”翻译成人话1.1 一个朴素又头疼的认证问题先回到一个特别基础的场景。假设你在做模拟项目X的登录接口用户输入账号密码你校验通过了然后呢下一个请求你要怎么知道它是同一个用户发来的最笨的办法是让客户端每次请求都带着用户名后端查一下用户是否存在。但这种方式等于完全不设防任何人都能伪造请求。于是大家习惯用 Session 方案。用户登录成功后服务器在内存或缓存里创建一个会话记录生成一个随机的 Session ID把这个 ID 放进 Cookie 交给浏览器。浏览器后续每次请求会自动带上 Cookie服务器拿到 Session ID 后去自己的“小本本”里查一下查到记录就说明认证通过。这在小项目里很好用但有个很现实的麻烦当服务从一台变成多台请求被负载均衡转发到 B 机器而 Session 存在 A 机器上B 机器查不到记录用户就会被莫名其妙地“踢下线”。解决办法不外乎 Session 同步、粘性会话、单独搭一个共享缓存存储运维和业务复杂度一下就上来了。JWT 想解决的正是这一类“状态维护”带来的痛点。它换了个思路把用户身份信息直接打包发给客户端由客户端自己保存服务端不再需要保存任何会话记录。请求来了服务端只要验证发回来的凭证是真的就直接信任。1.2 JWT 是一张“带签名的通行证”JWT 的全称是 JSON Web Token翻译过来就是“JSON 网络令牌”。它本质上是一段经过特定编码的 JSON 文本用来在各方之间传递一组声明也就是 Claims。一个完整的 JWT 一般由三大部分组成Header头部、Payload载荷、Signature签名中间用点号分隔。你可以把它想象成一张带着防伪章的会员卡。卡上写着持卡人姓名、有效期底部盖了一个只有场馆方才能盖的章。访客每次持卡进场管理员不需要去翻什么会员名册只需要验证章是真的、日期没过期就直接放行。JWT 就是同样的逻辑服务器签发一个不可伪造的凭证客户端自己保存请求时把它带回来服务器独立验证真伪。因为这个验证过程不依赖服务端的会话存储所以很多人称它为“无状态认证”。注意无状态指的是认证信息不依赖服务端的“流水账”不是说你什么都不用做了权限判断、业务校验该做还是得做。1.3 JWT 是标准不是某个语言的专属功能接触深了你会发现JWT 并不绑定任何特定语言。Java 能实现、Node 能实现、Python 也能实现甚至不同语言写的服务之间可以互相解析同一个 Token。这套东西背后是一套公开标准所有语言库的实现逻辑都大同小异先编码 Header 和 Payload再计算签名最后验签。所以我不建议你学习 JWT 时一上来就背某个框架的 API。最有效的路径是先弄懂 Token 这个字符串本身的结构然后把生成和验证的底层逻辑亲手写一遍。技术栈可以换但原理是通用的这个老本行吃透了走到哪儿都不会慌。2. 手撕一个真实的 JWT三个部分逐一解剖2.1 完整的 Token 长什么样一个 JWT 的典型形态是下面这种一长串文本。为了方便观察结构我把三段手动换行排开了eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiJ1c2VyXzEwMDEiLCJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6InByb2plY3QtbWFuYWdlciIsImlzcyI6InByb2plY3QtYXV0aC1zZXJ2aWNlIiwiaWF0IjoxNzE3MjM5MDAwLCJleHAiOjE3MTcyNDI2MDB9. dGhpc0lzQVNpZ25hdHVyZVBsYWNlaG9sZGVy第一段是 Header第二段是 Payload第三段是 Signature。必须提醒一下上面第三段是我为了展示结构手动填的占位字符串不是真的拿某个密钥算出来的结果。真实项目里这一段会由服务器根据约定算法算出一长串签名。这里存在一个特别容易误解的点第一眼看上去像乱码但它们并不是密文。Header 和 Payload 本质上都只是经过 Base64Url 编码的文本任何一个拿到 Token 的人都可以在没有任何密钥的情况下还原出原文。JWT 的安全性核心其实只押在最后一段签名上。2.2 Header 和 Payload只是编码不是加密把第一段按 Base64Url 解码得到的是下面这个 JSON{alg:HS256,typ:JWT}它告诉接收方两件事签名算法用的是 HS256令牌类型是 JWT。这个字段在解析时非常重要服务端必须校验它是不是自己支持的算法不能盲目信任。把第二段解码得到的是真正携带业务信息的载荷{ sub: user_1001, username: admin, role: project-manager, iss: project-auth-service, iat: 1717239000, exp: 1717242600 }sub 是标准字段通常存用户唯一标识username、role 是自定义字段iss 表示签发方iat 是签发时间exp 是过期时间单位都是秒。这些字段就是 JWT 里的 Claims描述当前 Token 的主人是谁、有什么权限、什么时候失效。这里我必须反复强调一句Payload 虽然写了用户名和角色但它只是做了编码不是加密。你不需要任何密钥随便用一个在线解析工具、或者几行脚本就能还原出这段 JSON。所以像密码、手机号、身份证号这类敏感信息绝对不能往 Payload 里塞。可以这么理解JWT 是一张明信片明信片上的字谁都能看到真正保证内容不被篡改的是那个防伪标记。顺便说一下 Base64Url。平时常见的 Base64 编码结果里可能会有、/、这些字符放到 URL 或请求头里容易出问题。所以 JWT 采用了 Base64Url 变体把替换成-把/替换成_并去掉末尾的。这样整个 Token 都由 URL 安全字符组成可以直接放进 Authorization 请求头。2.3 Signature让一段人人都能读的文本变得可信签名的作用一句话就能说清防止 Token 内容被篡改。服务器签发 Token 时会对“Header . Payload”这段完整字符串用约定算法加密钥计算一个签名。客户端后续请求把完整 Token 带回来服务器重新算一遍签名一比对就能知道内容有没有被动过。用 HS256 也就是 HMAC-SHA256 举例签名计算过程是这样signature HMACSHA256( base64url(header) . base64url(payload), secret )这里的 secret 就是服务器握在手里的“印章”绝对不能泄露。你把 Payload 里的 username 改成 admin哪怕只改一个字符重新编码后的 Payload 也变了。服务器拿新的 Header.Payload 重新计算签名得到的结果和原来的签名不一致校验直接失败。所以 JWT 防的是“篡改”而不是“偷看”。理解了这三个部分你就明白为什么说 JWT“自包含”了。头、内容、防伪标记全在这一个字符串里服务端不需要查任何存储就能对请求方的身份和Token可信度做出基本判断。3. 动手写一遍JWT 生成与校验的最小实现3.1 先搞清两种主流签名算法选签名算法是很多人纠结的地方。实际项目里最常见的两个选项是 HS256 和 RS256。HS256 是共享密钥方式签名和验签用同一个 secret。优势是简单适合前后端都由自己掌控的系统。缺点是任何一个持有该密钥的服务出了问题都有能力替服务器伪造 Token。所以 HS256 的密钥必须守好。RS256 是公私钥方式服务器用私钥签其他服务只拿公钥验。公钥只能验证签名不能伪造新签名。适合需要跨团队、跨系统协同时使用某一方泄露公钥也不会导致全局失控。简单总结同一个团队内部HS256 最省事要跨系统、多服务协作RS256 更稳妥。不管选哪种密钥都不该写死在代码仓库里放配置中心、环境变量或者密钥管理服务是基本操作。3.2 用 Node 内置模块写最小校验脚本为了真正理解原理我不建议只会调库至少自己写一遍校验逻辑。下面这个 Node 脚本只依赖内置模块没有任何第三方依赖。const crypto require(crypto); // 标准 Base64 转 Base64Url function toBase64Url(base64) { return base64.replace(/\/g, -).replace(/\//g, _).replace(/$/, ); } // 对字符串做 Base64Url 编码 function base64UrlEncode(str) { return toBase64Url(Buffer.from(str, utf8).toString(base64)); } // 计算 HS256 签名 function sign(data, secret) { const hmac crypto.createHmac(sha256, secret); hmac.update(data); return toBase64Url(hmac.digest(base64)); } // 生成 Token function createToken(payload, secret) { const header { alg: HS256, typ: JWT }; const headerPart base64UrlEncode(JSON.stringify(header)); const payloadPart base64UrlEncode(JSON.stringify(payload)); const signature sign(${headerPart}.${payloadPart}, secret); return ${headerPart}.${payloadPart}.${signature}; } // 校验 Token function verifyToken(token, secret) { const parts token.split(.); if (parts.length ! 3) return { ok: false, reason: 格式不正确 }; const [headerPart, payloadPart, signature] parts; const expectedSignature sign(${headerPart}.${payloadPart}, secret); if (expectedSignature ! signature) { return { ok: false, reason: 签名校验失败 }; } const payload JSON.parse(Buffer.from(payloadPart, base64url).toString(utf8)); if (payload.exp Date.now() / 1000 payload.exp) { return { ok: false, reason: Token 已过期 }; } return { ok: true, payload }; } const secret my-project-secret-2024; const token createToken( { sub: user_1001, username: admin, role: project-manager, exp: Math.floor(Date.now() / 1000) 3600 }, secret ); console.log(生成的 Token, token); console.log(verifyToken(token, secret));这段代码运行后的输出大概是生成的 Token eyJhbGciOi... { ok: true, payload: { sub: user_1001, username: admin, role: project-manager, exp: 1717242600 } }我特意没用任何第三方库就是想让你看清楚签名的核心其实就是一个 HMAC 计算加字符串比对逻辑并不复杂。把这个跑通了以后看框架源码或排查线上 Token 失效问题都会顺手很多。这里有个容易踩的小坑Node 的 createHmac 直接 digest 出来是标准 Base64里面可能有、/、字符必须做一次 Base64Url 转换再和 Token 里的签名段比较否则校验会一直失败。这个细节我第一次手写时也翻过车。3.3 把一条完整的登录认证流程串起来理论合齐了再看真实项目里怎么配合。以模拟项目X的登录模块为例完整链路一般是用户提交账号密码。服务端校验密码取出用户ID、角色等基本信息。服务端构造 Payload设置过期时间用密钥签名生成 Token。接口把 Token 返回给前端。前端可以放内存、localStorage 或者 Cookie。之后每次请求前端在请求头里加一行Authorization: Bearer token。后端在接口入口统一解析这个头校验签名和过期时间。通过后从 Payload 里取出用户ID和角色后续业务直接用。这个闭环最大的特点是服务端在处理请求时不需要去查“这个用户是否还在登录状态”的存储表。Token 本身已经提供了最基本的信任依据。当然角色权限、菜单范围这些业务逻辑还是得自己判断JWT 只帮你解决了“身份凭证可信”这一件事。4. JWT 和 Session 到底怎么选别被“无状态”冲昏头4.1 一张表看懂两种方案的本质差异不少新人学了 JWT 之后会觉得 Session 是老古董什么都想换成 JWT。实际上Session 方案至今还在大量项目里稳定运行根本原因是没有一个方案是免费的午餐。我把两种方案的核心差异整理成一张表| 对比项 | Session Cookie | JWT | | 服务端存储 | 需要通常内存或缓存中间件 | 不需要Token 自包含 | | 状态性 | 有状态 | 无状态 | | 横向扩展 | 需要共享会话存储或粘性会话 | 天然友好任意节点都能验签 | | 主动踢人/注销 | 删除服务端 Session 立即生效 | 签名有效期内很难立即失效 | | 请求体量 | Cookie 里只放一个会话ID极小 | Header Payload Signature 整体携带Token 越长请求越大 | | CSRF 风险 | 主流方案是 HttpOnly Cookie需要防 CSRF | 放 Authorization 头时不依赖 Cookie风险相对小 | | 在线状态可见性 | 服务端能统计在线用户 | 服务端很难准确知道谁在线 |从这张表能明显看出JWT 的核心优势在扩展性和跨系统验证不是“绝对更好的登录方案”。如果你的业务里有“管理员要把某个用户踢下线”这种强需求标准 JWT 确实很难处理好因为你没有服务端记录可删。4.2 哪些场景用 JWT 确实舒服结合长期使用经验我建议优先考虑 JWT 的场景主要有几类。第一前后端彻底分离同一套接口要同时支撑 Web、小程序、App。Session 方案里 Cookie 依赖浏览器自动携带App 端没有这个机制改造起来特别别扭。JWT 用 Authorization 头在不同端上行为完全一致更统一。第二微服务架构下用户登录某个服务后其他服务也要知道用户身份。JWT 可以在网关处统一校验并向下游透传下游服务只验签即可不用每次都往一个中心会话服务跑。第三第三方临时授权。比如给外部合作方发一个有效期几小时、限定接口范围的 Token到期自动失效实现起来非常干脆。4.3 哪些场景别硬上 JWT反过来看下面几个场景要慎重。单体应用机器不多用户规模也不大但需要很复杂的用户状态管理比如强制下线、封号、多端互踢。这种情况 Session 加服务端缓存的成本更低效果也更直接。接口对请求体积特别敏感或者一个页面要并发几十个接口。JWT 的体积天然比短小的 Session ID 大数据一多带宽压力会明显起来。信息保密要求极高的场景。JWT 本身不加密里面的信息可读敏感内容要么不放要么额外加密。如果你发现什么敏感数据都非要往 Token 里塞那就得重新考虑它是不是合适的载体。5. 新手最容易踩的 5 个坑都是实测实录5.1 把 Payload 当成加密保险箱我第一次正经用 JWT 时也想当然地把用户手机号、邮箱塞进了 Payload。结果后来用一个在线解析工具一解码明文全出来了。很多人对 JWT 有误解以为它是加密令牌。其实 Header 和 Payload 都不具备保密性。要传递敏感信息要么不放要么在做额外对称加密之后再放或者干脆把数据都留在服务端只在 Token 里保留一个不敏感的用户ID。5.2 过期时间设置成“永不过期”我在维护某个项目时见过 exp 被设置为 2030 年还有写成 0 的。有些解析库对 exp 为 0 的解释是“不过期”结果 Token 基本等于永久有效测试账号怎么都踢不掉翻代码才发现是过期时间设置的问题。合理的做法是区分 access token 和 refresh tokenaccess token 有效期短一点15 分钟到 2 小时refresh token 单独签发一个较长有效期只用于换取新的 access token。这样即使 access token 泄露被利用的时间窗口也非常有限。5.3 注销功能不知道怎么实现因为 JWT 没有服务端状态所以“删除 Token”这个动作本身不存在。用户点退出后想让旧 Token 立即失效需要额外设计。最简单的方案是引入黑名单缓存用户注销时把该 Token 的 jti 标识或过期时间点写进缓存校验时先查一下是否已经在黑名单里。另一个正向做法是给每个用户维护一个 version 字段签发 Token 时把 version 写进去重置密码或封号时给 version 加一旧 Token 里的 version 对不上自然失效。这些方案都会让 JWT 带一点点服务端状态但业务必要的时候该加还得加。5.4 存储位置没想清楚被 XSS 或 CSRF 打穿Token 放哪里是安全上很关键的决策。放 localStorage前端发 Authorization 头很方便但页面里任何一个 XSS 漏洞都可能把 Token 偷走。放 Cookie 并设置 HttpOnly能挡住 XSS 直接读取但 Cookie 会自动随请求发送又会引入 CSRF 风险得靠 SameSite、校验 Origin 这些办法防护。我的个人倾向是如果项目是纯 API 服务接口不依赖 Cookie 自动携带可以放 localStorage配合严格 CSP 做好 XSS 防护如果是传统页面应用本身就有 Cookie 会话优先 HttpOnly Cookie 加 SameSite同时补上 CSRF 校验。5.5 服务端没有按白名单校验算法JWT 标准里有一类历史遗留坑叫“算法混淆漏洞”。攻击者把 Header 里的 alg 改成 none或者把 HS256 改成 RS256诱导服务端用错误方式校验。如果服务端直接信任 Token 里的 alg 字段也没有限制算法就可能被绕过。严格的服务端不应该盲目信任客户端传来的算法而应该写死当前系统只接受哪几种算法。另外secret 强度太弱也会出问题。我见过有人把密钥字符串设置得和项目名一样这种弱密钥很容易被暴力碰撞出来。密钥应当足够长、足够随机并且放在服务端环境变量或专门的密钥管理服务里绝不能跟着代码提交到仓库。6. 写在最后的几条实战建议6.1 入门阶段我推荐怎么学如果你刚接触 JWT我建议先别急着套一个封装得很好的中间件。第一步用自己熟悉的语言亲手写一遍生成和校验第二步做几个破坏性实验改一下 Payload 里的用户名、把签名段随便改一个字符、把过期时间改成过去的时间然后观察服务端返回什么。亲眼见过校验失败的过程比背十篇博客都有用。我当时就是用模拟项目反复折腾才真正理解签名到底在保护什么。6.2 判断项目要不要上 JWT就我个人习惯来说判断一个团队适不适合上 JWT我会先问一句话你会不会经常需要把某个用户强制下线如果答案是经常标准 JWT 就别硬上老老实实引入状态方案。如果主要是接口鉴权、跨系统身份传递那 JWT 确实能帮架构省掉不少事。下一篇我准备用一个完整的模拟项目把 JWT 接入登录、刷新、权限控制的完整链路演示一遍。前面这些基础理解到位了下一篇就可以直接跟着动手做了。