编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 体系建设优化

  • 代码品质工坊

  • 稳定性与可靠性

  • 工程化与运维

  • 方案设计思想

    • README
    • 通用架构设计方案
    • 组件化方案的设计
    • SDK设计与发布方案
    • 缓存架构设计思想
    • 数据库SQL设计思想
    • 分库分表方案设计
    • 分布式ID生成方案
    • 消息队列方案选型
    • 认证授权方案设计
      • 一、案例引入:48 万 Token 挂到 GitHub 的那 3 小时
        • 1.1 事故现场:从"实习生一次提交"到"全线拖库"
        • 1.2 顺藤摸根因:不是"实习生的锅",是"架构没兜底"
        • 1.3 七个"为什么":疑惑清单
      • 二、架构决策三角:安全 × 体验 × 可扩展
      • 三、认证 ≠ 授权:先把两个概念钉死
        • 3.1 定义与例子
        • 3.2 为什么要严格分离
        • 3.3 现代架构的分工
      • 四、Session vs JWT:状态与无状态的世纪之争
        • 4.1 Session 机制的本质:服务端记账
        • 4.2 JWT 的本质:签名自证 + 无状态
        • 4.3 两者的正反面:一张表看清
        • 4.4 关键推论:JWT 不能主动作废是它最大的原罪
        • 4.5 选型速查
      • 五、双令牌机制:AccessToken + RefreshToken 数学化推导
        • 5.1 双令牌的角色分工
        • 5.2 换取流程
        • 5.3 为什么 refresh 要"旋转"(Rotation)
        • 5.4 泄露影响的数学量化
        • 5.5 双令牌的实施陷阱
      • 六、OAuth 2.0:把认证委托出去的四种姿势
        • 6.1 四种授权模式:不是并列关系,是安全等级递减
        • 6.2 授权码模式的经典流程
        • 6.3 PKCE:无后端场景的"secret 替代品"
        • 6.4 OIDC = OAuth 2.0 + 身份
      • 七、SSO 单点登录:CAS、SAML、OIDC-SSO 一次讲清
        • 7.1 SSO 的三代技术
        • 7.2 CAS 单点登录时序
        • 7.3 SSO 最难的三件事
        • 7.4 现代选择:Keycloak / Auth0 / 自研
      • 八、RBAC 与权限模型:从"角色爆炸"到 ABAC
        • 8.1 RBAC:主流但会"爆炸"
        • 8.2 RBAC 分层:把爆炸控制住
        • 8.3 数据级权限:光有"角色"不够
        • 8.4 ABAC:策略语言驱动的下一代权限
      • 九、微服务鉴权:网关不是终点,服务侧要有"二次校验"
        • 9.1 单层网关鉴权的问题
        • 9.2 分层鉴权模型
        • 9.3 服务间调用的三种鉴权方式
        • 9.4 防横向越权:最容易被漏掉的一层
      • 十、综合案例串讲:把 7 问全部回扣
        • 10.1 逐条回答一开始的 7 问
        • 10.2 事故重演:如果当时用了本文方案
        • 10.3 一个用户的"登录一生"完整时序
        • 10.4 四条设计哲学
        • 10.5 认证授权速查表
        • 10.6 上线检查清单(20 项)
      • 十一、写在最后
    • API网关设计方案
    • 路由库设计思想
    • 网络检测方案设计
    • 幂等性设计方案
    • 分布式锁方案设计
    • 限流熔断方案设计
    • 移动端防抓包实践
    • 通用轮训方案设计
    • 状态机设计的思想
    • 20.实时通信设计原理
  • 性能优化实践

  • 真经
  • 方案设计思想
杨充
2026-06-27
目录

认证授权方案设计

# 10.认证与授权方案设计

本篇定位:认证(AuthN,你是谁)和授权(AuthZ,你能干什么)是所有互联网系统绕不开的两块基石。做对了外人进不来、内人越不了权、微服务传得下、注销传得开;做错了 Token 泄露就是拖库,SSO 挂了就是全站崩,权限漏洞就是越权。本文从一次 48 万 Token 被挂到 GitHub 的事故讲起,回答七个刺入骨髓的问题——认证与授权到底怎么分?Session 和 JWT 该怎么选?OAuth2 的四种模式到底解决什么?RBAC 到底怎么设计不爆炸?

# 一、案例引入:48 万 Token 挂到 GitHub 的那 3 小时

# 1.1 事故现场:从"实习生一次提交"到"全线拖库"

某中型 SaaS 公司,做企业协同办公。安全同学在做例行 GitHub Dork 扫描(针对公司域名的关键字扫码泄露),凌晨 2 点扫到一条震撼级线索——一个刚入职三周的前端实习生,把本地 .env 文件误提交到了公开仓库。文件里躺着 48 万条历史 JWT Token 的采样日志(他为了排查一个 Token 解析问题在本地跑过一批用户的 Token)。

