访问控制:认证、授权、会话,与前后端安全过滤¶
这一章给应用上锁。 前半是概念与选项:四个术语、三种认证凭据、四种 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. 前后端安全过滤(本章重头戏)¶
动机: 只挡后端,用户会看到一堆没内容的破页面;前端得会应付拒绝、 随时能提供登录入口。要做的事一共六件:
- 后端加 JWT 认证;2. 后端存 refresh token 的地方;3. 后端开 refresh token 交换端点;
- 前端引入 identity 概念进全局状态;5. 前端对受保护页做「无身份就重定向」;
- 前端自动 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 自动恢复 → 废票即删、回登录页。
- 认证 ≠ 授权 ≠ 会话,三个词先分开,再谈方案。
- base64 不是加密;Basic 没有 HTTPS 就是裸奔。
- Digest 的哈希可重放、Bearer 怕窃听、API key 丢了就换锁——没有银弹,只有组合。
- 密码只存加盐哈希;安全库选经过独立审计的。
- 「登录」是把身份存进会话,认证是验凭据——axum-login 里两个调用分得很清。
- 路由保护用
route_layer(login_required!),层自下而上生效,公开路由别被误罩。 - 授权三机制按粒度递进:ACL → RBAC → JWT; JWT 配 refresh token 治「难撤销」。
- 无会话也能有「会话感」: JWT 内存 + refresh token 本地 + 废票即删 + 路由守卫。
- 每段自动恢复逻辑都要问一句:失败路径会不会转圈?
11. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 认证定义 | Access Control Solutions | text/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
| 字段 | 类型 | 用途 |
|---|---|---|
| dialog | Option<…> | 全局弹窗(登录框随时能起) |
| navbar_active | bool | 导航栏展开态 |
| identity | Option<LoginDto> | 本章主角:None = 匿名 |
| referer | Option<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 认证方案速查表(一句话记住四种方案的破绽)¶
| 方案 | 凭证形态 | 核心破绽(原书口径) |
|---|---|---|
| Basic | base64(用户名:密码) | 编码≠加密;每请求重发;无 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. 可带走的(再补一条)¶
- 认证体系的每一代都保留上一代的成果——Basic 的头解析被 JWT 轮复用, 会话中间件的手艺(tower service)来自 tower-session 的解剖; 读旧代码时,先问「它给下一代留了什么」。
12. 原文地图补充¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| tower JWT 层的三个职责 | Access Control Solutions | text/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. 可带走的(再补两条)¶
- 「错误文案来自当事方」——路由层说路径、提取器说 JSON、数据库说连接; handler 不替它们翻译,只包一层统一格式。
- refresh token 双通道(cookie 或 Bearer)是为了两种客户端各得其所; 废票即删是防无限循环的保险丝——每条自动恢复逻辑都要检查自己的失败路径。
14. 本篇卷尾(一句话版)¶
访问控制章的全部贡献,是一条「破绽驱动」的演进线:Basic 的明文逼出 会话,会话的状态违背 REST 逼出 JWT+refresh token,静态规则逼出 Casbin 动态策略——每一代都留下可复用的手艺,这正是读旧代码时 最该找的东西。48
15. 本篇卷尾补记(六步清单的复用)¶
「后端验票、前端认人、废票即删」这套六步骨架,同样适用于任何 「无状态的会话感」设计(移动端、第三方集成):先把身份存内存、 续命凭证存本地、失败路径能自收敛——三条齐备再谈体验。49
16. 本篇卷尾补记(访问控制验收单)¶
收卷前三问:密码哈希了吗(本章还是占位,生产必改)?refresh token 废票即删了吗?前端守卫与后端验票分开记账了吗?三问全过,会话体系 才算闭环。50
-
出处:「Terminologies」(
text/48-fm-terminologies.txt:10,搜「confirming the authenticity」)。 ↩ -
出处:「Terminologies」(
text/48-fm-terminologies.txt:19,搜「granular access」)、:28(搜「trivial form of authorization」)。 ↩ -
出处:「Terminologies」(
text/48-fm-terminologies.txt:34,搜「optimal lifespan」)、:37(搜「invalidate」)。 ↩ -
出处:「Terminologies」(
text/48-fm-terminologies.txt:43,搜「sign-in flow」)。 ↩ -
出处:「Introduction to access control」(
text/47-fm-introduction-to-access-control.txt:17,搜「Something you know」)、:42(搜「biometry」)。 ↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:14,搜「assume an authentication request」)。 ↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:27,搜「encoded, but not encrypted」)。 ↩↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:111,搜「extract_auth_from_header」)。 ↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:185,搜「axum-auth」)。 ↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:268,搜「Never do this in production」)、:283(搜「security audits」)。 ↩↩ -
出处:「Digest authentication」(
text/50-fm-digest-authentication.txt:7,搜「leaked hash」)、:10(搜「negotiation mechanism」)。 ↩↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:7,搜「identified with a token」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:37,搜「stateless」)、:43(搜「add entirely new functionality」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:216,搜「MemoryStore」)、:228(搜「AuthManagerLayerBuilder」)、:160(搜「three types」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:291,搜「login_required」)、:276(搜「bottom up」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:321,搜「Template」)、:210(搜「CSRF」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:489,搜「cover name」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:541,搜「requests a protected resource」)、:567(搜「cookie with the session ID」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:534,搜「on the fly」)。 ↩ -
出处:「User registration」(
text/52-fm-user-registration.txt:4,搜「email verification」)。 ↩ -
出处:「User registration」(
text/52-fm-user-registration.txt:149,搜「serious flaw」)。 ↩↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:11,搜「Add JWT authentication」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:53,搜「Read it like this」)、:56(搜「seamless experience」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:215,搜「Redis」)、:218(搜「universally unique」)、:338(搜「jsonwebtoken」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:248,搜「cookie OR bearer」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:458,搜「typical tower service」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:550,搜「Reducible」)、:601(搜「ContextProvider」)、:625(搜「update_identity」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:98,搜「navigator」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:474,搜「AUTHENTICATE_ENDPOINT」)、:173(搜「local_storage」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:716,搜「try_authenticate_async」)、:857(搜「delete」)、:825(搜「Faded」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:690,搜「boilerplate code」)、:705(搜「out of scope」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:131,搜「authenticate_raw」)、:356(搜「decode」)、:452(搜「always pass」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:455,搜「Basic scheme for the first step」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:458,搜「typical tower service」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:68,搜「identity: Option」)、:71(搜「referer」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:541,搜「services are stateless」)、:574(搜「UseReducerHandle」)。 ↩ -
出处:「Proper frontend and backend security filtering」(
text/53-fm-proper-frontend-and-backend-security-filtering.txt:716,搜「try_authenticate_async」)、:825(搜「Faded」)、:857(搜「delete」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Basic authentication」(
text/49-fm-basic-authentication.txt:27,搜「not encrypted」);「Digest authentication」(text/50-fm-digest-authentication.txt:7,搜「leaked hash」)。 ↩ -
路线归纳为我们的整理;各代方案定义见原书第 7 章 Basic/Digest/Bearer/API keys/Form-based 各节。 ↩
-
时序依据:「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」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:534,搜「on the fly」)。 ↩ -
CSRF 与 cookie 绑定的机制为 Web 平台通用知识(补充,不在书里)。 ↩
-
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:160,搜「three types」)、:121(搜「AuthnBackend」)。 ↩ -
出处:「Form-based authentication」(
text/51-fm-form-based-authentication.txt:49,搜「we have already implemented」)。 ↩ -
出处:「Terminologies」(
text/48-fm-terminologies.txt:28,搜「trivial form of authorization」)。 ↩ -
分工为我们的工程注记;六步清单见
text/53-fm-proper-frontend-and-backend-security-filtering.txt:11。 ↩ -
卷尾为我们的归纳;演进线见本篇 §8 与
14-admin-caching.md§4–5。 ↩ -
复用提醒见本篇 §6–§7;六步清单见原书
text/53-fm-proper-frontend-and-backend-security-filtering.txt:11。 ↩ -
验收单为我们的产品化整理;三问对应本篇 §3.1、§6.1、§7.3。 ↩