编程语言研究书库

第 7 章拆解 — 访问控制:认证、授权、会话,与前后端安全过滤

访问控制:认证、授权、会话,与前后端安全过滤

这一章给应用上锁。 前半是概念与选项:四个术语、三种认证凭据、四种 HTTP 认证方案、 三种授权机制——每个都讲清「解决什么、破绽在哪」。后半是落地:表单登录 + 真会话, 以及本章重头戏——前后端两道安全过滤,让「没登录的人看不见页面、调不通接口」。 主线例子:一个匿名用户从打开登录页,到刷新浏览器后依然保持登录的全程。

1. 顶层全景

  术语钉死:认证(你是谁)→ 授权(你能干什么)→ 会话(登录状态活多久)
    凭据三要素:你知道的(密码)/ 你持有的(密钥、令牌)/ 你本身的(指纹)
      四种方案:Basic(明文 base64!)→ Digest(发哈希但仍脆)→ Bearer(发令牌)→ API key(钥匙)
        授权三机制:ACL(名单)→ RBAC(角色+动作)→ JWT(自带权限的签名令牌,+refresh token)
          落地:表单认证 + 会话(axum-login)
            前后端过滤:Redis 存 refresh token → JWT 交换 → tower 验票;
                       前端 identity 全局化 + 路由守卫 + 自动会话恢复

  图说:从「概念」到「方案」到「机制」到「代码」,每一层都在回答上一层留下的破绽。

2. 四个术语,先钉死

令牌(token) 先认名:一串代替用户名密码的凭证,拿到它就等于拿到一段时间的通行权。

认证(authentication)= 确认身份:不只是「认出」一个人(IP 也能认人), 而是确认这个身份属于我们平台上注册的、被我们认可的用户; 并且认证完立刻就能做最粗粒度的一道授权——登录用户 vs 匿名用户1。

授权(authorization)= 细粒度访问权:已认证的用户之间再分三六九等 (开发者能读工单,经理还能删项目)。授权不能脱离认证存在; 「已登录才能看」就是最平凡的授权——不然连注册都办不到2。

会话(session)= 登录状态的寿命: 用户认证后那段时间叫会话。 别把会话开到无穷大——时间越久变数越多:IP 变了、密码丢了被盗了、 权限变了、人走开了。所以会话要能被主动终结;寿命多长要在安全和方便之间找平衡。 关键能力:随时让会话作废。这对 Web API 尤其难:既要「不必每个请求都登录」, 又要「想收回就能收回」3。

登录流程(sign-in flow)≠ 认证流程。 用户看到的是四步 (想访问受限资源 → 系统要凭据 → 给出凭据 → 放行/拒绝);后台实际有八步, 认证流程随架构技术差异很大——最朴素的形式是浏览器全包,开发者几乎无事可做, 但用户体验会差4。

认证三要素(信任从哪来): 你知道的(密码——好记和好猜永远打架,密码管理器用 「你持有的」来守「你知道的」)、你持有的(加密密钥、硬件令牌、证书)、 你本身的(指纹、虹膜、人脸——biometry 生物识别,手机最熟)。 本章只用前两种:密码和密钥5。

3. 四种 HTTP 层认证方案,各有什么破绽

3.1 Basic:最朴素,也最危险

流程全靠浏览器:服务器回 401 + 一个标准头 → 浏览器弹原生登录框 → 用户提交 → 浏览器把凭据拼成 用户名:密码 做 base64 编码(一种只换字母表、不含任何加密的编码,谁都能解),塞进 Authorization: Basic <编码> 头再发 → 服务器验完放行。 服务端默认要「假定这是个认证请求、先试着验一次」,不能上来就 401, 否则用户永远没有登录机会6。

破绽(书里加粗的事实):base64 是编码,不是加密——谁拿到都能解。 没有 HTTPS 绝对不许用 Basic7。

Rust 侧的准备(可复用的工具): 书里写了个 extract_auth_from_header: AuthScheme 枚举(Basic/Bearer),从 AUTHORIZATION 头剥掉 scheme 前缀、 取出剩下的部分8。两条路可以选:中间件挡所有保护路由,或每个 handler 挂提取器逐个性验(简单但有重复);书里两者都没选,留了 stub 给后面复用。 想偷懒有现成的 axum-auth crate(AuthBasic((id, password)) 直接当参数)9。

验证明 Stub 也得说一句:查库比对 creds.password == user.password—— 书里红字:生产环境绝不许这样!密码根本不该被「存下来」,应该加盐 (salt)做不可逆哈希,只比哈希;安全领域,要用经过独立安全审计的第三方库10。

