
JWT这东西做服务端开发的肯定不陌生。我印象很深的是刚工作那会儿第一次在项目代码里看到一长串密密麻麻的token长得像乱码夹着几个英文单词塞进请求头里就能验证身份。当时我盯着那串字符琢磨了半天服务端到底是怎么认出这个token的别人把它改一改服务端能不能发现后来花了几天时间把JWT从结构到原理整个捋了一遍才算是真正开了窍。如果你也在学JWT或者只是看着同事代码里那行“Authorization: Bearer xxx”有点发懵这篇文章就是为你准备的。我按自己的学习路径来写先搞明白它到底解决什么问题再拆开看它长什么样最后说清楚它为什么能保证安全。这一篇不急着写代码重点是建立完整的认知框架把“是什么”这层窗户纸捅破。1. 先从最常见的场景说起登录状态到底是谁记住的1.1 HTTP协议天生“记性差”要理解JWT得先回到最原始的Web交互场景。用过抓包工具或者看过浏览器开发者工具的朋友都知道HTTP协议本身是一个“无状态”的协议。什么叫无状态就是你每次发请求服务器看待你都像看待一个陌生人。我发第一次请求说“我是A用户”服务器处理完就把这件事忘了发第二次请求说“我是A用户”服务器依旧不记得你是谁。这带来的问题太明显了一个电商网站你辛辛苦苦把商品加进了购物车刷新一下页面购物车空了。你登录了账号点进个人中心结果它让你重新登录。没有“状态”这个概念所有需要记住用户身份的功能全都玩不转。所以从很早开始业界就在解决一个问题怎么让HTTP请求“记住”用户1.2 传统方案基于Session的会话保持最常见的传统方案是Session。我在很多项目里都接触过这套机制流程大概是这样的用户提交用户名密码服务端验证通过后在服务器内存或者Redis里存一份会话数据生成一个会话IDSession ID。这个ID通过响应头里的Set-Cookie下发给浏览器。浏览器收到后在后续请求的Cookie字段里自动带回这个ID。服务端拿到ID去内存或Redis里一查发现这个会话有效就放行。在整个链路里真正的用户数据存在服务端这边客户端手里只有一把“钥匙”叫Session ID。钥匙本身不包含任何用户信息甚至没有实际价值。这套机制在单体应用时代非常好用。但我后来在几个项目里逐步发现它有两个让人头疼的地方第一服务端需要保存Session数据。用户量一大这些会话数据占内存也占存储。更麻烦的是如果你部署了多台服务器用户在A服务器登录了下次请求被负载均衡分到B服务器B服务器那没有这个Session用户就掉线了。解决办法是搞Session共享把Session数据从各台服务器挪出来放到统一的Redis集群里。这确实能解决问题但多了一层基础设施链路也复杂了不少。第二Cookie在跨域场景下非常折腾。现在的项目尤其前后端分离的项目前端页面在A域名后端接口在B域名跨域请求里处理Cookie要额外配置域名、路径、SameSite之类的属性。你要是再做移动端AppApp里面根本就不是Cookie那套机制了还得手动管理Session ID的存储。这些问题叠加在一起业内自然开始找新的方案。JWT就是在这个背景下被越来越多人采用的一种替代思路。2. JWT到底是什么把三段结构拆开揉碎2.1 先看一眼实物JWT全称是JSON Web Token。按字面理解它是一个基于JSON格式的、用于网络身份认证的令牌Token。我在项目里见过很多次实体它长这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c猛一看就是三节乱码中间用两个英文句号..隔开所以JWT的整体结构是“三段式两分隔”。每一段分别叫Header头部、Payload载荷、Signature签名合在一起就是Header.Payload.Signature的格式。2.2 拆开看第一段Header把第一段解出来其实是个很简单的JSON{ alg: HS256, typ: JWT }这里面有两个字段。alg告诉服务端“我这个Token是用什么算法签的名”比如HS256表示HMAC-SHA256RS256表示RSA-SHA256。typ说明这是一个JWT类型的令牌。这个JSON经过Base64Url编码就成了我们在JWT里看到的第一段字符串。注意这里只是编码不是加密任何人都能把它解码出来。2.3 再拆第二段Payload第二段Payload是JWT里携带信息的核心位置里面存放着所谓的“声明”Claim。Claim就是“关于用户和Token本身的一些陈述”。常见的标准声明有这些subSubject这个Token的主题一般存用户IDiatIssued At签发时间时间戳格式expExpiration Time过期时间过了这个时间Token失效nbfNot Before在这之前不可用配合时间敏感的场景issIssuer签发者标识是谁签发的这个TokenaudAudience接收方标识Token是给谁用的jtiJWT IDToken的唯一标识除了标准声明我们完全可以往Payload里塞自定义字段。比如想在前端页面显示用户名就可以加一个username: 张三进去想区分用户角色权限可以加一个role: admin。一个实际的Payload解码出来长这样{ sub: 1234567890, name: John Doe, iat: 1516239022, exp: 1516242622, role: admin }同样这段JSON也是Base64Url编码明文可读。这里有个关键点我后面会反复强调Payload里的信息是任何人都能解的所以千万别把密码、身份证号、手机号这类敏感信息直接放进去。2.4 第三段是灵魂Signature前两段编码之后把结果和服务器保存的密钥一起喂给签名算法算出来结果就是第三段Signature。它的计算逻辑大致是signature HMACSHA256( base64urlEncode(header) . base64urlEncode(payload), secret )也就是说签名是对“前两段固定字符串”加上“密钥”做一次哈希计算得到一串定长的输出。这个Signature是JWT防篡改的关键。因为哈希计算有一个特性输入的内容哪怕只改一个字符输出结果就会完全不一样。而攻击者手里没有服务器那把密钥他就算伪造了Header和Payload也算不出一个能通过服务端验证的签名。我经常给朋友打的比方是JWT像一张盖了防伪章的通行证。证件上的信息Payload谁都能看但章是专用章拿不到章就伪造不出同款通行证。服务端验证的时候重新盖一次章比对两次章是否一致一致就说明这张通行证没有被改过。2.5 为什么要用Base64Url而不是普通Base64这里有个小细节值得多说一嘴。网上很多文章会说JWT用Base64编码这话不能说错但不准确。JWT用的是Base64Url变种。普通Base64编码的结果里可能含有、/以及末尾的填充字符。但和/在URL里是有特殊含义的——会被解析成空格/会干扰URL路径分割。JWT经常出现在URL参数、请求头这些地方如果里面夹着这种特殊字符解析起来就容易出问题。Base64Url的解决办法是把替换成-把/替换成_并且去掉末尾的填充。就这三处改动让JWT可以安全地放进URL和Header里不用额外转义。3. 完整走一遍从登录到放行的流程3.1 登录瞬间发生了什么光看结构有点抽象我把整个流程串起来讲一遍你就明白JWT是怎么在系统里流转的了。假设现在有一个前后端分离的订单系统。用户在浏览器里输入用户名密码点击登录。流程开始前端把用户名密码通过HTTPS POST到后端登录接口后端验证账号密码确认无误后取出用户ID把用户ID、角色、过期时间这些信息组装进Payload后端用密钥对Header和Payload做签名生成完整的JWT后端把JWT返回给前端登录流程结束注意看这个过程中服务端并没有在内存或Redis里保存这个Token的任何信息。Token生成完服务端自己就不再“持有”它了。接下来就看客户端怎么用。3.2 客户端拿到Token之后怎么用前端收到这个JWT后通常会把它存起来存哪可以是localStorage、sessionStorage或者cookie各有各的坑我后面专门讲。用户接下来要访问订单列表了前端在发请求时会在HTTP请求头里加上这样一行Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c这个Bearer前缀是一个约定俗成的写法表示后面这个字符串是一个承载令牌Bearer Token。也有一些系统直接用Cookie来携带JWT前端代码不用管浏览器自动带上。但显式放在Authorization头里是目前API对接最通用的方式。3.3 服务端怎么验证一个JWT后端收到请求后不会去数据库里查“这个Token是不是在有效会话列表里”它只需要做三件事把请求头里的Token按.切分成三段拿到Header、Payload、Signature用同样的算法Header里的alg字段会告诉它和服务器密钥对前两段重新计算一次签名把算出来的签名和客户端传来的签名比对一致则验证通过验证通过后服务端还会顺手检查Payload里的exp字段如果当前时间已经超过过期时间就直接拒绝。通过之后服务端就直接信任Payload里携带的用户ID、角色信息用它完成后续的业务逻辑。关键区别在于验证不依赖于查询“存储”。这就是它和Session最大的分水岭。3.4 无状态到底意味着什么Session方案里服务端拿到Session ID后必须“查”一下自己的存储确认会话存在才能放行这个动作叫“有状态认证”。JWT不需要这个查询动作——Token本身就包含了所有验证所需的信息服务端拿着密钥一算就知道Token真假。你只要算一遍签名验一下时间结果就在眼前。这就是常说的“无状态”。无状态带来的直接好处是任意一台服务器都能独立验证Token服务器之间不需要共享任何会话数据。应用需要扩容时随便加机器就行不用考虑Session同步的问题。微服务架构里各个服务各自验签认证逻辑从业务代码里剥离开来这是JWT在分布式环境下越来越流行的核心原因。4. JWT的优点和代价都得看清楚4.1 那些让人舒服的地方我在项目里实际对比过Session和JWTJWT的优势相当明显。跨域和异构场景友好。Session依赖CookieCookies有跨域限制设置起来又得动域名又得配属性。JWT放在Authorization头里不受浏览器同源策略约束前后端分离、多端共用一套API都能很自然地对接受。移动端和原生应用友好。App里面做Session要手动处理会话ID的保存和传递JWT相比之下简单得多拿到就存请求就带没有任何额外机制。天然支持水平扩展。前面说了服务端不存状态扩容时零负担。这个优势在访问量上来之后会体现得非常明显。信息自包含。Payload里带用户ID和角色微服务拿到Token就能直接知道用户是谁、权限多大不需要再回查统一的会话服务。在一个订单服务、用户服务、支付服务各自独立的架构里这种体验简直顺滑。4.2 那些必须付出的代价JWT也不是银弹我自己使用过程中踩过的坑也大多在这些方面。Token无法在失效前主动作废。这是JWT最让人纠结的点。Session方案里管理员把会话从Redis里删掉用户立刻失效。JWT呢只要Token没过期就算用户改了密码就算管理员把用户封了服务端验签仍然是“通过”的除非服务端额外维护一个黑名单那又回到“有状态”的老路上了。Token体积偏大。JWT的Payload里带了用户信息加签名后整体比一个Session ID大很多。每次请求都要在Header里带一个几KB的字符串虽然有Gzip压缩能缓解但在移动端网络环境下这开销是实打实的。风险面前置到客户端。Session方案里客户端只持有一个随机的ID就算被偷了ID本身没暴露什么信息。JWT直接在客户端存了一份可解码的用户信息。所以JWT对存Storage的安全要求更高XSS攻击的风险要专门防范。依赖签名算法和密钥管理。服务端密钥一旦泄露所有Token都能被伪造。而alg字段如果服务端没做校验还有算法混淆攻击的风险。这些在后面安全部分细讲。5. 新手最容易踩的几个坑5.1 把敏感信息直接塞进Payload这是我最想强调的一个坑。JWT的Payload是Base64Url编码不是加密。我见过有同学把用户密码、手机号、身份证号直接塞进Payload理由是“项目里需要用到”完全没有意识到这等于把敏感信息明文送到了客户端手里。正确做法是Payload里只放非敏感的用户标识比如用户ID、角色、过期时间这种信息。真正敏感的数据还是得到服务端按ID去查。一定记住一句话JWT的作用是“验证”和“标识”不是“加密存储”。所有写进Payload的内容默认都是半公开的。5.2 登出接口明明调了Token还是能用这是JWT新手几乎必踩的坑。前端把Token从本地删掉请求确实带不过去了但这只是“客户端这边不再发送”而已。如果攻击者提前把这个Token抄走了他照样可以用这个Token继续访问接口。服务端层面要做真正的“登出”办法有几种一是维护一个Token黑名单登出时把Token的jti或原始字符串加进黑名单过期时间到了才移除二是缩短Token有效期配合Refresh Token机制轮换三是部署时用网关层统一拦截。各有利弊但核心思想要明确单靠前端删Token不叫真正的登出。5.3 Token有效期设置不合理我见过两种极端一种是把exp设成一小时结果用户用着用着突然掉线体验很差另一种是一不做二不休把过期时间设成一年甚至干脆不设expToken永久有效出安全问题根本没法收场。后来我实践中常用的模式是双Token方案一个短期Token比如15分钟或1小时负责正常业务访问一个长期Refresh Token比如7天或30天负责快过期时申请新的短期Token。Refresh Token走单独的接口只在需要续期的时候携带被攻击的窗口就小很多。5.4 签名密钥和算法校验HS256和RS256两种算法都有团队在用但它们的密钥体系不一样。HS256是对称加密签名和验证用同一个密钥所以密钥必须妥善保管在服务端绝不能下发到前端。RS256是非对称加密签名用私钥验证用公钥公钥可以公开。正是因为Header里的alg字段是客户端可以改的服务端如果把算法写死只信任自己配置的那一种就不会出问题。如果动态读取alg来验证攻击者可以把alg改成none或者HS256来混淆算法。这类攻击在安全圈真实发生过服务端务必白名单校验算法。5.5 Token存哪里更安全localStorage、sessionStorage还是Cookie这个问题每次都能吵一轮。我的个人实践经验是localStorage简单但容易被XSS脚本读取Token一旦被拿走就是致命的Cookie配合HttpOnly属性脚本代码读不到防XSS效果好但要留意CSRF攻击如果项目XSS面控制得不好用HttpOnly Cookie更放心稳妥的做法是短期Token存内存或比较安全的地方Refresh Token放HttpOnly Cookie尽量减小被窃取的暴露面。具体取舍要看团队对安全风险的接受程度。6. 一份不折腾的学习路线建议6.1 最快的方式跑通一个最小示例学这个东西光看不练真的记不住。我的建议是直接写一个最小的Demo感受一下“签出来、验回去”的过程。用Node.js的jsonwebtoken库几分钟就能跑通。const jwt require(jsonwebtoken); // 模拟Payload const payload { sub: 10001, name: 某开发者, role: admin }; const secret my-secret-key; // 签发默认使用HS256算法 const token jwt.sign(payload, secret, { expiresIn: 2h }); console.log(生成的JWT:); console.log(token); // 验证 try { const decoded jwt.verify(token, secret); console.log(\n验证通过解析结果:); console.log(decoded); } catch (err) { console.log(\n验证失败:, err.message); }你把它save成demo.jsnpm install jsonwebtoken之后node demo.js跑起来控制台会打印出完整的JWT和验证后的对象。把Token复制到 jwt.io 上解码看看直观感受一下Payload的信息是完全可见的。这一步能让你把抽象的结构和实际字符串对上号。6.2 再动手写一段“伪造”Token跑通签发和验证之后我强烈建议你做一个实验把生成的Token随便改一个字符比如把Payload里的某个字母换掉再放进验证代码里跑一次。你会发现jwt.verify立刻抛错提示签名不匹配。这个“破坏性”实验做完你对“签名防篡改”的记忆会非常牢固。6.3 最后还想多说一句学了JWT之后别急着把项目里的Session全部替换掉。两者各有所长Session在需要“立即失效”场景下有不可替代的优势JWT在分布式、多端场景里更方便。你现在要做的不是站队而是理解它们各自解决什么问题。以后真正面对项目选型时脑子里能有个清晰的判断框架这比多背几个名词有用得多。我自己在好几次项目里从Session切到JWT感受最深的不是它“更安全”而是它“更简单”——它让每个服务都能独立验证请求把认证这件事从业务代码里解耦了出来。但每一次当有人问我“JWT能不能主动注销”我还是会停下来想两秒然后告诉他这就是无状态要付出的代价。把这个问题想明白了你就真正理解了JWT。