2:00 安全值班同学发现,紧急拉群。 2:15 尝试在 GitHub 后台下架仓库——仓库已经被 fork 了 3 次,其中一个 fork 账号的 IP 归属地在境外。 2:30 决定所有 Token 全部作废、强制全体用户重新登录。问题来了:JWT 是无状态的,一签发就"泼出去的水",服务端根本没有黑名单机制——48 万个 Token 到期前每一个都可以正常调用 API。 2:45 紧急上线一个"发布签名密钥 v2",把 JWT 校验里的密钥换掉——又崩了:Web、iOS、Android、桌面客户端、开放平台第三方接入方六端总共 200 万活跃用户全部被踢下线。客服电话被打爆。 5:00 密钥切换完成,全线恢复登录。事后统计:3 小时窗口内境外 IP 用泄露 Token 拉走的数据估计有 20~40GB,涉及 3 家客户的合同、员工薪资、客户名单。

# 1.2 顺藤摸根因:不是"实习生的锅",是"架构没兜底"

复盘会锁定 7 处设计缺陷(后续每一章都是它们的解药):

# 缺陷 直接后果
① JWT 里塞了太多信息(用户名、部门、角色、菜单权限……) Token 被抓包一次 = 用户完整画像
② JWT 有效期设成了 30 天 泄露 30 天内都能用
③ 没有 Refresh Token 双令牌机制 想缩短有效期就要用户天天登录
④ 没有黑名单 / 版本号机制 泄露只能靠"换密钥"这种核弹级操作
⑤ 网关做了鉴权,业务服务完全不做二次校验(只信任 header) 内网横向通吃
⑥ 权限校验只做到"能否访问 URL",没做数据级越权检查 用别人 Token 能查别人数据
⑦ 密钥用代码里的常量,没走 KMS / 秘密管理 换密钥要发新版本

一个实习生的一次误操作暴露了整个认证体系的七个洞。这不是运气问题,是架构没有兜底。

# 1.3 七个"为什么":疑惑清单

事故会上灵魂拷问:

  1. 认证和授权到底是不是一回事?为什么老是搞混?
  2. Session 好好用为什么大家都在往 JWT 迁?JWT 比 Session 好在哪、差在哪?
  3. JWT 说是"无状态",出了事故想让它失效——真的没办法吗?
  4. AccessToken + RefreshToken 双令牌到底解决了什么?只用一个不行吗?
  5. OAuth2 那四种授权模式到底怎么选?为啥有 authorization_code 还要 PKCE?
  6. RBAC 模型看起来简单,为什么到几百个角色就爆炸了?
  7. 网关都鉴权了,业务服务还要做什么?如何防"横向越权"?

这 7 问是本文的骨架。走完第 10 章会全部串起来。

# 二、架构决策三角:安全 × 体验 × 可扩展

认证授权系统本质上是在三个维度上做联合最优化:

              安全(防泄露、防越权、可作废)
                     ▲
                    /│\
                   / │ \
                  /  │  \
                 / 认 │ 授\
                /  证  │ 权 \
               /   系  │  统 \
              ────────┼──────
        体验(少登录、跨端、SSO)      可扩展(微服务、多租户)

任何"银弹方案"号称三边全占的,都在骗人:

  • Session + Cookie:安全高(服务端可控),扩展差(多机要 Session 共享)。
  • 纯 JWT 无状态:扩展极佳(服务端 0 存储),安全弱(作废难、泄露=灾难)。
  • OAuth2 授权码:安全 + 扩展双高,体验代价——多一个 302 跳转。

架构选择就是"接受哪个代价",而不是"哪个方案最好"。金融接受"每 15 分钟重新输密码"换安全,社交产品接受"7 天免登"换体验,B 端 SaaS 接受"SSO 中心化"换可扩展。

# 三、认证 ≠ 授权:先把两个概念钉死

新人(甚至一些老兵)最常犯的错就是混淆 AuthN 和 AuthZ。它们不是一件事,也不该由一个模块负责。

# 3.1 定义与例子

概念 英文 一句话 生活类比
认证 Authentication (AuthN) 你是谁? 门卫看你的身份证
授权 Authorization (AuthZ) 你能干什么? 你的门禁卡能开哪些门

认证是"我是我"的证明;授权是"我能做什么"的许可。一个人可以有身份证,但不代表他能进机房。

# 3.2 为什么要严格分离

