JWT 和 传统session登录的区别

387 阅读11分钟

传统登录方式 session模式 (有状态 - Stateful)

验证用户登录成功后,会生成一个唯一的key,key对应的value存着用户的信息。然后把key返回给浏览器(cookie)作为登录凭证

Session ID之所以不会被伪造,核心原因在于:

它被设计成一个通过密码学级别的随机算法生成的、足够长的、无意义的字符串,这使得通过猜测或计算来命中一个有效ID的概率小到了在现实世界中可以忽略不计的程度。

缺点:

  • 当用户量很大的时候,单台服务压力很大,需要的内存也很多。如果服务器挂了,整个登录服务就挂了。
  • 微服务架构,

有状态(Session) :需要专门的存储资源(内存、Redis等)来维护数百万甚至上亿用户的Session数据。这既是存储成本,也是I/O开销

JWT机制(无状态 - Stateless)

无状态”就是服务器在处理请求时,不需要依赖之前请求的状态。每次请求都像第一次见面一样,所有需要的信息都由客户端在本次请求中提供

  1. 登录:  用户提交用户名和密码。
  2. 服务器:  验证通过后,生成一个包含用户标识和权限信息的 JWT。这个 JWT 经过了签名,但服务器不存储这个 JWT
  3. 响应:  服务器将这个 JWT 直接返回给客户端。
  4. 客户端存储:  客户端(如浏览器)自己负责存储这个 JWT,通常放在 localStorage、sessionStorage 或 HTTP Header(Authorization 字段)中。
  5. 后续请求:  客户端在后续的每次请求中,都需要在 Authorization 请求头里附带上这个 JWT。
    Authorization: Bearer
  6. 服务器验证:  服务器收到请求后,从请求头中取出 JWT。它不需要查找任何存储,只需用自己的密钥验证 JWT 的签名是否有效。如果签名有效,服务器就可以信任 Payload 中的信息,从而确定用户的身份和权限

JWT优点

  1. 无状态和可扩展性 (Stateless & Scalable):  这是最大的优势。因为服务器不存储 Session,所以应用可以轻松地进行水平扩展。任何一台服务器都可以处理来自任何用户的请求,只要它们共享相同的密钥。这对于构建分布式系统和微服务架构非常有利。
  2. 跨域认证 (Cross-Domain Authentication):  传统的 Cookie 存在跨域问题(同源策略限制)。而 JWT 因为通常放在 Authorization 请求头中,天然不存在跨域问题,非常适合用于分离的前后端架构(如 SPA + API)或为多个不同的服务提供统一认证。
  3. 自包含性 (Self-Contained):  JWT 的 Payload 中可以包含用户的基本信息(如用户ID、角色),服务端在验证签名后,可以直接从 Payload 中获取这些信息,避免了频繁查询数据库,减轻了数据库的压力。
  4. 适用于多种终端 (Versatile):  不仅限于 Web 应用,JWT 同样适用于移动端(iOS, Android)、桌面应用等,因为其认证方式不依赖于 Cookie。
  5. 解耦 (Decoupling):  认证服务器和业务服务器可以解耦。认证服务器只负责生成和签发 Token,业务服务器只需验证 Token 的有效性即可,职责清晰。

