从Cookie到SessionId再到JWT的演变与对比
一、Cookie的诞生与原理
早期Web是无状态的,服务器无法识别用户身份。为了解决这个问题,Cookie应运而生。Cookie是浏览器存储在本地的小型文本数据,每次请求都会自动携带到服务器。
Cookie特点:
- 由浏览器自动管理和发送
- 可设置过期时间、作用域、安全属性
- 容量有限(一般4KB)
典型应用: 用于保存用户偏好、登录状态、跟踪信息等。
二、Session与SessionId
Cookie虽然能存储信息,但不安全且容量有限。于是,Session机制出现。Session在服务器端保存用户数据,客户端只保存一个SessionId(通常通过Cookie传递)。
SessionId流程:
- 用户登录,服务器生成Session并返回SessionId给客户端。
- 客户端通过Cookie保存SessionId。
- 每次请求时,浏览器自动携带SessionId,服务器根据SessionId查找用户数据。
Session优点:
- 数据存储在服务器,安全性高
- 可存储大量数据
Session缺点:
- 服务器需维护大量Session,消耗内存
- 分布式部署时需做Session同步
三、JWT(JSON Web Token)
随着前后端分离和分布式架构流行,JWT成为主流认证方式。JWT是一种自包含的令牌,所有信息都在Token中,服务器无需保存会话。
JWT结构详解:
Header(头部)
- 通常是一个JSON对象,描述令牌类型(typ,通常为JWT)和所用的签名算法(alg,如HS256、RS256)。
- 示例:
{ "alg": "HS256", "typ": "JWT" }
Payload(负载)
- 也是一个JSON对象,包含实际要传递的数据(Claims,声明)。常见声明有:
- iss(签发者)、exp(过期时间)、sub(主题)、aud(受众)、iat(签发时间)、nbf(生效时间)、jti(唯一ID)等。
- 也可以自定义字段,如用户ID、角色等。
- 示例:
{ "sub": "1234567890", "name": "John Doe", "admin": true, "exp": 1717171717 }
- 也是一个JSON对象,包含实际要传递的数据(Claims,声明)。常见声明有:
Signature(签名)
- 用于验证令牌的完整性和真实性。
- 生成方式:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) - 服务器用密钥对Header和Payload进行签名,客户端和服务器都可用密钥验证签名是否被篡改。
JWT的格式如下:
header.payload.signatureJWT实际示例
假设有如下JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjo2MTYyMDg5ODU2OTk1MzI4LCJ1c2VybmFtZSI6ImJiIiwiaXNzIjoic21hcnRtZW1vIiwiZXhwIjoxNzgwODI0OTMzfQ.teBlE6RxxDVdCE7SptrljFKScPMBTpCa2CmPNS_mXgA其中:
- Header(头部,Base64Url解码后):
{"alg":"HS256","typ":"JWT"} - Payload(负载,Base64Url解码后):
{ "user_id": 6162089856995328, "username": "bb", "iss": "smartmemo", "exp": 1780824933 } - Signature(签名): 使用HS256算法,密钥(加盐字符串)为“夏天夏天悄悄过去”,对header和payload进行签名生成。
签名过程:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
"夏天夏天悄悄过去"
)这样,只有知道密钥“夏天夏天悄悄过去”的服务端才能验证和生成该JWT。
为什么密钥能防止内容被篡改?
JWT的签名部分是用密钥(如“夏天夏天悄悄过去”)对Header和Payload进行加密哈希(如HS256)生成的。每次服务端收到JWT后,会用同样的密钥重新计算签名:
- 先Base64Url解码Header和Payload。
- 用密钥和算法重新生成签名。
- 与JWT中的Signature部分进行比对。
如果内容被篡改(如Payload被修改),重新计算出的签名就和原始签名不一致,服务端会拒绝该JWT。
因此,只有知道密钥的服务端才能生成和验证JWT,保证内容未被篡改。
为什么不能通过修改payload直接登录其他账号?
有些人可能会尝试将JWT的payload部分(如user_id或username)解码后修改为其他账号的信息,然后重新编码,企图伪造登录。
但实际上,这样做无法通过服务端校验:
- JWT的签名是基于原始header和payload、密钥共同生成的。
- 如果你修改了payload内容(如user_id),但没有密钥,无法重新生成正确的签名。
- 服务端收到JWT后,会用密钥重新计算签名,与JWT中的signature比对。
- 签名不一致,服务端会拒绝该请求。
因此,只有拥有密钥的服务端才能生成有效的JWT,用户无法通过简单修改payload来冒充其他账号。
JWT流程:
- 用户登录后,服务器生成JWT并返回给客户端。
- 客户端将JWT存储(如localStorage或cookie)。
- 客户端每次请求时,将JWT放在HTTP头部(如Authorization)。
- 服务器验证JWT的签名和有效性,确认用户身份。
JWT优点:
四、JWT常见问题与最佳实践
1. 为什么JWT可以被解析?
JWT的Header和Payload部分只是Base64Url编码(不是加密),任何人都可以解码查看内容,但无法验证真伪或伪造签名,除非知道密钥。
2. JWT的校验过程(后端做的事)
- 解码Header和Payload
- 使用服务端密钥重新计算签名
- 与Token中的Signature对比
- 一致则信任,不一致则拒绝
- 同时校验exp(过期)、iat(签发时间)等字段
3. JWT中不应该包含哪些信息?
- ❌ 明文密码、身份证、手机号等敏感信息
- ❌ 不能让前端或中间人看到的隐私字段
- ✅ 推荐只放非敏感声明信息,如user_id、username、role、exp、iat、iss等
- 如果需要敏感信息,应在服务端查询或用加密方式(如JWE)处理
4. 最佳实践建议
| 场景 | 是否推荐 | 说明 |
|---|---|---|
| 在JWT中放user_id或role | ✅ | 安全,常见做法 |
| 在JWT中放邮箱/用户名 | ⚠️ | 可放,但注意隐私 |
| 在JWT中放密码/手机号/敏感数据 | ❌ | 不推荐,存在泄露风险 |
| 只放user_id,其他信息通过服务端查 | ✅ | 安全且便于控制 |
如果你后续打算用JWT设计用户系统(如登录、权限、接口鉴权),建议只在JWT中存放必要且非敏感的信息,其他敏感数据通过服务端查询。
- 安全性高,通过签名防止数据篡改
JWT缺点:
- 无法主动失效,除非令牌过期
- Payload部分为明文,敏感信息不应存储其中
四、三者对比
| 特性 | Cookie | SessionId | JWT |
|---|---|---|---|
| 存储位置 | 客户端 | 客户端+服务器 | 客户端 |
| 安全性 | 较低 | 较高 | 依赖签名 |
| 扩展性 | 受限 | 服务器内存受限 | 易于分布式扩展 |
| 数据容量 | 小(4KB) | 大(服务器端) | 适中(Token体积) |
| 主动失效 | 可设置 | 可销毁 | 需额外机制 |
| 分布式支持 | 一般 | 需同步 | 天然支持 |
五、总结
Cookie、SessionId和JWT是Web认证与状态管理的三种主要方式。Cookie适合简单场景,SessionId适合安全性要求高的传统架构,JWT则更适合分布式和前后端分离的现代应用。实际选择需结合业务需求、安全性和系统架构。
面试口语化总结
在现代的 web 服务里,通常需要进行身份校验以访问一些特定资源。早期做法是直接在 cookie 里存储校验信息,比如用户 id 或 token,但 cookie 容量有限,而且明文存储用户信息存在安全隐患。
后来引入了 sessionID,服务端生成 session 并返回 sessionID,客户端通过 cookie 保存 sessionID,每次请求带上 sessionID,服务端查找对应的用户信息。sessionID 解决了明文暴露的问题,但也有缺点,比如服务器要维护大量 session,分布式部署时还要做 session 同步。
现在更常用的是 JWT token 校验。相比 cookie 和 session,JWT 的优点是无状态,服务器不用保存会话信息,天然支持分布式和前后端分离。
JWT token 由三部分组成:Header、Payload 和 Signature。Header 和 Payload 是明文存储的,Signature 是用 header、payload 和服务器密钥一起生成的。
校验时,服务端会用密钥重新计算签名,和 token 里的 signature 比对。如果有人拿到 a 用户的 token,修改 payload 里的 user_id 试图冒充 b 用户,因为没有密钥,无法生成正确的签名,服务端校验不通过。
所以 JWT 能防止信息篡改和冒名访问,但也不建议在 payload 里存储敏感信息。