把两者混在一起做会导致:

  • 改鉴权改到业务代码里去:因为角色/权限跟"我是谁"混在一个 Filter 里,加个角色要动登录模块。
  • 难以支持第三方登录:用微信登录进来"这个人是谁"清楚,但"这个人在你系统里能干什么"就没答案——微信只做 AuthN,AuthZ 是你自己的事。
  • 难以做细粒度权限:如果 JWT 里塞了整套权限,权限变更要重发 Token;分离后权限只在服务端查,Token 里只放 userId。

# 3.3 现代架构的分工

┌────────── 认证服务 (AuthN) ──────────┐  ┌────────── 授权服务 (AuthZ) ──────────┐
│  ・登录(密码、验证码、扫码、第三方) │  │  ・角色管理(RBAC)                   │
│  ・签发 Token(access/refresh)      │  │  ・权限点(Permission)               │
│  ・Token 校验(签名 + 过期)         │  │  ・策略引擎(ABAC / OPA)             │
│  ・SSO 单点登录                     │  │  ・数据权限(行级/列级过滤)           │
└──────────────────────────────────────┘  └───────────────────────────────────────┘

记住这一条:Token 里只放 userId + 少量必要字段(tenantId、deviceId),永远不要在 Token 里塞权限——权限变了、Token 还是老的,就是漏洞。

# 四、Session vs JWT:状态与无状态的世纪之争

这是所有面试必问、所有团队必吵的话题。答案是"看场景,各有其位",但要知道每一条边界。

# 4.1 Session 机制的本质:服务端记账

1. 客户端登录 → 服务端生成 sessionId → 写入服务端存储(内存/Redis)
2. Set-Cookie: sessionId=xxx; HttpOnly; Secure
3. 后续请求带 Cookie → 服务端根据 sessionId 查 Session 表 → 恢复用户上下文

特点:服务端有"账本",可以随时改(作废、更新用户信息、踢下线)。

# 4.2 JWT 的本质:签名自证 + 无状态

JWT 的结构(三段 Base64URL 拼接):

Header.Payload.Signature

Header:    {"alg":"HS256","typ":"JWT"}
Payload:   {"sub":"user123","exp":1706788800,"tenantId":"t01"}
Signature: HMACSHA256(base64(Header)+"."+base64(Payload), secret)

服务端每次收到 Token 只做一件事——用密钥重算签名,对上就信任 Payload 内容。服务端不存 Token、不查库、不管账。

# 4.3 两者的正反面:一张表看清

维度 Session(有状态) JWT(无状态)
服务端存储 每人一条 Session 记录 无
横向扩展 需 Session 共享(Redis / Sticky) 天然支持
主动作废 删 Session 记录即可 极难(需黑名单)
信息量 只在服务端存,Cookie 只带 ID 需要的字段全塞 Token,膨胀风险
跨域 Cookie 跨域受限(SameSite) Bearer Token 天然跨域
跨端 Cookie 不适合 App / 桌面 Token 各端都能用
CSRF Cookie 自动携带 = 易受 CSRF Bearer 头需手动带 = 天然免疫
XSS HttpOnly Cookie 抗 XSS localStorage 存 Token 易被 XSS 拿
典型场景 传统 Web、有状态后台管理 微服务、移动 App、开放 API

# 4.4 关键推论:JWT 不能主动作废是它最大的原罪

回到本文事故——48 万个 Token 已经签发,30 天内每一个都合法。JWT 无状态的代价就是"泼出去的水收不回"。修复只有 4 条路:

  1. 换签名密钥:所有 Token 一次性作废——误伤范围最大(本文事故做的就是这个,200 万活跃用户被踢)。
  2. 黑名单:把要作废的 jti(JWT ID)塞进 Redis,鉴权时查一次——JWT 变"半有状态",牺牲了它的核心卖点。
  3. 短 access + 长 refresh 双令牌:让 access token 只活 15 分钟——泄露的 access 15 分钟就没用了。
  4. 版本号 / tokenVersion:JWT 里塞 ver: 3,服务端存每个用户的当前 tokenVersion,不一致就拒——依然要查库,但比黑名单更聚焦。

"用哪一条"直接决定事故止损时间。生产系统一般 3 + 4 组合使用。

# 4.5 选型速查

  • 传统单体后台管理系统 → Session + Redis 共享,简单可靠。
  • 多端产品(Web+App+桌面) → JWT + 双令牌 + 黑名单/版本号兜底。
  • 开放平台 / 三方接入 → OAuth2 授权码(下一章讲)。
  • 金融级:Session 优先,服务端可随时踢下线是金融合规的硬要求。

# 五、双令牌机制:AccessToken + RefreshToken 数学化推导

单令牌的取舍是"长有效期方便但危险,短有效期安全但用户天天登"。双令牌把这对矛盾分层解开。