JWT缺点

  1. 无法主动失效 (Cannot be Invalidated Actively):  这是最大的缺点。一旦 JWT 被签发,在它的过期时间(exp)到达之前,它就一直是有效的。如果用户在此期间退出登录或被管理员禁用,服务器无法立即让这个 JWT 失效。
  • 解决方案:

    • 设置较短的过期时间:  配合 Refresh Token 机制来获取新的 JWT。(实践中双token比较多短时效Access Token + 长时效Refresh Token”  是一种在安全性和用户体验之间取得精妙平衡的行业标准实践。它承认了泄露的可能性,并通过限制时间来将风险控制在可接受的范围内
    • 维护一个黑名单 (Blacklist):  将需要失效的 JWT 存入 Redis 或数据库中。每次验证时,先检查该 JWT 是否在黑名单内。但这又破坏了 JWT 的无状态性
  1. 安全性问题 (Security Issues):

    • Payload 明文:  Payload 仅是 Base64 编码,不是加密。敏感信息绝不能存放。
    • 令牌泄露:  如果 JWT 被截获(例如通过 XSS 攻击),攻击者就可以在有效期内冒充用户身份。因此,推荐使用 HTTPS 来加密通信。将 JWT 存储在 HttpOnly 的 Cookie 中可以有效防止 XSS 攻击,但这又会引入 CSRF 风险(需要配合 SameSite 等策略来缓解)。
  2. 令牌体积较大 (Larger Size):  由于包含了 Header 和 Payload 信息,JWT 通常比一个简单的 Session ID 要大得多。每次请求都携带它,会增加网络传输的开销。

  3. 续签问题复杂 (Renewal Complexity):  如果 JWT 过期时间设置得较短,用户在使用过程中 Token 可能会过期,导致体验不佳。通常需要引入一套“Refresh Token”机制来自动续签,这增加了系统的复杂性。Refresh Token 本身需要安全地存储,并且也需要处理其失效和轮换的逻辑。

JWT的工作原理

一、签名的本质

JWT 的签名(Signature)是这样算出来的(以 HS256 为例):

signature = HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

也就是说:

  • 签名是由 header + payload + secret 三者共同决定的;

  • 其中 secret 是 服务器端私有的(攻击者拿不到)。

    非对称签名 非对称的原理就是(用公钥验证这个签名是不是 用私钥签发的,(私钥保密的,所以如果是私钥签发的,说明信息是可以相信的))

非对称签名用 一对密钥私钥(private key)公钥(public key)

  • 签名(Sign) :签发方用 私钥 对消息的哈希值做签名,得到签名值(signature)。私钥必须保密。
    常见流程:digest = Hash(message)signature = Sign_with_private_key(digest)
  • 验证(Verify) :验证方用 公钥 验证签名是否对应该消息(即验证签名对 digest 是否成立)。
    验证成功 → 消息确实由持有私钥的一方签发且在传输中未被篡改。

注意:签名不是加密。签名保证完整性不可否认性/来源证明(在非对称场景下),消息主体通常仍是明文可读的。 # 对称签名 ## 🧩 一、基本概念

  • 对称签名算法:指签名与验证都使用同一把密钥(secret)。
  • 常见算法:HS256HS384HS512,即基于 HMAC(Hash-based Message Authentication Code) 的算法。

所以:签名者和验证者共享同一个 secret。只要知道这个 secret,任何人都能签名和验证。


⚙️ 二、签名(生成 Token)过程

假设要生成一个 JWT,算法为 HS256

1️⃣ 准备三部分

header = {
  "alg": "HS256",
  "typ": "JWT"
}

payload = {
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1690000000
}
secret = "my-very-secret-key"

2️⃣ Base64URL 编码 header 和 payload

base64Header = base64urlEncode(header)
base64Payload = base64urlEncode(payload)

3️⃣ 拼接待签名字符串

message = base64Header + "." + base64Payload

4️⃣ 用 HMAC-SHA256 算法签名

signature = HMACSHA256(message, secret)

计算逻辑:

HMAC = Hash( (secret ⊕ opad) + Hash( (secret ⊕ ipad) + message ) )

HMAC 的原理是:

  • secret 做两次 hash 混合(内层 + 外层),
  • 这样可以避免“长度扩展攻击”,
  • 最终输出一个定长的 hash(32字节)。

5️⃣ 再 base64URL 编码签名

base64Signature = base64urlEncode(signature)

6️⃣ 拼出最终 JWT

jwt = base64Header + "." + base64Payload + "." + base64Signature

✅ 完成签名!服务端通常把这个 token 发给客户端。


🔍 三、验证(认证)过程

当客户端带着 JWT 来访问接口时,比如:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

服务端验证步骤如下:

1️⃣ 拆解 token

token = "header.payload.signature"
[b64Header, b64Payload, b64Signature] = token.split('.')

2️⃣ Base64URL 解码前两段(header、payload)

header = JSON.parse(base64urlDecode(b64Header))
payload = JSON.parse(base64urlDecode(b64Payload))

3️⃣ (可选)检查 header.alg 是否在允许列表中

不要盲信 header.alg,而是检查它是否在白名单,例如:

if (header.alg !== 'HS256') throw new Error('不支持该算法');

4️⃣ 用同一个 secret 重新计算签名

expectedSig = base64urlEncode( HMACSHA256(b64Header + "." + b64Payload, secret) )

5️⃣ 比较签名

常数时间比较(timing-safe compare)

if (expectedSig !== b64Signature) throw new Error('签名不匹配');

6️⃣ 验证 payload 的有效性

- exp(过期时间)> 当前时间?
- nbf(不可用时间)< 当前时间?
- iss / aud / sub 等字段是否匹配?

✅ 通过验证后:

服务器就认为这个 JWT 是由自己签发的合法 token,可以信任其内容。


🔐 四、签名与认证关系图

[签发方]                           [验证方]
   ↓                                   ↓
header + payload                  token(header.payload.signature)
   ↓                                   ↓
HMAC(header.payload, secret)      HMAC(header.payload, secret)
   ↓                                   ↓
 signatureA                          signatureB
   ↓                                   ↓
  比较:signatureA === signatureB  ✅ → 有效

两边都用同一个 secret,如果结果一样,说明 token 未被篡改。


⚠️ 五、安全要点(很关键)

  1. secret 不能泄露:一旦泄露,攻击者可以自己生成合法 token!

  2. 不要信任 header.alg:服务端必须强制指定算法,比如只允许 HS256。

  3. 签名前不要重新 JSON.stringify:要用原始的 base64url 字符串拼接,否则签名会不一致。

  4. 注意 timing attack:用 crypto.timingSafeEqual() 等方法比较签名。

  5. 密钥长度HS256 的 secret 建议至少 32 字节以上。

  6. 密钥轮换:定期更换 secret,可通过 kid(key id) 管理版本。

    jwt中为什么需要用哈希摘要之后,在用私钥加密。直接用私钥加密不就 行了

    这个问题非常棒 👍,说明你已经注意到 JWT 签名的两个阶段

① 先做哈希摘要(如 SHA256)
② 再用私钥(或密钥)加密这个摘要

你问的核心是:

“为什么不直接用私钥把整个内容加密?”

咱来一步步拆开讲 👇


🔹 1. JWT 的签名本质:不是为了保密,而是为了防篡改

JWT(JSON Web Token)中的签名部分(signature)是用于 验证数据完整性身份真实性,而不是为了“加密”内容。

也就是说:

  • 签名 ≠ 加密
  • 签名的目的:别人能看到内容,但不能伪造

JWT 通常长这样:

header.payload.signature

其中:

signature = Sign( base64url(header) + "." + base64url(payload) )

🔹 2. 为什么要“哈希摘要”后再加密

假设我们用 私钥直接加密整个内容(即直接对 header + payload 做私钥加密):

  • ❌ 加密速度慢:非对称加密(如 RSA)只能处理极少字节的数据(通常 < 245 字节)。
  • ❌ 消耗性能:RSA 私钥加密是一个大数运算,成本极高,不适合加密长文本。
  • ❌ 没必要:我们只是要验证“内容没被改”,不需要对内容保密。

所以签名时,先用哈希算法(如 SHA256)对内容生成 定长摘要(digest) ,再对这个摘要用私钥加密。


🔹 3. 为什么“哈希”是签名过程的核心

哈希函数(如 SHA256)有两个重要特性:

  1. 不可逆:不能从摘要反推出原文;
  2. 抗碰撞:不同内容几乎不可能生成相同摘要。

这样一来:

  • 不需要加密整个内容;
  • 一旦原文被改动,摘要立刻变化;
  • 公钥解密后能轻松比对摘要,验证签名是否有效。

🔹 4. 签名验证过程

以 RSA 为例(JWT 中的 RS256 算法):

  1. 生成签名:

    signature = Encrypt_with_private_key( Hash(header + "." + payload) )
    
  2. 验证签名:

    decrypted = Decrypt_with_public_key(signature)
    if decrypted == Hash(header + "." + payload):
        ✅ 签名有效
    else:
        ❌ 被篡改
    

也就是说:

  • 私钥“签名”(加密哈希)
  • 公钥“验签”(解密签名,再比对哈希)

🔹 5. 如果直接用私钥加密整个数据会怎样?

问题结果
加密体积太大RSA 加密长度有限(< 密钥长度),payload 通常远超范围
性能差RSA 加密耗时远高于哈希
意义错误签名只是验证,不需要隐藏内容
无法兼容规范JWT 的签名规范(JWS)明确要求对“哈希值”签名,而非原文

✅ 总结一句话

JWT 用“哈希摘要 + 私钥加密”的方式,是为了 高效、标准、安全地验证数据未被篡改
而不是为了加密内容。
直接用私钥加密原文既慢又不安全,还违反规范。