JWT 是什么?从登录认证说起
在做 Web 开发时,登录认证几乎是绕不开的一部分。
用户输入账号密码登录之后,服务器总得想办法记住“这个请求是谁发过来的”。传统做法通常是 Session:用户登录成功后,服务器创建一份会话数据,同时给浏览器一个 Session ID。之后浏览器每次请求都带上这个 ID,服务器再根据 ID 找到对应的用户信息。
JWT(JSON Web Token)则提供了另一种思路:服务器不再依赖 Session ID 去查询一份会话数据,而是直接生成一个 Token,把一些必要的信息放进 Token 中,再交给客户端保存。
客户端之后访问接口时,只需要携带这个 Token,服务器验证 Token 合法之后,就可以知道当前请求对应的是哪个用户。
这也是 JWT 在前后端分离项目中非常常见的原因。
JWT 长什么样
一个典型的 JWT 大概是这样的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMDAxIiwibmFtZSI6IlRvbSIsImlhdCI6MTcxMDAwMDAwMH0
.
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
看起来很长,但实际上它只有三个部分:
Header.Payload.Signature
也就是:
头部.载荷.签名
三个部分之间使用 . 分隔。
JWT 的很多设计其实都围绕这三部分展开。
Header:告诉服务器这个 Token 怎么生成的
第一部分 Header 一般用来描述 JWT 本身,例如:
{
"alg": "HS256",
"typ": "JWT"
}
其中:
alg
表示签名使用的算法,例如 HS256、RS256。
而:
typ
一般表示当前 Token 的类型是 JWT。
这个 JSON 最后会经过 Base64URL 编码,变成 JWT 中的第一段。
需要注意的是,Base64 并不是加密。
也就是说,Header 里面的内容实际上是可以直接解码查看的。
Payload:真正保存数据的地方
JWT 第二部分叫 Payload,也就是载荷。
比如:
{
"sub": "1001",
"name": "Tom",
"role": "admin",
"iat": 1710000000,
"exp": 1710003600
}
这里通常会保存一些与用户身份相关的信息。
例如:
sub
表示 Token 对应的主体,很多项目会在这里保存用户 ID。
iat 表示 Token 的签发时间,exp 表示 Token 的过期时间。
当然,也可以放一些自己的字段,比如:
{
"userId": 1001,
"username": "Tom",
"role": "admin"
}
这样服务器收到 JWT 后,就可以直接读取用户 ID 和角色,而不一定需要每次都根据 Session ID 查询会话。
不过这里有一个很容易误解的地方:
Payload 不是加密的。
它和 Header 一样,本质上只是进行了 Base64URL 编码。
所以 JWT 中不应该保存密码、身份证号、银行卡信息之类的敏感数据。
只要拿到了 Token,任何人理论上都可以解码出 Payload。
Signature:JWT 真正重要的部分
如果 Header 和 Payload 都可以被解码,那是不是意味着用户可以直接修改 JWT?
例如把:
{
"role": "user"
}
改成:
{
"role": "admin"
}
单纯修改 Payload 确实很容易。
但问题在于,JWT 还有第三部分:Signature,也就是签名。
以 HS256 为例,签名的逻辑可以简单理解成:
HMACSHA256(
Base64Url(Header) + "." + Base64Url(Payload),
secret
)
这里的 secret 是服务器自己保存的密钥。
例如服务器生成 Token 时使用:
my-super-secret-key
攻击者虽然可以修改 Payload,却不知道服务器的密钥,因此无法生成正确的新签名。
服务器收到 JWT 后,会重新计算一次签名。
如果计算出来的结果和 JWT 中携带的 Signature 不一样,就说明这个 Token 被修改过了,服务器可以直接拒绝请求。
所以 JWT 的签名解决的并不是“别人看不到数据”,而是:
证明这份数据没有被篡改。
这两者是完全不同的概念。
JWT 登录流程
假设现在有一个登录接口:
POST /login
用户提交:
{
"username": "tom",
"password": "123456"
}
服务器首先检查用户名和密码。
如果验证成功,就生成一个 JWT:
eyJhbGciOiJIUzI1NiJ9.xxxxx.xxxxx
然后返回给客户端:
{
"token": "eyJhbGciOiJIUzI1NiJ9.xxxxx.xxxxx"
}
客户端拿到 Token 后保存下来。
之后访问需要登录的接口,例如:
GET /api/user
客户端通常会在 HTTP 请求头中加入:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx.xxxxx
服务器收到请求后,从 Authorization 中取出 JWT。
然后检查:
- Token 的格式是否正确
- 签名是否正确
- Token 是否已经过期
如果验证全部通过,就认为这个 Token 是可信的。
之后便可以从 Payload 中取出用户 ID:
userId = 1001
再继续处理当前请求。
JWT 为什么适合前后端分离
传统 Session 模式下,真正的登录状态通常保存在服务器。
例如:
Session ID -> 用户信息
服务器收到请求之后,需要根据 Session ID 找到对应 Session。
如果系统只有一台服务器,这没有什么问题。
但如果以后变成:
客户端
↓
负载均衡
↓
服务器 A
服务器 B
服务器 C
问题就出现了。
用户登录时 Session 可能创建在服务器 A 上,而下一次请求却被负载均衡发送到了服务器 B。
服务器 B 并没有这份 Session。
当然,这个问题可以通过 Redis 共享 Session 等方式解决。
JWT 的思路则不同。
用户身份信息本身就在 Token 中:
客户端
↓
JWT
↓
任意服务器
只要所有服务器都拥有验证 JWT 所需要的密钥,就可以判断这个 Token 是否有效。
因此 JWT 天然比较适合分布式 API 和前后端分离场景。
JWT 并不是绝对的“无状态”
JWT 经常被描述为“无状态认证”。
从服务器不保存 Session 这个角度来说,这个说法没有问题。
但真正做项目的时候,事情往往没有那么简单。
比如一个 JWT 的有效期是 7 天:
{
"exp": 1710604800
}
如果用户第二天主动退出登录怎么办?
客户端当然可以把 Token 删除。
但如果这个 Token 已经被别人复制了,那么它理论上仍然可以继续使用,直到 7 天后过期。
因为服务器只检查:
签名正确吗?
过期了吗?
两个答案都是正常的。
服务器并不知道这个 Token 已经“退出登录”。
如果想让某个 JWT 提前失效,就可能需要维护 Token 黑名单,例如放进 Redis:
token -> revoked
这时候系统又出现了服务器端状态。
因此,与其说 JWT 一定是无状态的,不如说:
JWT 允许我们实现无状态认证,但具体系统是否真的无状态,还是取决于业务设计。
Access Token 和 Refresh Token
实际项目中一般也不建议直接发一个有效期几个月的 JWT。
因为 Token 的有效期越长,泄露之后造成的风险越大。
比较常见的方式是同时使用:
Access Token
Refresh Token
Access Token 的有效期比较短。
例如:
15 分钟
30 分钟
1 小时
平时调用业务接口都使用 Access Token。
而 Refresh Token 的有效期比较长,比如:
7 天
30 天
当 Access Token 过期之后,客户端拿 Refresh Token 请求服务器:
POST /refresh
服务器验证 Refresh Token 后,重新签发新的 Access Token。
这样即使 Access Token 被泄露,攻击者能够利用它的时间也比较有限。
而 Refresh Token 因为权限更敏感,通常需要更加谨慎地保存和管理。
JWT 放在哪里比较合适
浏览器项目里经常可以看到两种做法。
一种是:
localStorage
然后 JavaScript 主动读取 Token,并放进请求头:
Authorization: Bearer xxx
这种方式实现起来非常简单,但如果网站出现 XSS 漏洞,恶意 JavaScript 就有机会读取 localStorage 中的 Token。
另一种方式是把 Token 放进 Cookie,并设置:
HttpOnly
Secure
SameSite
其中 HttpOnly 可以阻止普通 JavaScript 直接读取 Cookie。
不过 Cookie 又涉及 CSRF 等问题,因此实际项目中不能简单地认为某一种方式绝对安全。
Token 放在哪里,本质上还是要结合系统架构和安全模型来设计。
JWT 和 Session 到底选哪个
JWT 出现之后,有一段时间经常能看到一种说法:
JWT 比 Session 更先进。
其实并不是这样。
JWT 和 Session 解决的是类似的问题,但设计思路不同。
Session 更像是:
客户端保存一个钥匙编号
服务器保存真正的数据
JWT 更像是:
客户端携带一张服务器签名的身份证明
服务器负责检查这张证明是不是真的
Session 的优势是登录状态更容易控制。
例如管理员想强制某个用户退出,只需要删除服务器上的 Session。
而 JWT 在完全无状态的情况下,一旦签发,在过期之前通常很难主动撤销。
JWT 的优势则是服务器之间不需要共享 Session,比较适合 API、微服务和分布式系统。
所以在实际项目中,不应该简单地问:
JWT 和 Session 谁更好?
更准确的问题应该是:
当前系统更适合哪一种认证方式?
最后
JWT 本身其实并不复杂。
它最核心的结构只有:
Header.Payload.Signature
Header 描述 Token 和签名算法,Payload 保存需要传递的数据,而 Signature 用来证明前面的数据没有被篡改。
真正需要注意的是,不要把 JWT 当成一种“加密用户数据”的技术。
JWT 默认情况下并不会隐藏 Payload。
它真正解决的问题,是让服务器能够验证:
这份 Token 是我签发的,而且里面的数据没有被修改。
理解这一点之后,再去看 JWT 的登录认证、权限校验、Access Token 和 Refresh Token,就会容易很多。
JWT 只是认证系统中的一个工具,而不是完整的安全方案。真正应用到项目里时,Token 的有效期、存储位置、撤销机制、刷新机制以及 HTTPS,往往比“用了 JWT”本身更加重要。
- 点赞
- 收藏
- 关注作者
评论(0)