相关问题
- JWT 和 Session 有什么区别?
- JWT 和 Session 分别是如何实现用户身份认证的?
- Cookie、Session 和 JWT 三者有什么关系?
- 为什么 Session 通常需要使用 Redis?分布式环境下如何共享 Session?
- JWT 由哪三部分组成?各自有什么作用?
- JWT 是如何生成和验证签名的?为什么能够防止篡改?
- JWT 是加密的吗?Payload 中可以存储敏感信息吗?
- JWT 中 HS256 和 RS256 有什么区别?
- JWT 为什么可以实现无状态认证?无状态有什么优缺点?
- 为什么 Session 可以主动注销,而 JWT 难以主动注销?
- JWT 有哪些注销方案?各自有什么优缺点?
- JWT 使用 Redis 黑名单或 Token 版本号后,是否又变成了有状态认证?
- Access Token 和 Refresh Token 分别有什么作用?为什么需要两种 Token?
- Refresh Token 应该如何存储?如果被盗了怎么办?
- JWT 和 Session 被盗后,攻击者能否冒用用户身份?如何防范?
- JWT 应该存储在 Cookie 还是 localStorage 中?分别有什么安全风险?
- 如果 JWT 每次验证都需要查询 Redis,为什么不直接使用 Session?
- 如果让你设计一个 Go 后端登录认证系统,你会选择 JWT 还是 Session?为什么?
Session 和 JWT
在后端开发中,Session 和 JWT 是两种常见的身份认证方案。它们经常被放在一起比较,但如果只是记住“Session 有状态,JWT 无状态”,其实并没有真正理解它们的设计思想。
要理解这两种方案,我们应该先思考一个问题:HTTP 是无状态协议,服务端如何知道连续两次请求来自同一个已经登录的用户?
一、为什么需要 Session 和 JWT?
假设用户第一次访问登录接口,提交用户名和密码:
|
|
服务端通过数据库验证账号密码,确认用户身份。
但是,当用户下一次访问订单接口时:
|
|
服务端并不能仅凭这次 HTTP 请求,就知道用户之前已经登录过。
一种最直接的办法是每次请求都携带用户名和密码,但这不仅不方便,还会增加密码暴露的风险。
因此,我们需要在用户第一次登录成功后,给客户端一个可以证明登录身份的凭证。以后客户端只需要携带这个凭证,服务端就可以识别用户,而不必每次重新验证密码。
Session 和 JWT 正是解决这个问题的两种方案。
它们最大的区别在于:
Session 把登录状态主要保存在服务端,客户端只保存会话标识;JWT 则把身份声明保存在经过签名的 Token 中,服务端可以通过验证 Token 来确认身份。
二、Session 的工作原理
Session 的核心思想是:用户登录成功后,服务端保存一份会话记录,并把对应的 Session ID 交给客户端。
假设用户张三登录成功,服务端生成一个足够随机、不可预测的 Session ID:
|
|
这里用 abc123 只是为了演示,生产环境需要使用密码学安全的随机数生成器,生成足够长的 Session ID。
服务端随后在 Redis 中保存:
|
|
然后通过 HTTP 响应头把 Session ID 返回给浏览器:
|
|
浏览器保存 Cookie 后,后续访问符合 Cookie 作用域的接口时,会自动携带 Session ID:
|
|
服务端收到请求后,从 Cookie 中取出 Session ID,再查询 Redis:
|
|
如果对应的 Session 存在且有效,就可以确认这次请求关联的是用户 1001。
整个认证流程可以概括为:
|
|
这里有两个容易混淆的概念。
首先,Session 不等于 Cookie。Session 是服务端维护的会话状态,Cookie 是浏览器保存和发送数据的一种机制。Session ID 可以通过 Cookie 传递,也可以通过其他合适的请求机制传递。
其次,Session 不等于 Redis。Session 可以保存在服务端内存、数据库或者 Redis 中。Redis 只是因为访问速度快、支持过期时间、方便多实例共享,所以经常被用来存储 Session。
为什么分布式系统经常使用 Redis 保存 Session?
假设一个 Go 服务部署了三个实例:
|
|
用户第一次登录时,请求被转发到 Go1。如果 Go1 把 Session 保存在自己的进程内存中,那么用户下一次请求被转发到 Go2 时,Go2 就无法查询到对应的登录状态。
解决办法是让三个实例共享 Session 存储:
|
|
这样无论请求被转发到哪个实例,都可以通过 Redis 查询用户的会话状态。
因此,Session 的分布式问题并不是无法共享,而是需要额外的共享状态管理机制。
三、JWT 的工作原理
Session 方案可以正常工作,但它通常需要服务端保存会话状态。如果每次请求都需要查询 Redis,那么认证过程就依赖一个共享存储系统。
这时我们可以思考:能否把用户身份信息直接放进凭证中,并让服务端验证凭证的真实性,而不必每次查询 Session?
JWT 就提供了这种能力。
JWT(JSON Web Token)是一种 Token 格式,通常由三部分组成:
|
|
其中 Header 表示头部,Payload 表示载荷,Signature 表示签名。
Header 通常描述 Token 的类型和签名算法:
|
|
Payload 用来保存身份声明:
|
|
其中 sub 表示主体,一般可以保存用户 ID;iat 表示签发时间;exp 表示过期时间。
Signature 则用于验证 Header 和 Payload 是否被篡改。
以 HS256 为例,签名的计算方式可以概括为:
|
|
最后把三部分连接起来:
|
|
需要特别注意:JWT 默认不是加密,而是编码和签名。
Header 和 Payload 使用 Base64URL 编码,任何拿到 Token 的人都可以解码查看其中的数据。因此不能把密码等敏感信息直接放进普通签名 JWT 的 Payload 中。
JWT 为什么能防止篡改?
假设一个普通用户的 JWT 包含:
|
|
攻击者想获得管理员权限,于是把 role 修改为 admin:
|
|
修改 Payload 并不困难,因为 Base64URL 只是编码。
但问题在于:Payload 一旦修改,原来的 Signature 就不再匹配。
服务端收到 JWT 后,会使用可信密钥和规定的算法验证签名。如果攻击者不知道正确的签名密钥,就无法为修改后的 Payload 生成有效签名。
因此 JWT 并不是阻止客户端修改内容,而是让服务端能够检测未经授权的修改。
当然,服务端不能只验证签名,还需要按业务要求验证 exp、nbf、iss、aud 等声明,并在认证之后检查用户是否具有相应的业务权限。
JWT 的登录流程
用户登录成功后,服务端生成包含用户 ID、有效期等信息的 JWT,并使用密钥进行签名,然后将 JWT 返回给客户端。
客户端后续请求可以通过请求头携带:
|
|
服务端接收到请求后,验证 JWT 的签名、有效期以及必要的 Claims,再提取用户身份信息。
|
|
这就是 JWT 可以用于无状态认证的原因:服务端不必为每个 JWT 保存独立的 Session 记录,就能够验证这个凭证是否可信。
不过,无状态只表示可以独立验证凭证,不代表业务系统不需要数据库或 Redis。实时权限、账号封禁、Token 撤销等功能仍然可能需要服务端状态。
四、JWT 和 Session 最大的区别:登录状态保存在哪里?
现在再回过头比较两种方案,就很容易理解了。
Session 认证中,客户端通常只知道 Session ID,真正的身份映射保存在服务端:
|
|
JWT 无状态认证中,客户端直接持有包含身份声明的签名凭证:
|
|
两者的关键区别如下:
| 对比 | Session | JWT 无状态认证 |
|---|---|---|
| 客户端保存 | Session ID | JWT |
| 服务端保存 | 会话状态 | 不必保存每个 Token |
| 身份验证 | 查询 Session 记录 | 验证签名和 Claims |
| 分布式部署 | 通常需要共享 Session 存储 | 各实例可独立验签 |
| 主动注销 | 删除 Session 记录 | 需要额外撤销机制 |
| 权限实时变化 | 可以更新服务端状态 | 已签发 Claims 不会自动改变 |
因此,Session 的优势是登录状态容易管理和撤销,JWT 的优势是可以在不查询会话存储的情况下独立验证身份。
但 JWT 并不一定比 Session 更快或者更安全,实际还要考虑验签算法、Redis 延迟、缓存、并发规模和撤销需求。
五、为什么 Session 容易注销,而 JWT 注销比较麻烦?
这个问题能够很好地体现两种认证方案的本质区别。
假设用户点击退出登录。
如果使用 Session,服务端只需要删除对应的 Redis 记录:
|
|
客户端即使继续携带原来的 Session ID,服务端也查不到有效会话,因此会拒绝请求。
但是 JWT 不一样。
假设 JWT 的有效期是 2 小时,用户登录 10 分钟后退出。
客户端可以删除本地 JWT,但服务端此前签发的 JWT 并不会因为客户端删除它就自动失效。
如果攻击者已经复制了这个 JWT,那么只要签名正确、没有过期,并且没有额外撤销检查,就仍然可能通过认证。
为了解决这个问题,通常有三种方案。
方案一:Redis 黑名单
给 JWT 增加唯一标识 jti:
|
|
正常验证 JWT 时,服务端除了验证签名和有效期,还要检查 Redis 中是否存在对应的黑名单记录。
用户退出登录时,把 jti 放入 Redis:
|
|
并把 Redis 过期时间设置为 JWT 的剩余有效时间。
这样以后即使 JWT 本身没有过期,只要命中黑名单,服务端也会拒绝它。
黑名单的本质,就是记录哪些已经签发的 Token 不再被允许使用。
这种方式可以精确撤销某个 Token,但每次验证都要进行额外的状态检查。
方案二:Token 版本号
另一种办法是不记录每个失效的 Token,而是记录用户当前允许使用的 Token 版本。
例如 Redis 保存:
|
|
用户登录时,签发的 JWT 包含:
|
|
每次验证时,服务端检查 JWT 的版本号与 Redis 中保存的版本号是否一致。
如果用户修改密码或者需要退出所有设备,就把版本号修改为 2。
此时所有包含 version=1 的旧 JWT 都会被拒绝。
版本号方案的本质,是通过改变服务端认可的版本,让一批旧 Token 同时失效。
它非常适合退出所有设备、账号安全异常等场景,但通常需要在认证时查询或获取当前版本号。如果想精确注销某一个设备,还要设计设备级别的版本或会话标识。
方案三:Access Token + Refresh Token
这个方案的思路有所不同。
它并不直接让已经签发的 Access Token 立即失效,而是缩短访问凭证的有效期,并通过服务端控制能否继续续期。
例如:
|
|
Access Token 用于访问普通业务接口。
Refresh Token 不用于访问普通业务接口,而是用于向认证服务申请新的 Access Token。
用户登录成功后,客户端持有两个 Token:
|
|
为了支持主动注销,服务端通常还会保存 Refresh Token 对应的有效会话状态:
|
|
服务端一般不需要保存 Refresh Token 明文,可以保存安全哈希值或者其他可用于验证和撤销的会话记录。
当 Access Token 过期时,客户端携带 Refresh Token 请求刷新接口。
|
|
这里客户端必须能够持有或发送 Refresh Token,否则就无法申请续期。
对于浏览器应用,可以考虑把 Refresh Token 保存在配置了 HttpOnly、Secure 和适当 SameSite 属性的 Cookie 中,并配合必要的 CSRF 防护。
当用户退出登录时,服务端撤销 Refresh Token,客户端清除相关凭证。
这样旧 Refresh Token 就不能再用于续期了。
但是这里有一个非常重要的细节:只撤销 Refresh Token,并不会自动使尚未过期的 Access Token 立即失效。
如果 Access Token 还剩 10 分钟有效期,那么它可能在这 10 分钟里继续被使用。
因此 Access + Refresh Token 方案的本质是控制续期,并把旧 Access Token 的剩余可用时间限制在较短窗口内。
如果业务要求立即撤销,还需要结合黑名单、版本号等机制。
实际生产环境还可以使用 Refresh Token Rotation:每次成功刷新时同时签发新的 Refresh Token,并让旧 Refresh Token 失效。服务端可以检测旧 Token 被重复使用的异常情况,并据此撤销相关会话。
三种方案可以总结为:
| 方案 | 本质 | 主要用途 |
|---|---|---|
| Redis 黑名单 | 禁止某一个 Token | 精确撤销 |
| Token 版本号 | 淘汰一批旧 Token | 退出所有设备 |
| Access + Refresh Token | 控制续期并缩短风险窗口 | 长期登录与续期 |
这三种方案并不互斥,实际项目中可以组合使用。
六、如果 JWT 或 Session ID 被盗,会怎么样?
不论使用 JWT 还是 Session,都必须面对一个问题:登录凭证一旦被盗,攻击者就可能冒用用户身份。
例如 Session 认证中:
|
|
正常用户携带:
|
|
如果攻击者也拿到了 abc123,那么他就可以尝试发送相同的请求。
服务端通常只知道这个 Session ID 对应用户 1001,无法仅凭 Session ID 判断请求究竟来自正常用户还是攻击者。
这种攻击叫作会话劫持(Session Hijacking)。
JWT 同样存在类似问题。攻击者不需要修改 JWT,只要获得一份仍然有效的 Token,就可能直接使用它。
这是因为 Session ID 和常见的 JWT Access Token 都属于 Bearer Credential,也就是持有有效凭证的一方就可能使用它。
因此,安全措施不能只依赖凭证本身,通常还需要 HTTPS、安全的 Cookie 属性、合理的过期时间、凭证撤销机制,以及对修改密码、转账等敏感操作进行二次身份验证。
两种方案在凭证被盗时最大的区别,仍然是撤销能力。
Session 通常可以直接删除服务端会话记录,使被盗 Session ID 立即失效。
纯无状态 JWT 则需要黑名单、版本号等额外机制,否则只能等待 Token 过期。
七、实际项目应该选择 JWT 还是 Session?
这两种方案没有绝对的优劣,关键要看业务需求。
如果是一个普通的后台管理系统,服务端可以统一管理会话,而且要求管理员随时踢人下线,那么 Session + Redis 往往是比较直接的设计。
因为服务端可以通过 Redis 控制每个会话的有效状态,注销、封禁和强制下线的实现都比较方便。
如果是多个微服务共同提供接口,希望它们能够独立验证用户身份,减少集中式 Session 查询,那么短期 JWT Access Token 可能更适合。
不过,随着系统规模扩大,如果又需要立即撤销 Token、实时更新权限、控制多设备登录,那么 JWT 往往也需要引入 Redis 或数据库来维护部分会话状态。
因此不要简单地认为:
“Session 需要 Redis,所以性能差;JWT 不需要 Redis,所以性能好。”
JWT 是否需要 Redis,取决于你如何设计 Token 撤销、Refresh Token 管理以及业务权限校验。
真正的架构取舍,是无状态验签的便利性和服务端会话控制能力之间的平衡。
八、秋招面试应该怎么回答?
如果面试官问:“JWT 和 Session 有什么区别?”
可以从它们的认证原理开始回答。
Session 主要是服务端维护登录状态,客户端保存 Session ID。每次请求时,服务端根据 Session ID 查询会话数据,确认用户身份。分布式部署时,通常可以使用 Redis 共享 Session。
JWT 则是一种自包含的签名 Token,客户端保存包含身份声明的凭证,服务端可以通过验证签名和相关 Claims 来认证身份,不必每次都查询 Session 存储。
这种区别又决定了它们在注销方面的行为:Session 可以直接删除服务端记录,而纯无状态 JWT 一旦签发,在过期之前通常无法直接撤销,需要借助黑名单、版本号或者短期 Access Token 等机制。
最后再补充安全性:两者的凭证被盗后都可能遭到身份冒用,需要 HTTPS、合理的有效期、安全存储以及必要的撤销机制。
这样回答就不是简单地列举 JWT 和 Session 的差异,而是从认证原理 → 服务端状态 → 分布式部署 → 注销机制 → 安全性形成一条完整的逻辑链。