# 5.1 双令牌的角色分工

令牌 有效期 用途 存放位置 泄露影响
AccessToken 15 分钟 ~ 2 小时 调 API 时带 内存 / 短期 localStorage 有效期内可用
RefreshToken 7 天 ~ 30 天 只用来换新 access 加密安全存储 / HttpOnly Cookie 一票作废、绑设备

# 5.2 换取流程

[初次登录]
User → AuthServer: 用户名/密码
AuthServer → User: { access:AT_1 (15min), refresh:RT_1 (30d) }

[15 分钟后 access 过期]
User → API: 请求 (AT_1)  ── 401 Expired
User → AuthServer: /refresh (RT_1)
AuthServer 校验 RT_1 + 查库确认未作废
AuthServer → User: { access:AT_2, refresh:RT_2 }  ── 关键:refresh 也旋转
User 用 AT_2 继续请求

[30 天到期或用户主动登出]
User → AuthServer: /logout (RT_x)
AuthServer 把 RT_x 标记为 revoked → 所有相关 access 15 分钟内自然过期

# 5.3 为什么 refresh 要"旋转"(Rotation)

安全模型:假设 RT 被盗,攻击者用 RT_1 换到 AT_2 + RT_2,同时受害者也在用——两个客户端同时用同一个 RT_1 去换新令牌。

  • RT 不旋转:两边都能不断换,攻击者可以永久保有会话——根本发现不了。
  • RT 旋转:RT_1 → RT_2,一个人换成功之后 RT_1 就废了。另一端下次请求会 401——立刻被系统发现"这个 RT 被两处用了",触发**"全家作废"**(把该用户的所有 refresh 都作废,强制重登)。这就是 Refresh Token Rotation with Automatic Reuse Detection(OAuth 2.1 建议做法)。

# 5.4 泄露影响的数学量化

假设 AT 15 分钟、RT 30 天、每次 rotation 也 30 天窗口:

  • 单令牌 30 天:泄露损失 = 30 天 × 全权限 = 43,200 分钟任意调用。
  • 双令牌:泄露 AT,损失 = 15 分钟 × 全权限 = 15 分钟。
  • 双令牌 + rotation:泄露 RT,攻击者用一次就被检测到"重复使用",损失 ≤ 15 分钟。

降险 3000 倍。这就是为什么"双令牌 + 旋转"是 OAuth 2.1 的默认推荐。

# 5.5 双令牌的实施陷阱

  • RT 不能放 localStorage:一旦 XSS,30 天权限交出去了。建议 HttpOnly Cookie 或原生 App 的安全存储(Keychain / Keystore)。
  • 换令牌的 endpoint 要做防刷:/refresh 是所有过期 AT 的必经之路,容易被扫描器打爆。
  • 要监控"同一 RT 短时间在多个 IP 使用":这是盗号的强信号。

# 六、OAuth 2.0:把认证委托出去的四种姿势

OAuth 2.0 常被误解为"登录协议"——它不是。它是"授权协议"——让第三方拿到有限权限访问用户资源的协议(你允许"某小软件"读你的微博)。用来做"用微信登录我们的 App"叫 OpenID Connect (OIDC),是 OAuth 2.0 上叠的一层。

# 6.1 四种授权模式:不是并列关系,是安全等级递减

模式 grant_type 场景 安全性
授权码 authorization_code Web 站点、有后端的 App ⭐⭐⭐⭐⭐
授权码 + PKCE authorization_code + PKCE 移动 App、SPA(无后端) ⭐⭐⭐⭐⭐
客户端凭证 client_credentials 机器对机器(M2M)、后端服务 ⭐⭐⭐⭐
密码模式(Password) password 已被 OAuth 2.1 废弃 ⭐
隐式模式(Implicit) implicit 已被 OAuth 2.1 废弃 ⭐

OAuth 2.1(起草中)的核心变化:只保留 authorization_code (含 PKCE) 和 client_credentials,其余全删。

# 6.2 授权码模式的经典流程

[用户在你 App 里点"微信登录"]
Your App                     User Browser              WeChat OAuth
──────                       ────────────              ────────────
① redirect →               → https://open.weixin/authorize
                             ?client_id=your_app
                             &redirect_uri=https://your.com/callback
                             &scope=userinfo
                             &state=random_nonce

② 用户在微信页面授权(同意)

                                                     ← redirect 到 callback
③ Your.com/callback ← code=xxx&state=random_nonce

④ 后端拿 code 换 token(走后端到后端):
   Your Backend → https://open.weixin/token
                  { code, client_id, client_secret }
                → { access_token: WT_1, openid: xxx }

