JWT的工作流程
第一部分:密码验证
密码验证的核心原则是:服务器绝对不能也无需存储用户的明文密码。存储明文密码是极其危险的,一旦数据库泄露,所有用户密码都将暴露。
标准流程(使用哈希加盐)
-
用户注册
- 步骤1: 用户在客户端(如网页、App)输入用户名和密码(明文)。
- 步骤2: 客户端将注册信息(包含明文密码)通过 HTTPS 安全地发送到服务器。
- 步骤3: 服务器收到请求后,并不会直接存储密码。而是会进行以下操作:
- 生成一个随机“盐”(Salt):一个长而随机的字符串,每个用户的盐都不同。
- 将盐与明文密码组合:例如
salt + password
。 - 使用哈希函数(如 bcrypt, scrypt, Argon2)进行加密:将组合后的字符串通过哈希函数计算,生成一个固定长度的、看起来毫无规律的“密码哈希值”。
- 存储信息:将
用户名
、密码哈希值
和盐
一起存入数据库。
用户名 密码(数据库中存储的) 盐 alice $2b$10$N9qo8u...iuHS01
N9qo8u...iuHS01
(通常和哈希值存在一起)bob $2b$10$s6B6f7...fG2sA2
s6B6f7...fG2sA2
为什么用 bcrypt 而不是 MD5/SHA-256?
bcrypt 是专门为密码设计的哈希算法,它速度很慢(可以通过“工作因子”调整),并且内置了盐的处理,能有效抵御暴力破解和彩虹表攻击。而 MD5/SHA 速度太快,不适合用于密码哈希。 -
用户登录
- 步骤1: 用户在客户端输入用户名和密码(明文)。
- 步骤2: 客户端通过 HTTPS 将登录信息发送到服务器。
- 步骤3: 服务器根据用户名从数据库中查找对应的记录。
- 如果找不到用户,返回错误“用户名或密码错误”。
- 步骤4: 服务器进行密码验证(这是最关键的一步):
- 从数据库中找到该用户对应的
盐
。 - 将接收到的明文密码与这个
盐
进行组合。 - 使用相同的哈希函数(如 bcrypt)对组合后的字符串进行哈希计算。
- 将计算出的新哈希值与数据库中存储的旧哈希值进行比对。
- 从数据库中找到该用户对应的
- 步骤5: 如果两个哈希值完全一致,则证明密码正确;否则,密码错误。
核心思想:服务器不比较密码本身,而是比较用同样方法和盐计算出的哈希值。即使两个用户密码相同,因为他们的盐不同,最终存储的哈希值也完全不同。
第二部分:JWT (JSON Web Token) 流程
密码验证成功后,服务器需要一种方式来记住用户已经登录过了。传统的做法是用 Session/Cookie,而 JWT 是一种更现代、无状态的替代方案。
JWT 的本质是一个数字签名的令牌,它本身就包含了用户的身份信息(称为“声明”)。
JWT 的组成
一个 JWT 看起来像这样:xxxxx.yyyyy.zzzzz
,它由三部分组成,用点号分隔:
-
Header (头部)
- 通常包含令牌类型(
typ: "JWT"
)和所用的签名算法(alg: "HS256"
)。 - 这是一个 Base64Url 编码的 JSON 字符串。
- 通常包含令牌类型(
-
Payload (有效载荷)
- 包含你要发送的“声明”(用户信息和其他数据)。常见的声明有:
sub
(Subject):用户IDname
:用户名iat
(Issued At):令牌签发时间exp
(Expiration Time):令牌过期时间
- 注意:Payload 只是经过 Base64Url 编码,并没有加密**。任何人都可以解码看到内容。所以绝不能在里面存放敏感信息(如密码)。
- 包含你要发送的“声明”(用户信息和其他数据)。常见的声明有:
-
Signature (签名)
- 这是 JWT 的安全核心。签名用于验证消息在整个过程中没有被篡改。
- 生成方式:
签名 = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
- 服务器用一个只有自己知道的
密钥(secret)
来生成这个签名。如果头部或有效载荷被修改,没有密钥就无法生成匹配的新签名,验证就会失败。
#### JWT 的工作流程(登录成功后)
-
签发 Token (登录成功时)
- 在密码验证通过后,服务器端生成 JWT。
- Payload 中会包含用户身份信息,如
{ "sub": "12345", "name": "alice", "exp": 1735689600 }
。 - 用 Header 里声明的算法和服务器自己的
secret
密钥生成签名。 - 将三部分组合成一个字符串
JWT Token
,返回给客户端(通常放在 HTTP 响应的 Body 或一个 Header 里)。
-
客户端存储和发送 Token
- 客户端(通常是浏览器)收到 JWT 后,需要把它存储起来(常见做法是存在
localStorage
、sessionStorage
或Cookie
中)。 - 此后,客户端每次向服务器请求需要认证的 API(如获取个人资料),都必须在 HTTP 请求的
Authorization
头部带上这个 Token:
Authorization: Bearer <你的JWT令牌>
- 客户端(通常是浏览器)收到 JWT 后,需要把它存储起来(常见做法是存在
-
验证 Token (访问受保护接口时)
- 服务器收到请求后,从
Authorization
头中取出 JWT。 - 验证签名:这是最关键的一步。服务器用自己的
secret
密钥,按照 Header 中指定的算法,对收到的 Header 和 Payload 部分重新计算一次签名。 - 比对签名:将计算出的新签名与 JWT 自带的第三部分(Signature)进行比对。
- 如果签名不一致:说明 Token 被篡改了,立即拒绝请求。
- 如果签名一致:证明 Token 是有效且未被篡改的,服务器可以信任 Payload 中的信息。
- (可选)检查其他声明:检查 Token 是否过期(
exp
)、是否提前生效(nbf
)等。 - 验证全部通过后,服务器就从 Payload 中的
sub
(用户ID) 知道这个请求是谁发出的,然后处理业务逻辑并返回数据。
- 服务器收到请求后,从