3.2 Digest、Bearer、API key:各退一步,各留一手

方案改进了什么仍然脆在哪
Digest不发密码本体,发「用户名+密码+realm(保护域)」的哈希;服务端客户端多一轮协商协商本身可被中间人降级(有的实现拿 Basic 兜底);哈希本身泄露就等于密码泄露(可重放)
Bearer发预生成的令牌,服务端验令牌令牌可被窃听;单打独斗弱,配 JWT 才是正经用法(OAuth 2.0 出身)
API key形式自由:能塞 query、path、cookie、自定义头安全全押在用户和客户端能不能藏好它;丢了就像丢了物理钥匙——换锁(作废重发)

Digest 唯一值得带走的新意是「先协商、后放行」——这个思想 JWT 原样继承11。

4. 表单认证 + 真会话:axum-login 全家桶

解决什么问题: 原生登录框没法定制 UX。网站/SPA 都想自己画登录界面—— 这就是表单认证,技术上通常和会话搭配:会话数据存服务器, 会话 ID 随每个请求旅行,客户端靠它维持登录态12。

REST API 是无状态的,「经典意义上的会话」本不该有——书里提醒记住这一点; 但服务器端渲染(SSR)那一套(同域、Askama 模板、cookie 会话)值得学会, 所以加了一组全新的服务端点,不动现有 API 和 Yew 客户端13。

工具链(tower 生态): cargo add axum-login。

  • Session store: MemoryStore(够用了;也能换数据库);
  • 两个 layer: SessionManagerLayer::new(store) + AuthManagerLayerBuilder::new(auth_backend, session_layer).build();
  • AuthnBackend trait(名字拗口,就一个方法):要实现三个类型——User(身份模型)、 Credentials(LoginDto,放 shared 前后端共用)、Error。authenticate 验凭据, get_user 按 id 取人——authenticate 我们在 Basic 那里写过,直接复用14。

路由层的精细活: 层是自下而上生效的—— route_layer(login_required!(AuthBackend, login_url = "/login")) 放在 /protected 下面,只罩它上面的路由;/login、/logout 保持公开15。