⑤ 拿 access_token 调用微信 API 拿 userinfo
   Your Backend → https://open.weixin/userinfo?access_token=WT_1
                → { nickname, avatar, ... }

⑥ 后端把"微信 openid" 与你系统的 userId 绑定 → 签发自家 JWT 给前端

关键设计要点:

  • code 只在浏览器出现一次,服务端拿到 code 后立刻换 token(code 有效期一般 5 分钟,一次性用完作废)。
  • client_secret 只在后端出现,前端永远看不到——这就是为什么 SPA / 移动 App 没有 secret 时要用 PKCE 替代。
  • state 参数防 CSRF:由客户端生成随机数,回调时校验一致——忘了这一步 = 授权码劫持漏洞(本文事故的邻居坑)。
  • redirect_uri 必须白名单:不做严格校验的话攻击者可以把 code 引到自己的域名。

# 6.3 PKCE:无后端场景的"secret 替代品"

移动 App、SPA 没有 client_secret(客户端里的秘密都不算秘密),怎么防"code 被中间人截获"?答案是 PKCE(Proof Key for Code Exchange):

① 客户端生成 code_verifier(随机 43~128 字符)
   计算 code_challenge = BASE64URL(SHA256(code_verifier))

② 授权请求:带上 code_challenge
   /authorize?client_id=...&code_challenge=xxx&code_challenge_method=S256

③ 授权服务器把 code_challenge 与 code 绑定存起来

④ 客户端换 token 时带上 code_verifier
   /token?code=xxx&code_verifier=YYY

⑤ 授权服务器校验:SHA256(code_verifier) == 之前存的 code_challenge
   通过 → 发 token

为什么这能防止 code 被劫持?攻击者就算截到 code,没有 code_verifier 换不到 token——而 verifier 只在原客户端内存里。

2020 年后:所有移动 App / SPA 场景,PKCE 是强制的。

# 6.4 OIDC = OAuth 2.0 + 身份

OAuth 2.0 只给你"访问某资源的令牌",不告诉你"用户到底是谁"。OIDC 在 authorization_code 流程上加了一个 id_token(JWT 格式),里面装了 sub、email、name 等身份声明——这才是"社交登录"的正确协议。

微信、QQ、GitHub、Google、Apple、Facebook 现在的登录接口都是 OIDC 或类 OIDC。

# 七、SSO 单点登录:CAS、SAML、OIDC-SSO 一次讲清

企业内部一堆系统(HR、CRM、ERP、OA、报销、内网 Wiki),如果每个都要登一遍——员工每天光输密码就要 30 分钟。SSO 就是解决"一次登录,全站畅通"。

# 7.1 SSO 的三代技术

代次 代表 特点
第一代:Session 共享 相同域名下共享 Cookie 只能同域,扩展性差
第二代:CAS (Central Authentication Service) 耶鲁 CAS 中心认证,通过 ticket 传递
第三代:SAML 企业老牌,XML 协议 复杂但稳,B2B 场景标配
第四代:OIDC-SSO Keycloak / Auth0 / Okta 基于 OIDC + JWT,云原生首选

# 7.2 CAS 单点登录时序

User        App-A                  CAS Server            App-B
────        ─────                  ──────────            ─────
访问 A →   未登录 302 → CAS
                                   显示登录页
                                   用户登录成功
                                   ← redirect 到 A?ticket=ST-1
校验 ticket → A 向 CAS: /validate?ticket=ST-1
                                   CAS 校验,返回 user 信息
                                   → 建立 A 的会话
用户操作 A(正常)

访问 B →   未登录 302 → CAS
                                   CAS 检测到已有全局 Session(TGC Cookie)
                                   直接签发新 ticket ST-2
                                   ← redirect 到 B?ticket=ST-2
校验 ticket → B 向 CAS: /validate?ticket=ST-2
                                   → 建立 B 的会话
用户不再需要输密码

关键概念:

  • TGC (Ticket Granting Cookie):CAS 域名下的全局 Session Cookie,"你在 CAS 已经登录了"的凭据。
  • ST (Service Ticket):一次性、绑定单个应用的临时票据。5 分钟有效,用完作废。

# 7.3 SSO 最难的三件事

难点一:单点登出(SLO) —— 用户在 A 点了退出,B 和 C 该怎么办? 方案有两条:前端广播(CAS 把登出通知发给所有已注册的应用回调 URL)或心跳检测(每个应用定期回问 CAS "我这个 session 还活着吗")。前者实时但复杂,后者简单但有延迟。

难点二:跨域 —— SSO 中心和各应用不同域,Cookie 传不了。 用 CAS 的 ticket 传(本身就是为这场景设计的)或 OIDC 的 id_token。

