既然服务器像金鱼只有 7 秒记忆,那就把 JWT 纹在身上吧
JWT 全家桶指南:从入门到“防脱发”实战 🧴
今天咱们展开讲讲那个让微服务起飞、让前端喊爽、让安全团队皱眉头的 JWT (JSON Web Token)。
如果你还在纠结“Token 到底存 Cookie 还是 LocalStorage”,或者被“Token 过期了用户填了一半的表单全丢了”这种 bug 搞得想辞职,那你来对地方了。
1. 为什么 Session 被嫌弃了?(前情回顾)
先给不知道前因后果的兄弟补个课。
传统的 Session 模式是这样的:
用户:我登录了!
服务器:好的,我在内存里记下来 SessionID_888 = 王大锤,然后把 SessionID_888 塞给你。
用户(下次来):我是 SessionID_888。
服务器:等等,我翻翻内存... 哦,你是王大锤。
这在单机时代很完美。但到了微服务时代,这简直是灾难:
-
内存爆炸:几百万用户在线,服务器内存直接被 Session 撑爆。
-
分布式噩梦:你的 Session 在服务器 A,结果负载均衡把你甩到了服务器 B。服务器 B:“你谁啊?滚去登录!”(虽然可以用 Redis 集中存储,但每次请求都查 Redis 也是网络开销啊)。
-
跨域想死:Cookie 在跨域场景下有多难搞,写过 CORS 配置的都懂,那是会呼吸的痛。
2. JWT 的“无状态”哲学
JWT 的逻辑是 “以证代记”。服务器不再记账,而是给你发个自带防伪水印的工牌。
这个工牌(Token)里写着:
-
我是谁(UserId)
-
我是干啥的(Role)
-
我什么时候过期(Exp)
-
服务器的亲笔签名(Signature)
下次你带着工牌来,服务器只需要算一下签名对不对。
“嗯,签名是我的,没被改过,放行!”
全程不需要查库,不需要查 Redis,不需要耗费 IO,CPU 算一下算力就完事。这就是**无状态(Stateless)**的快乐。
3. 解剖 JWT:这串乱码里到底有啥?
Token 看起来像这样:aaaaa.bbbbb.ccccc。
别怕,拿去 Base64 解码一下,它就是个 JSON 对象。
Part 1: Header(头部)
告诉服务器:“我是 JWT,我用的签名算法是 HS256。” 没啥花头。
Part 2: Payload(负载)—— 核心数据区
这里是存数据的地方。除了官方定义的 exp(过期时间)、iss(签发人),你可以在这里塞自定义数据。
JSON
{
"uid": "10086",
"role": "admin",
"nickname": "暴躁程序员"
}
🔴 高危警告:
我见过有人把 password 甚至 手机号 放在 Payload 里。
兄弟,住手! Base64 是可以被还原的!任何人截获了这个 Token,都能看到里面的数据。
JWT 只是防篡改,不是防偷窥! 除非你对 Payload 进行了加密(JWE),否则别放敏感数据。
Part 3: Signature(签名)—— 防伪印章
这是 JWT 的灵魂。
Hash(Header + Payload + 你的私钥Secret)
只有服务器知道 Secret 是啥。如果黑客想把 Payload 里的 role: user 改成 role: admin,但他不知道 Secret,算出来的签名就和最后一段对不上。服务器一验签:“何方妖孽,竟敢造假!” 直接 401。
4. 世纪难题:JWT 到底存哪里?🏠
这是一个引发了无数次键盘战争的话题。
前端兄弟拿到 Token 后,该把它塞哪儿?
选手 A:LocalStorage / SessionStorage
-
做法:前端收到 Token,
localStorage.setItem('token', ...),每次发请求在 Header 里带上Authorization: Bearer ...。 -
优点:简单,前端想怎么拿怎么拿,不惧跨域。
-
致命弱点:XSS(跨站脚本攻击)。
如果你的网站被注入了恶意 JS 代码(比如你有漏洞,或者你引用的第三方 npm 包有毒),黑客一行代码 const token = localStorage.getItem('token') 就能把你用户的身份窃取走。
这就像把你家钥匙挂在门口的挂钩上,方便是方便,但小偷也方便。
选手 B:Cookie (HttpOnly)
-
做法:服务器设置
Set-Cookie: token=...; HttpOnly; Secure。 -
优点:JS 读不到! 就算有 XSS 漏洞,黑客的代码也拿不到 Cookie 里的 Token。
-
致命弱点:CSRF(跨站请求伪造)。
虽然黑客拿不到 Token,但他可以引诱你点一个链接,利用浏览器自动带 Cookie 的特性,替你发请求(比如 POST /api/delete_account)。
不过现在的框架(React/Vue/Spring Security)防 CSRF 都有成熟方案,相对好解决。
裁判建议:
为了安全,推荐 HttpOnly Cookie。
如果非要存 LocalStorage,请确保你的 XSS 防护做得滴水不漏(但这很难,真的)。
5. 进阶玩法:双 Token 机制(Access + Refresh)🔄
用 JWT 最头疼的是啥?
过期时间怎么定?
-
定长了(比如 7 天):万一 Token 被偷了,黑客能爽 7 天,你毫无办法(因为无法撤销)。
-
定短了(比如 15 分钟):用户正填着表单呢,突然弹出来“请重新登录”,用户想顺着网线过来打你。
为了解决这个矛盾,咱们得用双 Token 策略(也就是 OAuth2 的那一套):
-
Access Token(短命鬼):
-
有效期:15 分钟。
-
作用:平时请求接口就用它。
-
特点:即使被偷,黑客也只能用十几分钟。
-
-
Refresh Token(长命锁):
-
有效期:7 天(甚至更久)。
-
作用:专门用来换新的 Access Token。
-
存储:存在数据库/Redis 里,且只在“刷新接口”能用。
-
丝滑的体验流程:
-
前端发请求,Access Token 过期了,服务器返回 401。
-
前端静默(用户无感知)拿着 Refresh Token 去找服务器:“大哥,续个杯。”
-
服务器查一下 Refresh Token:
-
在库里吗?在。
-
被封号了吗?没有。
-
好,给你个新的 Access Token(再续 15 分钟)。
-
-
前端拿到新 Token,重发刚才失败的请求。
-
用户觉得:哇,这系统真稳,永远不掉线!
如果你想踢人下线?
只需要在数据库里把他的 Refresh Token 删掉/标记失效。
等他手里的短命 Access Token(最多 15 分钟)过期后,他来续杯时,服务器冷冷一笑:“Refresh Token 无效,滚去登录。”
完美解决了 JWT 无法注销的痛点!
6. JWT 的那些“坑” 🕳️
有些坑,真的是谁踩谁知道:
-
Alg: None 攻击:
早期的 JWT 库有个弱智 Bug。如果你把 Header 里的 alg 改成 none,然后把 Signature 删掉。后端验签时竟然直接通过了!
(现在的成熟库都修复了,但自己写轮子的兄弟要注意)
-
Secret 太弱:
你的私钥是不是叫 123456 或者 mysecret?
黑客用显卡跑个字典,几秒钟就能暴力破解出你的私钥,然后给自己签发一个 admin 的 Token。
私钥一定要长!长!长!最好是随机生成的 64 位以上乱码。
-
Token 体积太大:
别什么破烂都往 Payload 里塞。Token 是要放在 HTTP Header 里的,太大了每次请求都费流量。就像你出门买菜不需要把房产证背在身上一样。
7. 总结一下
JWT 不是银弹,它也有权衡。
-
如果你做的是银行 App:老老实实回服务器查 Session 吧,安全第一,麻烦点没关系。
-
如果你做的是普通互联网 App/微服务/SPA:JWT + 双 Token 机制是目前的标准答案。
最后送大家一句话:
Token 就像钞票,离身即失效,防伪靠签名,丢了找不回。
写代码去吧,记得把 Secret Key 藏好,别硬编码在 Git 里,不然安全团队真的会拿刀顺着网线来找你的。








