BodySense Redis 第 5 课:Session Authority——JWT 为什么仍然需要 Redis
深入理解 stateless JWT、server-side revocation、Session authority、Redis miss 与 Redis failure 的差别,以及 BodySense 为什么对认证 Redis 采用 fail closed。
[!info] related notes
- 课程 MOC:BodySense Redis 从零到一课程 MOC
- 前置课程:第 4 课
- 对应知识笔记:Redis Session Authority、Access Token、Refresh Token
- 关系笔记:Stateful、Stateless 与 Hybrid 认证架构
BodySense Redis 第 5 课:Session Authority——JWT 为什么仍然需要 Redis
[!abstract] 学习目标 能完整解释:“JWT 签名有效”与“用户当前仍然被授权访问”为什么不是同一个事实。
前置回顾
第 1 课已经看到:
JWT
→ 客户端持有的访问凭证
Redis Session
→ 服务端当前在线授权状态
这一课把这条链完整拆开。
先区分三个经常混在一起的词
这一课如果不先区分 Authentication、Credential、Authorization,后面很容易把 JWT 和 Session 说成同一个东西。
Authentication:你是谁
登录时:
email + password
→ 验证身份
→ user_id = u1
这是 Authentication。
Credential:你拿什么证明
登录成功后,服务端不会要求每个请求都重新发密码,而是签发 Credential:
Access Token
Refresh Token
Session cookie
...
它们是“之后用来证明身份/授权上下文的凭证”。
Authorization:现在是否允许你做这件事
即使 Credential 是真的,也还要判断:
这个 Session 是否被撤销?
用户是否还有这个权限?
资源是否属于该用户?
账号是否被冻结?
所以:
Credential valid 不自动等于 Authorization live。
JWT 又是什么
JWT = JSON Web Token。
常见紧凑形式:
header.payload.signature
可以粗略理解为:
Header
→ 使用什么算法/Token 类型
Payload
→ claims,例如 sub/sid/exp/iat
Signature
→ 服务端对 header + payload 的签名
服务端收到 Token 后,可以本地验证:
签名是否正确?
是否过期?
claims 是否符合要求?
这使 Access Token 验证不一定要每次查询 PostgreSQL。
但签名只能回答:
这个 Token 是不是可信签发的,并且 Token 自己是否还在有效期。
它无法神奇地修改已经被客户端拿走的那串 bytes。
这就是为什么 Logout/Revoke 会成为问题。
Stateless 和 Stateful 是什么
Stateless 在这里不是“服务器完全没有数据库”。
它指:
判断这个 Access Token 本身是否有效
主要依赖 Token 自带 claims + signature
不必先查一份 server-side token row
而 Stateful Session 更像:
客户端只有 session id / credential
服务端必须查 Session state
才能知道是否有效
BodySense 实际采用的是一种 Hybrid:
JWT
→ 自包含 identity + expiration
Redis Session
→ server-side live/revoked authority
所以它不是“JWT 和 Session 二选一”。
Stateless JWT 能解决什么
一个自包含 JWT 可以携带:
sub user identity
sid session identity
exp expiration
iat issued at
...
服务器可以本地:
parse
→ verify signature
→ validate claims
而不必为了 Token 本身每次查询数据库。
这就是 JWT 的 stateless 优势。
但它带来一个直接问题:
已经签发出去的 JWT,在 exp 之前通常不会因为用户 Logout 自动改变。
为什么需要 Server-side Revocation
用户场景:
22:00 Login → Access Token exp=22:30
22:05 Logout
22:06 旧 Access Token 再次出现
如果服务器只做:
signature valid?
exp valid?
22:06 仍可能通过。
引入 Session authority:
JWT sid=s1
↓
Redis session:s1
Logout:
DEL session:s1
下一请求:
JWT valid
+
session:s1 miss
→ 401
于是可以即时注销。
Session Authority 这个词到底是什么意思
Authority 在这里可以理解成:
当多个信息冲突时,哪一份状态拥有最终裁决权。
例如:
JWT 说:
exp 还没到
Redis 说:
session:s1 不存在
BodySense 的在线授权规则选择相信:
session:<sid>
= revocation authority
因此 Session miss 会覆盖“JWT 还没过期”这个事实。
这不是因为 Redis 比 JWT 更高级,而是架构明确规定:
JWT owns credential integrity / expiry
Redis owns live session revocation
BodySense 的 Auth Pipeline
当前心智链:
Authorization: Bearer <access-token>
↓
Validate HS256 JWT
↓
extract session_id
↓
Redis session:<sid>
├─ hit → authorization can continue
├─ miss → 401
└─ error → 503
这里最重要的不是 Redis 快,而是它承担了在线授权 authority。
在进入 Miss / Error 前:Fail Open 与 Fail Closed
安全系统经常需要先选择失败策略。
Fail Open
依赖出错
→ 为了可用性继续放行
优点:服务看起来更可用。
风险:安全控制在依赖故障时失效。
Fail Closed
依赖出错
→ 无法确认安全条件
→ 拒绝或暂时不可用
优点:不会因为基础设施故障绕过安全边界。
代价:依赖故障会影响可用性。
BodySense 把 Redis Session 作为 revocation authority,因此选择:
Redis error
→ fail closed
→ 503
这和普通 Cache Redis 出错时 fallback PostgreSQL 完全不是一种语义。
Redis Miss 与 Redis Error 完全不同
Definitive Miss
Redis 正常响应:
GET session:s1
→ nil
这是一个已知事实:
当前没有这个 Session authority。
因此:
→ 401 Unauthorized
Redis Error / Unavailable
例如:
timeout
connection refused
cluster unavailable
此时服务器不知道:
session:s1 到底存在还是不存在
如果因为查不到 Redis 就放行:
Redis down
→ skip revocation check
→ allow
会让已经被撤销的 Token 穿透。
因此 BodySense 的安全边界是:
Redis unavailable
→ 无法证明仍被授权
→ fail closed
→ 503 Service Unavailable
[!important] 401 与 503 的事实语义 401 = 已知 Session 不存在/已失效。
503 = 授权依赖不可用,系统无法安全判断。
Session Key 与 User Session Index
核心:
session:<sid>
→ authority
user_sessions:<uid>
→ secondary index
即使 user_sessions Set 有 stale member,也不能改变 Session authority。
user_auth_revoked Tombstone
账户删除、隐私擦除或全局授权窗口关闭时,可能存在并发登录:
A: erase user / revoke all
B: login request already in flight
如果只是删除当前 Session,而 B 稍后又创建新 Session,会产生“撤销之后又复活”的竞态。
因此短期 tombstone:
user_auth_revoked:<uid>
可以表达:
在这段授权窗口中,不允许并发流程重新建立 Session。
这是 Fast State Plane 的典型状态,不是普通 cache。
Session 与 Refresh Lifetime
Session 生命周期通常不是只跟 Access Token 绑定:
Access Token
→ 短期
Refresh Token / Session
→ 更长期
因此:
Access Token exp
≠
Session 必须立刻结束
Access Token 到期后,可以用当前有效的 Refresh Token 轮换获得新 Access Token。
而 Session 被 revoke 时:
即使 JWT 未过期
即使 Refresh Token 自身格式合法
→ 整条授权链都应结束
Failure Injection
[!example] 三种测试
A. Session exists
B. Session deleted
C. Redis unavailable
期望:
A → allow
B → 401
C → 503
还可以继续:
D. JWT expired + Session exists
→ JWT 自身失败,不应因为 Session exists 放行
E. JWT valid + Session missing
→ 401
F. JWT valid + Redis timeout
→ 503
Prediction
[!question]
- 为什么不把“Redis 访问失败”也当成 Session miss 返回 401?
- 为什么不能在 Redis down 时为了可用性临时跳过 Session 检查?
user_sessions:<uid>里仍有 sid,但session:<sid>不存在,应信谁?- Access Token 还剩 10 分钟,但用户点击 Logout,下一请求应该怎样?
[!success]- Answer / 解析 1. 为什么 Redis 访问失败不能当成 Session miss?
Session miss 是 Redis 正常响应后得到的确定事实:session:不存在,因此可以判定当前在线授权已经失效,返回 401。Redis timeout / connection error 则是“无法读取 authority”,此时 Session 可能存在,也可能不存在;把未知状态伪装成 miss 会把基础设施故障错误解释成用户凭证失效。 2. 为什么 Redis down 时不能为了可用性跳过 Session 检查?
因为这样会把系统从 fail closed 变成 fail open。已经 Logout、被管理员撤销、被 replay detection 封禁的 JWT,只要自身签名和 exp 仍合法,就可能重新被接受。也就是说,一次 Redis 故障会临时关闭服务端撤销能力。3. user_sessions:
和 session: 冲突时信谁?
信 session:。它是 Session authority;user_sessions: 只是为了从 user 反向找到候选 Session 的 secondary index。Set 中残留 sid 只能说明索引 stale,不能证明 Session 仍有效。 4. Logout 后旧 Access Token 还剩 10 分钟,下一请求怎样?
JWT 本身可能仍能通过签名与 exp 校验,但服务端继续查询 session:时得到 miss,因此应立即拒绝,通常返回 401。这正是 server-side Session authority 给 stateless JWT 补上的即时撤销能力。
Explain-back / L4 Gate
能从头讲清:
Login
→ issue JWT
→ create Session authority
→ protected request
→ JWT validation
→ Session check
→ logout/revoke
→ immediate invalidation
并明确:
valid credential ≠ live authorization
把概念落到当前真实代码
apps/api/internal/cache/user_session.go
Exists:
EXISTS session:<sid>
├─ val > 0 → live
└─ 0 → definitive miss
Redis client error 不会被转成 false,nil,而是把 error 返回上去。这正是“miss 与 unavailable 必须区分”的代码实现。
apps/api/internal/middleware/auth.go
顺序是:
1. Extract Bearer
2. Validate JWT signature + expiry
3. Require session id
4. sessionCache.Exists
├─ error → 503 AUTHENTICATION_UNAVAILABLE
├─ false → 401 Session revoked
└─ true → c.Next()
这段代码就是本课核心命题的可执行版本:
JWT valid 只是第一道条件;live Session authority 是第二道条件。
user_auth_revoked:<uid> 为什么不是普通 Session Key
它是 tombstone(墓碑记录)。
Tombstone 的意思不是“保存完整被删除对象”,而是保留一个短期标记:
这个 identity 已被 revoke
不要让并发旧流程重新创建它
这和分布式系统里“删除后仍需要短期记住删除事实”的思路类似。
BodySense 代码锚点
apps/api/internal/cache/user_session.go
apps/api/internal/middleware/auth.go
apps/api/internal/auth/jwt.go
apps/api/internal/service/auth_service.go