登录页: Askama 模板(#[template(path = "login.html")])+ CSRF token (CsrfToken,token 字段进 LoginDto,可兼作 CSRF 位;redirect 字段记登录后回哪)16。

do_login 的三道关: 先验 CSRF token → auth_session.authenticate(dto) 真验身份 → auth_session.login(&user)(login 只是把身份存进会话的别名, 真认证在 authenticate 里)→ 重定向17。登出就是 auth_session.logout()。

完整流程走一遍(主线例子的上半程): 用户敲 /protected → login_required! 中间件查会话,没身份就带着 ?next= 重定向到 /login → 提交表单 → 服务器验 CSRF + 验身份 → 重定向回受保护页 → 之后随便逛,刷新页面会话也在(cookie 里的会话 ID 随请求走) → 点 /logout,会话销毁18。幕后是 tower-session:请求来就现造 cookie、 每次都从 cookie 里捞会话 ID——我们马上要为自己的 JWT 写一个同款中间件19。

5. 注册与授权三机制

注册: 最简版就是把 user create 端点公开。书里特意留了作业: 将来要加邮件验证——不然匿名用户能灌水灌进一堆假数据(发邮件要等第 8 章)20。

认证只能区分「登录 vs 匿名」,管不了「经理 vs 开发者」。 授权三机制:

机制是什么优劣
ACL(访问控制列表)资源 → 允许的用户/组 → 操作 的映射表每个用户都要登记到每张资源名单,太繁;改进:按组登记。表可放文件或库——入库就能在线改,即成动态 ACL(第 10 章)
RBAC(基于角色的访问控制)判定标准从「名单」换成「角色」,还能限定动作(helpdesk 只能发密码重置链接,读密码、改资料都不行)粒度更细、定义更直白;代价:要盯着别让用户攒一堆用不到的权限
JWT服务器签发一枚自带权限和有效期的令牌,签名(可选加密),偷了也改不了;服务器无状态——信息全在请求里天生配 REST API;实现细节别自己写,Rust 已有现成(下一节用)

JWT 有个认真的毛病:前端存得住就偷得着,签出去还难撤回。 药方是 refresh token(书中的贴士):JWT 只存前端内存; 客户端重启后用存本地的 refresh token 找服务器换一枚新的; refresh token 放缓存里,想撤就让它在缓存里失效——两个病一起治21。

6. 前后端安全过滤(本章重头戏)

动机: 只挡后端,用户会看到一堆没内容的破页面;前端得会应付拒绝、 随时能提供登录入口。要做的事一共六件:

  1. 后端加 JWT 认证;2. 后端存 refresh token 的地方;3. 后端开 refresh token 交换端点;
  2. 前端引入 identity 概念进全局状态;5. 前端对受保护页做「无身份就重定向」;
  3. 前端自动 JWT 恢复逻辑22。

6.1 决策流程(防无限循环是题眼)

  页面加载 → 受保护吗?
    ├─ 否 → 直接渲染
    └─ 是 → 内存里有 JWT(identity)吗?
        ├─ 有 → 带 JWT 请求数据,渲染受保护页
        └─ 否 → local storage 里有 refresh token 吗?
            ├─ 有 → 调交换端点换 JWT → 成功:存内存,重试渲染
            │                    └─ 失败:删掉这个 refresh token,渲染登录页
            └─ 没有 → 渲染登录页

  图说:注意「失败就删 refresh token」——不删的话,刷新→失败→刷新会无限循环;
  删掉,循环最多一圈。(这是书里明写的坑。)

这套逻辑每个受限页面加载都跑一遍,用户得到的体验「像经典会话网站一样无缝」, 但全程没有真会话、没有 cookie 参与23。

6.2 后端三件事

存: Redis(第 2 章提过的键值库)登场——快,而且以后还能干别的缓存。 authenticate_raw 端点:Basic 头取凭据 → 验身份 → jsonwebtoken crate 签 JWT → uuid 生成 refresh token(全局唯一,永不相撞)→ 存 Redis:key=refresh token, value=JWT → 把 key 返回给客户端24。 refresh token 从 cookie 或 Bearer 头都能来(两种客户端都照顾)25。

验: 自己写 tower service + layer——请求进来,验 JWT、查用户还在不在; 挂在所有受保护资源路由下面,没票或票废一律 401。 写书时 Axum 还没有现成的 JWT 层,「我们得自己造;这正是 tower-session 在幕后 干的那类事,马上我们就亲手干一遍」26。

6.3 前端三件事

identity 进状态: AppState 加 identity: Option<LoginDto> 和 referer; 状态要实现 Reducible trait(reduce 收到一个完整的新状态),再用 AppStateProvider 组件 + ContextProvider 广播给整棵组件树; update_identity 帮手方法负责「复制旧值、只改身份」27。

路由守卫: 受保护页的 create 里订阅 context,identity.is_none() 就 navigator.replace(&Route::Login)28。登录页提交时不真发 HTML 表单, 而是 JSON 发 /authenticate(带 Basic 头),拿回 refresh token 存 local storage(web-sys 的 storage API,需开 storage feature)29。

自动恢复: try_authenticate_async:内存有 JWT 直接返回; 否则从 local storage 取 refresh token,带 Bearer 头打交换端点; 成功 → update_identity(注意密码位填空串、用户名占位 <Faded>—— 恢复出来的身份只有 token,没有人会再拿它去登录)→ 失败 → 删掉 local storage 里的 refresh token 再报错(循环只能转一圈的保险丝)30。

7. 主线例子走一遍:匿名到「刷新不掉线」

  匿名用户打开 /projects(受保护)
      │  ① identity 是 None → 重定向到 /login
      ▼
  用户填表提交 → 前端 base64(用户名:密码) → POST /authenticate(Basic 头)
      │  ② 后端验身份 → 签 JWT → uuid 当 refresh token 存 Redis → 返回 refresh token
      ▼
  前端把它存 local storage;带 Bearer refresh token 调 /refresh-token → 拿到 JWT
      │  ③ JWT 只进内存(Reducible 状态广播);此后请求都带 Bearer JWT
      ▼
  用户按 F5 刷新页面 ——内存清空,但 local storage 还在
      │  ④ try_authenticate:refresh token 换 JWT 成功 → 会话「自动恢复」
      ▼
  三天后 refresh token 被服务器端作废(Redis 里删了)
      │  ⑤ 换 JWT 失败 → 前端删掉 local storage 里的废票 → 回登录页(循环只一圈)

  图说:六步清单全部落位;「废票即删」是整条链上唯一一处显式防坑。
  (端点名、状态名出自原文;Redis/uuid/jsonwebtoken 是书里选的 crate。)

8. 作者的判断与证据(分开标)

有出处的: base64≠加密、Digest 的可重放弱点、JWT 的难撤销 + refresh token 药方, 均为书中明写71121;axum-login/tower-session/jsonwebtoken 的用法是工具链事实。

作者自己的评估:

  • 「密码必须加盐哈希、安全库必须用经过独立审计的」是引书建议(Web Application Security 等), 书里反复红字强调10;
  • 「页面级守卫是样板代码,更好做法是抽成 provider」被作者标为 out of scope 的已知改进31;
  • JWT 层「写书时没有现成的,自己造」是时间快照,今天请先查生态再动手。

9. 边界与局限

  • 密码仍是明文比对的教学 stub(verify_password 留在注释里)——第 8 章也不修, 真实系统第一天就要哈希;
  • CSRF 只在表单/会话流里出现,JWT Bearer 方案天然免疫(CSRF 打的是 cookie 自动携带);
  • 「自称」的身份恢复:恢复出的 identity 用户名是占位符——前端展示用户名需要另取;
  • 刷新 token 永不过期(直到被删)——生产要加 TTL,书里未展开(第 10 章的缓存可以)。

10. 可带走的

主线例子一行走完: 匿名访问受保护页 → 重定向登录 → Basic 认证换 refresh token (存 Redis)→ Bearer 换 JWT(存内存)→ 带票访问 → F5 后 refresh token 自动恢复 → 废票即删、回登录页。

  1. 认证 ≠ 授权 ≠ 会话,三个词先分开,再谈方案。
  2. base64 不是加密;Basic 没有 HTTPS 就是裸奔。
  3. Digest 的哈希可重放、Bearer 怕窃听、API key 丢了就换锁——没有银弹,只有组合。
  4. 密码只存加盐哈希;安全库选经过独立审计的。
  5. 「登录」是把身份存进会话,认证是验凭据——axum-login 里两个调用分得很清。
  6. 路由保护用 route_layer(login_required!),层自下而上生效,公开路由别被误罩。
  7. 授权三机制按粒度递进:ACL → RBAC → JWT; JWT 配 refresh token 治「难撤销」。
  8. 无会话也能有「会话感」: JWT 内存 + refresh token 本地 + 废票即删 + 路由守卫。
  9. 每段自动恢复逻辑都要问一句:失败路径会不会转圈?

11. 原文地图

主题原书章原文位置
认证定义Access Control Solutionstext/48-fm-terminologies.txt:10(搜「confirming the authenticity」)
授权与「最平凡授权」同上text/48-fm-terminologies.txt:19(搜「granular access」)、:28(搜「trivial form」)
会话寿命与安全平衡同上text/48-fm-terminologies.txt:34(搜「optimal lifespan」)、:37(搜「invalidate」)
登录流程前后台八步同上text/48-fm-terminologies.txt:43(搜「sign-in flow」)
信任三要素同上text/47-fm-introduction-to-access-control.txt:17(搜「Something you know」)
Basic 流程与先验后 401同上(Basic authentication)text/49-fm-basic-authentication.txt:14(搜「401 Unauthorized」)、:14(搜「assume an authentication request」)
base64≠加密同上text/49-fm-basic-authentication.txt:27(搜「encoded, but not encrypted」)
extract_auth_from_header同上text/49-fm-basic-authentication.txt:111(搜「extract_auth_from_header」)
axum-auth 贴士同上text/49-fm-basic-authentication.txt:185(搜「axum-auth」)
密码哈希红字同上text/49-fm-basic-authentication.txt:268(搜「Never do this in production」)
Digest 的可重放弱点同上(Digest authentication)text/50-fm-digest-authentication.txt:7(搜「leaked hash」)
Bearer 与 API key同上text/50-fm-digest-authentication.txt:16(搜「vulnerable to eavesdropping」)、:22(搜「physical keys」)
表单认证与会话同上(Form-based authentication)text/51-fm-form-based-authentication.txt:7(搜「session ID」)、:37(搜「stateless」)
axum-login 三件套同上text/51-fm-form-based-authentication.txt:216(搜「MemoryStore」)、:228(搜「AuthManagerLayerBuilder」)
login_required 路由层同上text/51-fm-form-based-authentication.txt:291(搜「login_required」)、:276(搜「bottom up」)
CSRF 与 authenticate vs login同上text/51-fm-form-based-authentication.txt:417(搜「verify」)、:489(搜「cover name」)
tower-session 幕后同上text/51-fm-form-based-authentication.txt:534(搜「on the fly」)
注册与邮件验证伏笔同上(User registration)text/52-fm-user-registration.txt:4(搜「email verification」)
ACL/RBAC/JWT 对比同上text/52-fm-user-registration.txt:19(搜「mapping resources」)、:130(搜「helpdesk」)、:139(搜「signed and optionally encrypted」)
refresh token 药方同上text/52-fm-user-registration.txt:149(搜「serious flaw」)
六步清单同上(Proper filtering)text/53-fm-proper-frontend-and-backend-security-filtering.txt:11(搜「JWT authentication」)
决策流程与防循环同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:53(搜「Read it like this」)、:59(搜「infinite loop」)
Redis + uuid + jsonwebtoken同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:215(搜「Redis」)、:218(搜「universally unique」)、:338(搜「jsonwebtoken」)
cookie 或 Bearer 双通道同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:248(搜「cookie OR bearer」)
自造 JWT 层同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:458(搜「typical tower service」)
Reducible 与 Provider同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:550(搜「Reducible」)、:601(搜「ContextProvider」)
路由守卫同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:98(搜「navigator」)
local storage 与废票即删同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:529(搜「local_storage」)、:857(搜「delete」)
「样板代码,更好是 provider」同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:690(搜「boilerplate」)、:705(搜「out of scope」)

6.4 后端实现的逐行账(authenticate_raw 全流程)

把第 6.2 节的三段代码合成一条完整的请求生命周期,每步旁边是它的职责 与失败出口(依据原书 Code listing 7.17–7.19):32

  POST /authenticate(带 Authorization: Basic <base64>)
      │
      ├─ extract_auth_from_header(headers, Basic)          # 剥 scheme 前缀
      │     └─ 失败 → 400 + {message:"No auth token found"}
      ├─ base64 解码 → "user:pass" 按冒号切两半              # Credentials 成形
      │     └─ 失败 → 400 "Invalid token"
      ├─ auth_backend.authenticate(LoginDto{…})              # 查库比对
      │     └─ 无此用户 → 401 "Invalid credentials"
      ├─ jsonwebtoken::encode_jwt(user.name)                 # 签 JWT
      │     └─ 失败 → 500 系(ApiError 包装)
      ├─ Uuid::new_v4() → key                                # refresh token 发号
      ├─ redis con.set(key, jwt)                             # 键值落缓存
      │     └─ Redis 挂了 → 500 + {origin:"db_error"} 的同类
      └─ Ok(key)                                             # 只把 key 给客户端

  图说:六步里四步有明确的失败出口;密码本体从未离开过服务器内存。

「第一轮 Basic、后续 Bearer」的混合制(书里明说的取舍):第一步用 Basic(传统、浏览器原生支持),之后的请求全走 Bearer JWT—— 「Basic 仍然不加密,所以我们有责任保护数据;JWT 永远过,这就是它 只住内存的原因」33。

自造 JWT 过滤层的三要素(原书一段话说全的):它是个典型 tower service——收请求、按自家逻辑筛;筛的内容是「验 JWT + 查用户还在不在」; 通过则放行,否则一律 401 Unauthorized;挂在所有受保护资源路由之下。 「写书时 Axum 还没有现成的 JWT 层」——这句话到 2026 年请先重查生态再 决定要不要自己造34。

6.5 前端身份状态的完整形状(AppState 全字段)

AppState 在本章定形,值得把字段全摆出来(依据原书 Code listing 7.14 与 7.26):35

字段类型用途
dialogOption<…>全局弹窗(登录框随时能起)
navbar_activebool导航栏展开态
identityOption<LoginDto>本章主角:None = 匿名
refererOption<Route>登录成功后回跳的目标

Reducible 的实现为什么「偷懒」:type Action = AppState + reduce 直接返回新状态——「未参数化,只能传完整 AppState」;于是配套纪律是 帮手方法(update_identity)负责「拷贝旧值、只改一个字段」。 「页面成了状态的生产者」——services 保持无状态(只回调),状态变更 集中在持有状态的页面里;这套 shape 就是后来 Redux 系的状态管理雏形36。

透明认证(try_authenticate)的三层短路(原书 7.28 清单的决策表):37

层检查结果
内存identity 有非空 token?有 → 直接返回,零请求
本地local storage 有 refresh token?无 → 报错,走登录页
交换Bearer 打 /refresh-token成功 → update_identity(密码置空、用户名占位 <Faded>);失败 → 删 local storage 的废票 再报错

7.1 与前后章的债务关系(本章欠谁、谁欠本章)

  • 欠第 4 章: DTO/ValidatedJson/From<Model> 全是第 4 章收口的装备;
  • 欠第 6 章: Redis 是第 6 章(本组 11 篇)进场的,本章只是它的第二个用途;
  • 欠下的债: 密码明文比对(verify_password 还在注释里)、邮件验证注册 (第 8 章发信才有了材料)、refresh token 无 TTL、页面守卫的样板代码 (「更好的做法是抽成 provider」作者自己点了名但留在 scope 外)38。

7.2 认证方案速查表(一句话记住四种方案的破绽)

方案凭证形态核心破绽(原书口径)
Basicbase64(用户名:密码)编码≠加密;每请求重发;无 HTTPS 即裸奔
Digest哈希(用户+密码+realm)可降级回 Basic;哈希泄露=密码泄露(可重放)
Bearer预发令牌窃听即到手;单打独斗弱,配 JWT 才是形态
API key随机串,走哪都行安全全押在藏得好;丢了换锁(作废重发)39

表单认证与会话为什么另起炉灶: 前四种都是「浏览器与服务器之间的 低层协商」;表单认证把 UI 收回自己手里,代价是自己管会话—— 而 REST 的无状态约束让「经典会话」名不正言不顺,于是第 6 节那条 「无会话的会话感」路线(JWT 内存 + refresh token 本地)成了本章的 落点。从 Basic 到 JWT,是同一场需求(「认识 recurrent 的用户」) 换了四代解法40。

4.1 表单认证全流程的时序账本(axum-login 的每一步)

把第 4 节的机制铺成请求时序(依据原书 Figure 7.3/7.4 与 login_controller 各 handler):41

  1. 浏览器 GET /protected
       → login_required! 中间件查 AuthSession:无身份
       → 302 重定向 /login?next=/protected(记下回跳目标)
  2. GET /login
       → CsrfToken 提取器生成一次性令牌
       → Askama 模板 LoginTemplate{token, redirect} 渲染表单(令牌藏表单里)
  3. POST /login(Form<LoginDto>)
       → 先验 CSRF token(token.verify(&dto.token)),不匹 → 401
       → auth_session.authenticate(dto):查库比对身份
       → auth_session.login(&user):把身份写进会话(「login 是存身份的别名」)
       → 302 到 dto.redirect.unwrap_or("/login-success")
  4. 之后任意请求:cookie 里的会话 ID 随请求走,tower-session 自动恢复会话
       → 刷新页面也保持登录
  5. GET /logout → auth_session.logout() → 会话销毁,302 回 /login

  图说:会话的载体是 cookie 里的 ID;会话的数据在服务器(MemStore/数据库)。

「会话永远在创建」的幕后事实(书里揭秘的一句):「即使没有身份可存, 会话也在被创建——tower-session 在后台干这个:请求来就现造 cookie、 每次都从 cookie 捞会话 ID。」第 6 节的自造 JWT 层,就是照这个套路写的 「无会话版同款」42。

CSRF 为什么只在表单流里出现: CSRF 攻击打的是「cookie 会被浏览器 自动携带」这个特性;Bearer JWT 方案里令牌不会自动随行,攻击面天然消失—— 两种认证形态的安全清单不一样,书里在表单流里装了 CSRF,在 JWT 流里 没提,不是疏漏43。

4.2 AuthnBackend 的三个关联类型(动手前先看这)

实现 AuthnBackend 之前要定好三个类型,书里逐个给了定义:44

  • type User = entity::users::Model——身份就长实体那样;
  • type Credentials = LoginDto——来自 shared 成员(前后端共用, token 字段留作 CSRF、redirect 记回跳);
  • type Error = DbErr——SeaORM 的错误直通。

两个方法:authenticate(creds)(验身份,Basic 章写过的逻辑直接复用)、 get_user(id)(按主键取人,User::find_by_id(*user_id).one(&self.db))。 「写 Basic 时我们已实现过 authenticate——现在是复用时刻」, 这也是本章把 Basic 的 stub 留到前面的原因45。

8. 本章与全书的接口账(认证体系的三次升级)

  第 7 章第一次升级:Basic(明文 base64)→ 表单会话(cookie + 服务端状态)
      解决的问题:UX 自定义、凭据不再每请求重发
  第 7 章第二次升级:会话 → JWT(无状态)+ refresh token(可撤销)
      解决的问题:REST 的无状态约束 + 「想撤就撤」
  第 10 章(本组 14 篇)第三次升级:静态规则 → Casbin 动态策略
      解决的问题:权限变更不再发版
  ——每一次升级都保留上一代的成果:Basic 的头解析函数被 JWT 轮复用,
    JWT 层的手艺(tower service)又来自会话中间件的解剖课。

  图说:认证体系的演化史,就是「破绽驱动」的编年史。

这一章最容易被低估的一句: 「授权不能没有认证;认证本身就是最平凡的 授权」——所有「登录才能看」的功能,其实已经在做授权;RBAC/JWT 只是把 这个能力拧到更细的粒度46。

7.3 安全过滤的三层架构图(前后端分工全览)

  浏览器 ──► 前端路由守卫(identity 在内存吗?没有 → 重定向 /login)
      │
      │  请求带 Bearer JWT
      ▼
  后端 tower JWT 层(验签 + 查用户存在?)── 401 即止
      │
      ▼
  handler(ValidatedJson 校验 → 业务 → ActiveModel)
      │
      ▼
  数据库(外键/唯一约束兜底)

  图说:每一层都假设上面的层会被绕过 —— 前端守卫挡的是体验,后端层挡的是数据。

「前端守卫不是安全机制」的再强调: 浏览器里的一切都可被绕过(用户 可以自己 curl);前端守卫买的是体验(不显示破页面),后端过滤买的是 安全(不放行真请求)。六步清单里前三件是安全、后三件是体验—— 分开记账,别拿一个顶替另一个47。

11. 可带走的(再补一条)

  1. 认证体系的每一代都保留上一代的成果——Basic 的头解析被 JWT 轮复用, 会话中间件的手艺(tower service)来自 tower-session 的解剖; 读旧代码时,先问「它给下一代留了什么」。

12. 原文地图补充

主题原书章原文位置
tower JWT 层的三个职责Access Control Solutionstext/53-fm-proper-frontend-and-backend-security-filtering.txt:458(搜「typical tower service」)
恢复身份的占位用户名同上text/53-fm-proper-frontend-and-backend-security-filtering.txt:827(搜「Faded」)
「login 是存身份的别名」同上(Form-based)text/51-fm-form-based-authentication.txt:489(搜「cover name」)

13. 可带走的(再补两条)

  1. 「错误文案来自当事方」——路由层说路径、提取器说 JSON、数据库说连接; handler 不替它们翻译,只包一层统一格式。
  2. refresh token 双通道(cookie 或 Bearer)是为了两种客户端各得其所; 废票即删是防无限循环的保险丝——每条自动恢复逻辑都要检查自己的失败路径。

14. 本篇卷尾(一句话版)

访问控制章的全部贡献,是一条「破绽驱动」的演进线:Basic 的明文逼出 会话,会话的状态违背 REST 逼出 JWT+refresh token,静态规则逼出 Casbin 动态策略——每一代都留下可复用的手艺,这正是读旧代码时 最该找的东西。48

15. 本篇卷尾补记(六步清单的复用)

「后端验票、前端认人、废票即删」这套六步骨架,同样适用于任何 「无状态的会话感」设计(移动端、第三方集成):先把身份存内存、 续命凭证存本地、失败路径能自收敛——三条齐备再谈体验。49

16. 本篇卷尾补记(访问控制验收单)

收卷前三问:密码哈希了吗(本章还是占位,生产必改)?refresh token 废票即删了吗?前端守卫与后端验票分开记账了吗?三问全过,会话体系 才算闭环。50

Footnotes

  1. 出处:「Terminologies」(text/48-fm-terminologies.txt:10,搜「confirming the authenticity」)。 ↩

  2. 出处:「Terminologies」(text/48-fm-terminologies.txt:19,搜「granular access」)、:28(搜「trivial form of authorization」)。 ↩

  3. 出处:「Terminologies」(text/48-fm-terminologies.txt:34,搜「optimal lifespan」)、:37(搜「invalidate」)。 ↩

  4. 出处:「Terminologies」(text/48-fm-terminologies.txt:43,搜「sign-in flow」)。 ↩

  5. 出处:「Introduction to access control」(text/47-fm-introduction-to-access-control.txt:17,搜「Something you know」)、:42(搜「biometry」)。 ↩

  6. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:14,搜「assume an authentication request」)。 ↩

  7. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:27,搜「encoded, but not encrypted」)。 ↩ ↩2

  8. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:111,搜「extract_auth_from_header」)。 ↩

  9. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:185,搜「axum-auth」)。 ↩

  10. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:268,搜「Never do this in production」)、:283(搜「security audits」)。 ↩ ↩2

  11. 出处:「Digest authentication」(text/50-fm-digest-authentication.txt:7,搜「leaked hash」)、:10(搜「negotiation mechanism」)。 ↩ ↩2

  12. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:7,搜「identified with a token」)。 ↩

  13. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:37,搜「stateless」)、:43(搜「add entirely new functionality」)。 ↩

  14. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:216,搜「MemoryStore」)、:228(搜「AuthManagerLayerBuilder」)、:160(搜「three types」)。 ↩

  15. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:291,搜「login_required」)、:276(搜「bottom up」)。 ↩

  16. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:321,搜「Template」)、:210(搜「CSRF」)。 ↩

  17. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:489,搜「cover name」)。 ↩

  18. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:541,搜「requests a protected resource」)、:567(搜「cookie with the session ID」)。 ↩

  19. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:534,搜「on the fly」)。 ↩

  20. 出处:「User registration」(text/52-fm-user-registration.txt:4,搜「email verification」)。 ↩

  21. 出处:「User registration」(text/52-fm-user-registration.txt:149,搜「serious flaw」)。 ↩ ↩2

  22. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:11,搜「Add JWT authentication」)。 ↩

  23. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:53,搜「Read it like this」)、:56(搜「seamless experience」)。 ↩

  24. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:215,搜「Redis」)、:218(搜「universally unique」)、:338(搜「jsonwebtoken」)。 ↩

  25. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:248,搜「cookie OR bearer」)。 ↩

  26. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:458,搜「typical tower service」)。 ↩

  27. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:550,搜「Reducible」)、:601(搜「ContextProvider」)、:625(搜「update_identity」)。 ↩

  28. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:98,搜「navigator」)。 ↩

  29. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:474,搜「AUTHENTICATE_ENDPOINT」)、:173(搜「local_storage」)。 ↩

  30. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:716,搜「try_authenticate_async」)、:857(搜「delete」)、:825(搜「Faded」)。 ↩

  31. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:690,搜「boilerplate code」)、:705(搜「out of scope」)。 ↩

  32. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:131,搜「authenticate_raw」)、:356(搜「decode」)、:452(搜「always pass」)。 ↩

  33. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:455,搜「Basic scheme for the first step」)。 ↩

  34. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:458,搜「typical tower service」)。 ↩

  35. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:68,搜「identity: Option」)、:71(搜「referer」)。 ↩

  36. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:541,搜「services are stateless」)、:574(搜「UseReducerHandle」)。 ↩

  37. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:716,搜「try_authenticate_async」)、:825(搜「Faded」)、:857(搜「delete」)。 ↩

  38. 出处:「Proper frontend and backend security filtering」(text/53-fm-proper-frontend-and-backend-security-filtering.txt:705,搜「provider」);密码比对见 text/49-fm-basic-authentication.txt:268(搜「Never do this」)。 ↩

  39. 出处:「Basic authentication」(text/49-fm-basic-authentication.txt:27,搜「not encrypted」);「Digest authentication」(text/50-fm-digest-authentication.txt:7,搜「leaked hash」)。 ↩

  40. 路线归纳为我们的整理;各代方案定义见原书第 7 章 Basic/Digest/Bearer/API keys/Form-based 各节。 ↩

  41. 时序依据:「Form-based authentication」(text/51-fm-form-based-authentication.txt:541,搜「requests a protected resource」)、:556(搜「verifies the input」)、:567(搜「cookie with the session ID」)。 ↩

  42. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:534,搜「on the fly」)。 ↩

  43. CSRF 与 cookie 绑定的机制为 Web 平台通用知识(补充,不在书里)。 ↩

  44. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:160,搜「three types」)、:121(搜「AuthnBackend」)。 ↩

  45. 出处:「Form-based authentication」(text/51-fm-form-based-authentication.txt:49,搜「we have already implemented」)。 ↩

  46. 出处:「Terminologies」(text/48-fm-terminologies.txt:28,搜「trivial form of authorization」)。 ↩

  47. 分工为我们的工程注记;六步清单见 text/53-fm-proper-frontend-and-backend-security-filtering.txt:11。 ↩

  48. 卷尾为我们的归纳;演进线见本篇 §8 与 14-admin-caching.md §4–5。 ↩

  49. 复用提醒见本篇 §6–§7;六步清单见原书 text/53-fm-proper-frontend-and-backend-security-filtering.txt:11。 ↩

  50. 验收单为我们的产品化整理;三问对应本篇 §3.1、§6.1、§7.3。 ↩

On this page