← [ BACK_TO_NOTES ]

BodySense Redis 第 5 课:Session Authority——JWT 为什么仍然需要 Redis

深入理解 stateless JWT、server-side revocation、Session authority、Redis miss 与 Redis failure 的差别,以及 BodySense 为什么对认证 Redis 采用 fail closed。

#course #growing #backend #security #redis #bodysense

[!info] related notes

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]

  1. 为什么不把“Redis 访问失败”也当成 Session miss 返回 401?
  2. 为什么不能在 Redis down 时为了可用性临时跳过 Session 检查?
  3. user_sessions:<uid> 里仍有 sid,但 session:<sid> 不存在,应信谁?
  4. 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
创建于 2026/9/18 更新于 2026/9/18
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本