难点三:SSO 自己不能挂 —— 一挂全崩。

  • SSO 集群多机部署。
  • 各应用做"降级模式":如果 SSO 不可达,允许用之前签发过、还没过期的 Token 继续用一段时间——牺牲一点安全换可用性。

# 7.4 现代选择:Keycloak / Auth0 / 自研

  • 中小团队:直接用 Keycloak(开源、Java、功能完备)。
  • 不差钱、要海外合规:Auth0 / Okta(云服务,MAU 收费)。
  • 超大规模 / 有特殊定制:基于 Spring Authorization Server 或 OIDC 库自研。

# 八、RBAC 与权限模型:从"角色爆炸"到 ABAC

授权层的核心问题:给定一个用户和一个操作,判断能不能做。这个函数 can(user, action, resource) 内部长啥样,决定了整个权限系统的复杂度。

# 8.1 RBAC:主流但会"爆炸"

RBAC(Role-Based Access Control)三元素:

User ──── UserRole ──── Role ──── RolePermission ──── Permission
                        │
                        ├─ 普通员工
                        ├─ 部门主管
                        ├─ 部门总监
                        └─ 超级管理员

优点:概念清晰、DBA 好维护。 致命缺点:当业务有"部门 × 岗位 × 项目 × 客户 × 数据敏感级"多维交叉时,角色数量爆炸。

举例:一家 SaaS 公司业务上有 10 个部门 × 5 个岗位级别 × 3 个数据敏感级 = 150 个角色。再加"跨部门临时项目组"、"某客户专属权限"、"某季度大促临时授权"……很快就到几百上千个角色,运维崩溃。

# 8.2 RBAC 分层:把爆炸控制住

不要用扁平 RBAC,用RBAC1(有继承)+ RBAC2(有约束):

Role: 数据分析师
  ├─ 继承 Role: 员工基础 (所有员工共有权限)
  ├─ 加:读取 dw 库
  ├─ 加:使用报表工具
  └─ 排斥(互斥约束):不能同时是"审批人"(利益冲突)

Role: 高级数据分析师
  ├─ 继承 Role: 数据分析师
  └─ 加:读取脱敏前敏感表

收益:150 个角色可以压缩到 20 个基础角色 + 继承树。

# 8.3 数据级权限:光有"角色"不够

RBAC 只能回答"能不能访问 URL /order/list"——但回答不了"用户只能看自己部门的订单"。数据级权限得靠行级过滤(Row-level Security):

-- 应用层查询时自动注入过滤条件
SELECT * FROM orders 
WHERE dept_id IN (${current_user.dept_and_children})  -- 行过滤
  AND status != 'DELETED';

这就要在 ORM 层做拦截器(MyBatis Interceptor / Hibernate Filter),或者用数据库原生的行级安全(PostgreSQL RLS)。

# 8.4 ABAC:策略语言驱动的下一代权限

ABAC(Attribute-Based Access Control)把权限判断变成策略表达式:

allow if
  user.department == resource.department
  and user.level >= resource.required_level
  and current_time between 09:00 and 18:00
  and request.ip in company_network

优点:一条策略搞定复杂条件组合,不需要建几百个角色。 代价:策略引擎(Open Policy Agent / OPA 是事实标准)需要单独部署,学习曲线陡。

结论:

  • < 50 个角色的中小系统 → RBAC 够用。
  • 有多维交叉、动态策略的大型系统 → RBAC + ABAC 混合(角色定粗粒度,ABAC 补细粒度)。
  • 超大规模 / 云原生 → OPA 独立部署,所有服务通过 OPA 询权限。

# 九、微服务鉴权:网关不是终点,服务侧要有"二次校验"

事故里的第 ⑤ 条缺陷——"网关做了鉴权、业务服务不做校验"——是微服务时代最普遍的坑。

# 9.1 单层网关鉴权的问题

Client → API Gateway (校验 JWT ✓) → 业务服务 (完全信任 header)
                                     ↑
                                     内网横向调用(无鉴权)
                                     内网穿透(无鉴权)
                                     绕过网关直连(无鉴权)

风险:任何能进入内网的攻击者(包括你的服务被 RCE 后作为跳板)都能通吃所有业务服务。

# 9.2 分层鉴权模型

第一层(网关):粗粒度
  ・签名验证 + 过期检查
  ・全局限流
  ・注入 X-User-Id、X-Tenant-Id 到下游

第二层(业务服务):细粒度
  ・接口级权限校验(这个用户能不能调这个 API)
  ・数据级校验(这个用户能不能操作这条数据)  ← 防越权关键
  ・信任传递机制(内部调用要有服务间凭证)

第三层(数据层):兜底
  ・行级安全(RLS)
  ・审计日志

