03 · 认证与授权:Session / JWT / OAuth2 与权限模型
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 前端天天在处理 token,本篇讲清服务端的完整设计:你是谁(认证)与你能做什么(授权) |
| 核心内容 | Session 与 JWT 的真实取舍、Token 存放位置、刷新机制、OAuth2/OIDC 适用场景、RBAC 与 ABAC |
| 读完能回答 | 我该用 Session 还是 JWT?token 放 localStorage 还是 Cookie?退出登录怎么做到真正失效? |
| 相关板块 | 02 · API 设计(401 与 403 的区别)| E01 · Python 基础 | 08 · 前端安全 |
一、先分清两件事
| 概念 | 英文 | 回答的问题 | HTTP 表现 |
|---|---|---|---|
| 认证 | Authentication / AuthN | 你是谁 | 401 |
| 授权 | Authorization / AuthZ | 你能做什么 | 403 |
这两个词常被混用,但它们是两个独立的环节。 认证通过不代表有权限——登录成功只能说明"你是你",能不能看这份数据要看授权。
二、Session 与 JWT:真实的取舍
| 维度 | Session(服务端状态) | JWT(无状态) |
|---|---|---|
| 状态存在哪 | 服务端(Redis / DB) | 客户端(token 自带) |
| 服务端要查吗 | 每次查一次存储 | 验签即可,不查库 |
| 立即失效 | ✅ 删掉 session 就生效 | ❌ 天然做不到(见下) |
| 水平扩展 | 需要共享存储(Redis) | ✅ 天然支持 |
| 跨域 / 多服务 | 要处理 Cookie 域与共享 | ✅ 天然适合 |
| token 体积 | 一个 ID,很小 | 携带信息,较大 |
| 泄露后的影响 | 删 session 即止 | 在过期前一直有效 |
2.1 JWT 的"无法立即失效"是它最大的代价
JWT 一旦签发,在过期前服务端无法单方面作废(因为不查库)。常见补救:
| 方案 | 做法 | 代价 |
|---|---|---|
| 短过期 + 刷新 | access token 5~15 分钟,靠 refresh token 续 | 仍需 refresh token 的黑名单机制 |
| 黑名单 / 版本号 | 验签后再查一次 Redis(是否作废) | 等于又变回有状态 |
| 换 Session | 干脆不用 JWT 存会话 | 简单直接 |
💡 一句大实话:如果你需要"踢人下线""退出登录立刻生效""查看在线设备", 那你最终一定会引入服务端状态——此时 JWT 的"无状态优势"已经没了。
判断框架:
- 需要即时失效 / 强管控 → Session
- 服务间调用、短期授权、跨域 SSO → JWT
- 拿不准 → Session,它更不容易出错
2.2 JWT 的结构(要知道里面没加密)
eyJhbGciOiJIUzI1NiIs... . eyJ1c2VySWQiOjEyM30... . HMAC签名
Header(算法) Payload(载荷) Signature⚠️ Payload 只是 Base64 编码,不是加密。任何人都能解出来看。 绝不能放密码、身份证、敏感业务数据。只放 userId、角色、过期时间这类非敏感信息。
三、Token 该放哪:前端最关心的问题
| 存放位置 | XSS 风险 | CSRF 风险 | 说明 |
|---|---|---|---|
| localStorage | ❌ 高风险:XSS 可直接读取 | ✅ 免疫(不自动发送) | 一旦有 XSS,token 就没了 |
| Cookie(HttpOnly + Secure + SameSite) | ✅ JS 读不到 | ⚠️ 需要防 CSRF | 推荐的常规做法 |
| 内存(变量) | ✅ 较好 | ✅ | 刷新页面即丢失,体验差 |
推荐组合:
access token → HttpOnly Cookie(JS 读不到,防 XSS)
CSRF 防护 → SameSite=Lax/Strict + 同源校验 + 关键操作二次校验
refresh token → 更严格的 Cookie(路径限制 / 更长有效期 / 可轮换)为什么不用 localStorage:XSS 无法完全避免(第三方脚本、依赖投毒、富文本), 而 localStorage 对 JS 完全开放。HttpOnly Cookie 是当前最稳妥的默认选择。
四、OAuth2 与 OIDC:什么时候才需要
| 名词 | 是什么 | 你需要吗 |
|---|---|---|
| OAuth2 | 授权框架:让第三方应用拿到"有限权限" | 做第三方登录 / 开放平台时 |
| OIDC | 在 OAuth2 之上加了一层身份认证 | 需要"确认用户是谁"时 |
| SSO | 单点登录:一次登录,多系统通用 | 企业内部多系统 |
常见误解:"用微信登录"用的是 OAuth2 + OIDC,不是 OAuth2 单独。 OAuth2 只解决"授权",不解决"你是谁"——后者是 OIDC 的
id_token提供的。
什么时候不需要 OAuth2:
- 自己的系统、自己的用户 → 自己发 session/JWT 就够了
- 只有一套前后端 → 不要为了"看起来专业"上 OAuth2
引入 OAuth2 的代价是真金白银的:授权服务器、client 管理、scope 设计、token 生命周期。 没有第三方参与就不要上。
五、权限模型:从简单到复杂
| 模型 | 表达 | 适用 |
|---|---|---|
| ACL | 用户 → 资源 → 权限(直接授予) | 少量资源、简单场景 |
| RBAC | 用户 → 角色 → 权限 | 绝大多数业务系统的默认选择 |
| ABAC | 基于属性(部门/时间/金额)动态判断 | 规则复杂、需要上下文 |
演进建议:从 RBAC 起步,遇到"同一个角色在不同部门权限不同"这类需求时, 再局部引入 ABAC。不要一上来就做通用权限引擎——那是投入产出比很低的坑。
一条容易忽略的原则:权限校验必须在服务端做,且在数据访问层再做一次。
❌ 只在前台隐藏按钮 → 直接调接口照样能访问
❌ 只在网关校验一次 → 内部调用、批量接口容易绕过
✅ 接口层校验 + 数据层过滤 → 越权查询从 SQL 层面就被拦住尤其注意 IDOR(越权访问):
/orders/123改成/orders/124就能看别人的订单。 防御方式是查询时带上归属条件(where id = ? and user_id = ?),而不是先查后判。
六、自检清单
- [ ] 能区分 401 与 403,并知道各自该在什么情况返回
- [ ] 说得出 JWT 的最大代价(无法立即失效)
- [ ] 知道 JWT 的 Payload 不是加密
- [ ] token 存放在 HttpOnly Cookie,且有 CSRF 防护
- [ ] 退出登录能真正让会话失效
- [ ] 密码用 bcrypt/argon2 等慢哈希存储(不是 MD5/SHA)
- [ ] 权限校验在数据层也有兜底(防越权)
- [ ] 知道自己不需要 OAuth2(除非有第三方参与)
- [ ] 关键操作(改密码、付款)有二次验证
七、常见误区
| 误区 | 事实 |
|---|---|
| "JWT 更安全" | 两者安全性取决于实现;JWT 的代价是无法即时失效 |
| "JWT 里多存点信息方便" | Payload 只是编码不加密,且会让 token 变大 |
| "token 放 localStorage 最方便" | XSS 一来就全没了;HttpOnly Cookie 更稳 |
| "用了 HTTPS 就不用担心 token" | HTTPS 防的是传输窃听,防不了 XSS 与本地泄露 |
| "前端隐藏了按钮就等于没权限" | 接口必须独立校验,否则等于裸奔 |
| "登录成功后就不用再校验权限了" | 每次数据访问都要带归属条件 |
| "OAuth2 是登录协议" | 它是授权框架;身份认证靠 OIDC |
| "自己实现一套加密方案" | 用成熟库。密码学不要自己造 |
八、动手练习
练习 A:设计一次"退出登录"
要求:用户点退出后,所有设备上的会话立即失效。 想一想:纯 JWT 能做到吗?做不到的话你会怎么改?
练习 B:找出越权漏洞
@app.get("/orders/{order_id}")
def get_order(order_id: int, user=Depends(current_user)):
return db.query(Order).filter(Order.id == order_id).first()这里有安全漏洞吗?怎么修?
提示:换个
order_id会发生什么。
练习 C:判断要不要上 OAuth2
给三个场景(自家后台管理系统、支持微信登录的 App、要给合作方开放数据接口), 分别判断是否该引入 OAuth2,并说明理由。
相关文档
本区
- 02 · API 设计 —— 401 / 403 的语义、幂等与版本
- 01 · 运行时选型
- C · 后端计划
其他域
- 08 · 前端安全 —— XSS / CSRF 的前端视角
- 09 · 安全治理与合规 —— AI 场景的数据合规
- E01 · Python 语言基础