传统登录方式 session模式 (有状态 - Stateful)
验证用户登录成功后,会生成一个唯一的key,key对应的value存着用户的信息。然后把key返回给浏览器(cookie)作为登录凭证
Session ID之所以不会被伪造,核心原因在于:
它被设计成一个通过密码学级别的随机算法生成的、足够长的、无意义的字符串,这使得通过猜测或计算来命中一个
有效ID的概率小到了在现实世界中可以忽略不计的程度。
缺点:
- 当用户量很大的时候,单台服务压力很大,需要的内存也很多。如果服务器挂了,整个登录服务就挂了。
- 微服务架构,
有状态(Session) :需要专门的存储资源(内存、Redis等)来维护数百万甚至上亿用户的Session数据。这既是存储成本,也是I/O开销
JWT机制(无状态 - Stateless)
无状态"就是服务器在处理请求时,不需要依赖之前请求的状态。每次请求都像第一次见面一样,所有需要的信息都由客户端在本次请求中提供
- 登录: 用户提交用户名和密码。
- 服务器: 验证通过后,生成一个包含用户标识和权限信息的 JWT。这个 JWT 经过了签名,但服务器不存储这个 JWT。
- 响应: 服务器将这个 JWT 直接返回给客户端。
- 客户端存储: 客户端(如浏览器)自己负责存储这个 JWT,通常放在 localStorage、sessionStorage 或 HTTP Header(Authorization 字段)中。
- 后续请求: 客户端在后续的每次请求中,都需要在 Authorization 请求头里附带上这个 JWT。
Authorization: Bearer 服务器验证: 服务器收到请求后,从请求头中取出 JWT。它不需要查找任何存储,只需用自己的密钥验证 JWT 的签名是否有效。如果签名有效,服务器就可以信任 Payload 中的信息,从而确定用户的身份和权限。
JWT优点
- 无状态和可扩展性 (Stateless & Scalable): 这是最大的优势。因为服务器不存储 Session,所以应用可以轻松地进行水平扩展。任何一台服务器都可以处理来自任何用户的请求,只要它们共享相同的密钥。这对于构建分布式系统和微服务架构非常有利。
- 跨域认证 (Cross-Domain Authentication): 传统的 Cookie 存在跨域问题(同源策略限制)。而 JWT 因为通常放在 Authorization 请求头中,天然不存在跨域问题,非常适合用于分离的前后端架构(如 SPA + API)或为多个不同的服务提供统一认证。
- 自包含性 (Self-Contained): JWT 的 Payload 中可以包含用户的基本信息(如用户ID、角色),服务端在验证签名后,可以直接从 Payload 中获取这些信息,避免了频繁查询数据库,减轻了数据库的压力。
- 适用于多种终端 (Versatile): 不仅限于 Web 应用,JWT 同样适用于移动端(iOS, Android)、桌面应用等,因为其认证方式不依赖于 Cookie。
- 解耦 (Decoupling): 认证服务器和业务服务器可以解耦。认证服务器只负责生成和签发 Token,业务服务器只需验证 Token 的有效性即可,职责清晰。
JWT缺点
- 无法主动失效 (Cannot be Invalidated Actively): 这是最大的缺点。一旦 JWT 被签发,在它的过期时间(exp)到达之前,它就一直是有效的。如果用户在此期间退出登录或被管理员禁用,服务器无法立即让这个 JWT 失效。
-
解决方案:
- 设置较短的过期时间: 配合 Refresh Token 机制来获取新的 JWT。(
实践中双token比较多) 短时效Access Token + 长时效Refresh Token" 是一种在安全性和用户体验之间取得精妙平衡的行业标准实践。它承认了泄露的可能性,并通过限制时间来将风险控制在可接受的范围内 - 维护一个黑名单 (Blacklist): 将需要失效的 JWT 存入 Redis 或数据库中。每次验证时,先检查该 JWT 是否在黑名单内。但这又破坏了 JWT 的无状态性。
- 设置较短的过期时间: 配合 Refresh Token 机制来获取新的 JWT。(
-
安全性问题 (Security Issues):
- Payload 明文: Payload 仅是 Base64 编码,不是加密。敏感信息绝不能存放。
- 令牌泄露: 如果 JWT 被截获(例如通过 XSS 攻击),攻击者就可以在有效期内冒充用户身份。因此,推荐使用 HTTPS 来加密通信。将 JWT 存储在 HttpOnly 的 Cookie 中可以有效防止 XSS 攻击,但这又会引入 CSRF 风险(需要配合 SameSite 等策略来缓解)。
-
令牌体积较大 (Larger Size): 由于包含了 Header 和 Payload 信息,JWT 通常比一个简单的 Session ID 要大得多。每次请求都携带它,会增加网络传输的开销。
-
续签问题复杂 (Renewal Complexity): 如果 JWT 过期时间设置得较短,用户在使用过程中 Token 可能会过期,导致体验不佳。通常需要引入一套"Refresh Token"机制来自动续签,这增加了系统的复杂性。Refresh Token 本身需要安全地存储,并且也需要处理其失效和轮换的逻辑。
JWT的工作原理
一、签名的本质
JWT 的签名(Signature)是这样算出来的(以 HS256 为例):
scss
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)。
- 常见算法:
HS256、HS384、HS512,即基于 HMAC(Hash-based Message Authentication Code) 的算法。
所以:签名者和验证者共享同一个
secret。只要知道这个 secret,任何人都能签名和验证。
⚙️ 二、签名(生成 Token)过程
假设要生成一个 JWT,算法为 HS256:
1️⃣ 准备三部分
makefile
header = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"sub": "1234567890",
"name": "John Doe",
"iat": 1690000000
}
secret = "my-very-secret-key"
2️⃣ Base64URL 编码 header 和 payload
ini
base64Header = base64urlEncode(header)
base64Payload = base64urlEncode(payload)
3️⃣ 拼接待签名字符串
ini
message = base64Header + "." + base64Payload
4️⃣ 用 HMAC-SHA256 算法签名
ini
signature = HMACSHA256(message, secret)
计算逻辑:
scss
HMAC = Hash( (secret ⊕ opad) + Hash( (secret ⊕ ipad) + message ) )
HMAC 的原理是:
- 对
secret做两次 hash 混合(内层 + 外层), - 这样可以避免"长度扩展攻击",
- 最终输出一个定长的 hash(32字节)。
5️⃣ 再 base64URL 编码签名
ini
base64Signature = base64urlEncode(signature)
6️⃣ 拼出最终 JWT
ini
jwt = base64Header + "." + base64Payload + "." + base64Signature
✅ 完成签名!服务端通常把这个 token 发给客户端。
🔍 三、验证(认证)过程
当客户端带着 JWT 来访问接口时,比如:
makefile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
服务端验证步骤如下:
1️⃣ 拆解 token
ini
token = "header.payload.signature"
[b64Header, b64Payload, b64Signature] = token.split('.')
2️⃣ Base64URL 解码前两段(header、payload)
ini
header = JSON.parse(base64urlDecode(b64Header))
payload = JSON.parse(base64urlDecode(b64Payload))
3️⃣ (可选)检查 header.alg 是否在允许列表中
不要盲信
header.alg,而是检查它是否在白名单,例如:
javascript
if (header.alg !== 'HS256') throw new Error('不支持该算法');
4️⃣ 用同一个 secret 重新计算签名
ini
expectedSig = base64urlEncode( HMACSHA256(b64Header + "." + b64Payload, secret) )
5️⃣ 比较签名
用常数时间比较(timing-safe compare) :
javascript
if (expectedSig !== b64Signature) throw new Error('签名不匹配');
6️⃣ 验证 payload 的有效性
diff
- exp(过期时间)> 当前时间?
- nbf(不可用时间)< 当前时间?
- iss / aud / sub 等字段是否匹配?
✅ 通过验证后:
服务器就认为这个 JWT 是由自己签发的合法 token,可以信任其内容。
🔐 四、签名与认证关系图
scss
[签发方] [验证方]
↓ ↓
header + payload token(header.payload.signature)
↓ ↓
HMAC(header.payload, secret) HMAC(header.payload, secret)
↓ ↓
signatureA signatureB
↓ ↓
比较:signatureA === signatureB ✅ → 有效
两边都用同一个 secret,如果结果一样,说明 token 未被篡改。
⚠️ 五、安全要点(很关键)
- secret 不能泄露:一旦泄露,攻击者可以自己生成合法 token!
- 不要信任 header.alg:服务端必须强制指定算法,比如只允许 HS256。
- 签名前不要重新 JSON.stringify :要用原始的
base64url字符串拼接,否则签名会不一致。 - 注意 timing attack :用
crypto.timingSafeEqual()等方法比较签名。 - 密钥长度 :
HS256的 secret 建议至少 32 字节以上。 - 密钥轮换 :定期更换 secret,可通过
kid(key id) 管理版本。