# 9.3 服务间调用的三种鉴权方式

服务 A → 服务 B 的调用要不要鉴权?必须。方案:

方案 原理 适用
透传用户 Token A 收到用户请求时的 Token 原封不动传给 B 简单,但 Token 生命周期跨服务后难管
服务身份(M2M Token) A 用自己的 client_id/client_secret 换 Token 调 B 服务间清晰,但丢失用户上下文
组合模式 服务 Token + X-User-Id header 传用户身份 生产推荐
mTLS + 服务网格 双向 TLS 证书身份,业务不感知 Service Mesh 场景(Istio)

# 9.4 防横向越权:最容易被漏掉的一层

接口越权的经典代码:

// ❌ 漏洞:只校验了"登录",没校验"是不是自己的订单"
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id) {
    return orderRepo.findById(id);  // 任何登录用户传别人的 id 都能查
}

// ✅ 修复:数据级校验
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable Long id, @CurrentUser User user) {
    Order order = orderRepo.findById(id);
    if (!order.getUserId().equals(user.getId())) {
        throw new AccessDeniedException("not your order");
    }
    return order;
}

根治方案:Repository 层强制注入 userId 条件——业务代码想漏都漏不了。

public interface OrderRepo {
    // Repository 里的方法必须传 userId
    Order findByIdAndUserId(Long id, Long userId);
}

# 十、综合案例串讲:把 7 问全部回扣

# 10.1 逐条回答一开始的 7 问

# 疑问 答案(章节)
① 认证与授权的区别? §3:AuthN 你是谁 / AuthZ 你能干什么,必须分离
② Session vs JWT 怎么选? §4:单体后台 Session,多端产品 JWT 双令牌
③ JWT 能不能主动作废? §4.4:黑名单 / 版本号 / 换密钥,各有代价
④ 双令牌解决什么? §5:AT 短、RT 长 + rotation,泄露损失降 3000 倍
⑤ OAuth2 四种模式怎么选? §6:只用 authorization_code + PKCE 和 client_credentials
⑥ RBAC 怎么不爆炸? §8:分层 RBAC + 数据级过滤,超大规模上 ABAC
⑦ 网关鉴权后业务服务干嘛? §9:分层鉴权,业务层做数据级校验防横向越权

# 10.2 事故重演:如果当时用了本文方案

回到开头的 48 万 Token 泄露:

若当时用了完整方案:

  1. JWT 有效期 = 15 分钟(不是 30 天)→ 泄露 15 分钟后自然失效。
  2. RT 存在服务端 Redis + 版本号机制 → 一个命令 SET tokenVersion:userId 2 让所有历史 RT 失效,重登时刷 access。
  3. JWT 里不塞权限,只塞 userId → 泄露的 Token 不包含用户画像。
  4. 密钥走 KMS → 想换密钥不用发新版本。
  5. 业务服务做数据级校验 → 就算 Token 被用,也拿不到跨用户数据。

结果:事故止损时间从 3 小时缩到 5 分钟,数据泄露从 20~40GB 降到几 KB,用户无感知。

# 10.3 一个用户的"登录一生"完整时序

── T0: 首次登录 ─────────────────────────────
User → LoginPage → 输入手机号+密码
    ↓
AuthServer 验证 → 生成
    ・access_token AT_1 (JWT, sub=U123, ver=1, exp=T0+15m)
    ・refresh_token RT_1 (opaque, 存 Redis, exp=T0+30d)
    ↓
Set-Cookie: refresh_token=RT_1; HttpOnly; Secure; SameSite=Strict
返回 AT_1 给前端(放内存或短期 localStorage)

── T0 ~ T0+15m: 正常调用 ────────────────────
每次请求 Authorization: Bearer AT_1
Gateway 校验签名 + 过期 → 注入 X-User-Id → 转发业务

── T0+15m: AT 过期 ─────────────────────────
下一次请求返回 401
前端调用 /refresh (Cookie 自动带 RT_1)
AuthServer 校验 RT_1
    ・查 Redis 存在 ✓
    ・检测有无并发使用 ✓
    ・生成 AT_2 + RT_2
    ・Redis: DEL RT_1, SET RT_2
    ↓
Set-Cookie: refresh_token=RT_2
返回 AT_2 (Rotation 完成)

── T0+3h: 用户在别的设备登录 ─────────────────
新设备走一遍 T0 流程 → 拿到 AT_A / RT_A
原设备 RT_1 依然有效(多设备共存,通过 Redis 独立记录)

── T0+2d: 用户点"退出" ──────────────────────
Client → /logout (带当前 RT)
AuthServer: Redis DEL RT → 15 分钟内所有 AT 自然失效
    可选加强: SET tokenVersion:U123 = 2, 让所有历史 JWT 立即失效

── T0+15d: 事故! Token 疑似泄露 ──────────────
运维: SET tokenVersion:U123 = 3
所有该用户历史 JWT 校验时发现 ver=1 ≠ 3 → 拒绝
用户下次访问被引导重新登录

── T0+30d: RT 自然过期 ──────────────────────
Redis TTL 到 → RT_x 自动删除
用户下次访问 401 → 引导重新登录

# 10.4 四条设计哲学

哲学一:认证与授权是两件事。 认证回答"你是谁",授权回答"你能干什么"。混在一起做的系统必定难维护。Token 里只放身份,不放权限。

哲学二:无状态不是免费的午餐。 JWT 的"扩展性好"是用"作废难"换来的。生产环境必须用双令牌 + 版本号/黑名单——纯无状态 JWT 只能玩玩 demo。

哲学三:多层防御,不信任任何单点。 网关鉴权只是第一层。业务服务的数据级校验是防越权的最后一道墙。Repository 层强制注入 userId 是最优雅的解法。

哲学四:安全参数一定要"绕最小圈"泄露。 JWT 密钥、client_secret、数据库密码——永远走 KMS / Vault / K8s Secret,绝不能是代码里的常量、.env 文件、配置中心明文。事故的根源正是这一条。

# 10.5 认证授权速查表

决策点 首选 备选 禁忌
Session vs JWT 多端产品 JWT 单体后台 Session JWT 30 天有效期
令牌机制 AT 15min + RT 30d + rotation 单 Token(简单场景) 单 Token 30 天
OAuth 模式 authorization_code + PKCE client_credentials(M2M) password / implicit
SSO Keycloak / Auth0 / OIDC-SSO 自研 CAS Session Cookie 跨子域
权限模型 分层 RBAC + 数据级过滤 RBAC + ABAC (OPA) 扁平 RBAC + 几百角色
微服务鉴权 分层:网关+业务+数据 mTLS + Service Mesh 只在网关做
密钥管理 KMS / Vault K8s Secret 代码常量 / .env
Refresh 存放 HttpOnly Cookie / Keychain 加密 localStorage 明文 localStorage

# 10.6 上线检查清单(20 项)

认证

  • [ ] 密码加盐哈希(bcrypt / Argon2,不用 MD5/SHA1)
  • [ ] 登录接口有防暴力破解(IP 频次 + 账号锁定)
  • [ ] 手机号/邮箱验证码有效期 ≤ 5min
  • [ ] 关键操作二次验证(改密码、支付)

令牌

  • [ ] AT 有效期 ≤ 1 小时
  • [ ] RT 存服务端,可主动作废
  • [ ] RT 旋转 + 重复使用检测
  • [ ] JWT 密钥走 KMS
  • [ ] JWT payload 不含权限、敏感字段

授权

  • [ ] AuthN 与 AuthZ 模块分离
  • [ ] 权限校验做到接口 + 数据两级
  • [ ] 越权测试(改 URL 参数试图访问他人数据)
  • [ ] 内部 API 也需鉴权(不能"内网免鉴权")

OAuth / SSO

  • [ ] redirect_uri 严格白名单
  • [ ] state 参数防 CSRF
  • [ ] PKCE 强制启用(移动/SPA)
  • [ ] SLO 单点登出方案定妥

运维

  • [ ] 登录/异常行为审计日志
  • [ ] 泄露应急预案:一键强制全体重登
  • [ ] 灰度演练:换密钥、作废批量 token

# 十一、写在最后

认证授权是"看不见的功课"——做得好时用户毫无察觉,做得差时一次事故就是全线塌方。48 万 Token 泄露的三小时里,暴露的不是运气问题,是架构没做多层兜底。

认证授权的三条底线:

  • AuthN / AuthZ 分离——概念不清所有后续设计都变形。
  • 双令牌 + 服务端可作废——JWT 的"无状态"只是宣传,生产必须有"状态"兜底。
  • 多层防御 + 最小权限——网关不是终点,Repository 才是最后一道墙。

下次面对"登录设计一下"、"加个权限模块"这种需求时,希望你脑子里冒出的不是"抄个 JWT demo",而是——"AT 多久、RT 多久、密钥走哪里、越权怎么防、SSO 挂了怎么办"。这,就是认证授权系统的真正功力。

#认证#授权#OAuth2#JWT#SSO
上次更新: 2026/07/03, 18:18:34
消息队列方案选型
API网关设计方案

← 消息队列方案选型 API网关设计方案→

最近更新
01
11.数据库的原理总结
07-12
02
11.操作系统原理总结
07-10
03
7.资深程序员软能力
07-10
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式