From 9865fbf3b25520fabe362cd57ee63f78f5213f27 Mon Sep 17 00:00:00 2001 From: hezhaohui Date: Mon, 13 Jul 2026 10:08:46 +0800 Subject: [PATCH] vault backup: 2026-07-13 10:08:46 --- technical/sso.md | 95 ++++++++++++++++ technical/sso/sso-cas.md | 179 +++++++++++++++++++++++++++++ technical/sso/sso-comparison.md | 133 ++++++++++++++++++++++ technical/sso/sso-oauth2.md | 142 +++++++++++++++++++++++ technical/sso/sso-oidc.md | 166 +++++++++++++++++++++++++++ technical/sso/sso-saml.md | 192 ++++++++++++++++++++++++++++++++ 6 files changed, 907 insertions(+) create mode 100644 technical/sso.md create mode 100644 technical/sso/sso-cas.md create mode 100644 technical/sso/sso-comparison.md create mode 100644 technical/sso/sso-oauth2.md create mode 100644 technical/sso/sso-oidc.md create mode 100644 technical/sso/sso-saml.md diff --git a/technical/sso.md b/technical/sso.md new file mode 100644 index 0000000..d029ce3 --- /dev/null +++ b/technical/sso.md @@ -0,0 +1,95 @@ +--- +tags: [sso, auth, architecture] +create time: 2026-07-13 10:03 +--- + +# SSO 单点登录 + +## 概述 + +SSO(Single Sign-On,单点登录)是一种身份认证方案:用户只需登录一次,即可访问所有相互信任的应用系统。核心目标是**统一身份源、降低登录摩擦、集中安全管控**。 + +> [!info] 什么时候需要 SSO? +> 当你的组织内有 3 个以上的应用需要用户登录时,就该认真考虑 SSO 了。否则每多一个系统就多一套账号密码,用户体验和安全管控都是灾难。 + +## 核心原理 + +SSO 的本质是一个**认证委托**模型: + +```mermaid +sequenceDiagram + participant U as 用户 + participant App as 业务应用 + participant IdP as 身份提供商 (IdP) + + U->>App: 1. 访问受保护资源 + App->>IdP: 2. 重定向到登录页 + U->>IdP: 3. 输入凭证 + IdP-->>U: 4. 颁发 SSO Token / Session + IdP->>App: 5. 回调 + 认证凭证 + App->>IdP: 6. 验证凭证有效性 + IdP-->>App: 7. 返回用户身份信息 + App-->>U: 8. 授予访问权限 +``` + +三个核心角色: + +| 角色 | 全称 | 职责 | +|------|------|------| +| **User** | End User | 发起请求的终端用户 | +| **SP** | Service Provider(服务提供方) | 业务应用,依赖 IdP 做认证 | +| **IdP** | Identity Provider(身份提供商) | 统一认证中心,管理用户身份 | + +> [!tip] SP vs IdP +> 简单记忆:IdP 知道「你是谁」,SP 知道「你能干什么」。SP 把「你是谁」这件事委托给了 IdP。 + +## 主流协议概览 + +| 协议 | 时代 | 传输方式 | 典型场景 | +|------|------|----------|----------| +| CAS | 2000+ | 浏览器重定向 + Ticket | 企业内部门户、高校系统 | +| SAML 2.0 | 2005+ | XML + 浏览器重定向 | 企业级 SSO、SaaS 集成 | +| OAuth 2.0 | 2012+ | HTTPS API | 第三方授权、API 访问控制 | +| OIDC | 2014+ | HTTPS API (JWT) | 现代 Web / 移动端 / SPA | + +详细文档见各子笔记: + +- [[sso/sso-oauth2|OAuth 2.0 授权码模式]] +- [[sso/sso-oidc|OpenID Connect (OIDC)]] +- [[sso/sso-saml|SAML 2.0]] +- [[sso/sso-cas|CAS 协议]] +- [[sso/sso-comparison|协议对比与选型指南]] + +## 关键概念 + +### Session 与 Token 的区别 + +| 维度 | Session-Cookie | Token (JWT) | +|------|---------------|-------------| +| 存储位置 | 服务端 | 客户端 | +| 扩展性 | 需要共享 Session Store | 天然无状态 | +| 跨域 | Cookie 受同源策略限制 | Header 传递,无跨域问题 | +| 撤销 | 删除 Session 即可 | 需要黑名单或短过期时间 | + +### SSO 的两种实现思路 + +**1. 共享 Session(Cookie Domain 共享)** + +IdP 登录后写入一个顶级域的 Cookie,所有 SP 通过访问 IdP 的验证端点来确认登录状态。实现简单但受限于同主域。 + +**2. 分布式 Token(标准协议方式)** + +IdP 颁发 Token,SP 独立验证。跨域、跨平台无障碍,是当前主流方案。 + +> [!warning] 安全要点 +> - 始终使用 HTTPS,Token 一旦泄露等同于身份被盗 +> - Token 有效期不宜过长,Access Token 建议 5-30 分钟 +> - 使用 `state` 参数防 CSRF,`nonce` 参数防重放攻击 + +## 关联笔记 + +- [[sso/sso-oauth2|OAuth 2.0 授权码模式]] +- [[sso/sso-oidc|OpenID Connect (OIDC)]] +- [[sso/sso-saml|SAML 2.0]] +- [[sso/sso-cas|CAS 协议]] +- [[sso/sso-comparison|协议对比与选型指南]] diff --git a/technical/sso/sso-cas.md b/technical/sso/sso-cas.md new file mode 100644 index 0000000..5b4b0f7 --- /dev/null +++ b/technical/sso/sso-cas.md @@ -0,0 +1,179 @@ +--- +tags: [sso, cas, auth] +create time: 2026-07-13 10:03 +--- + +# CAS 协议 + +## 概述 + +CAS(Central Authentication Service)是由耶鲁大学于 2000 年开发的 SSO 协议,后由 Apereo 基金会维护。它是最早的 SSO 标准之一,设计简洁,在高校和企业内部门户中仍有广泛应用。当前版本为 CAS 3.0(CAS Protocol Specification)。 + +> [!info] CAS 的定位 +> 相比 SAML 的重量级 XML 和 OIDC 的 JWT 生态,CAS 的核心优势是**简单**。整个协议可以用几段 HTTP 请求描述清楚,实现成本极低。如果你只需要企业内部几个 Web 应用的 SSO,CAS 是最省事的选择。 + +## 核心流程 + +CAS 的核心思路是 **Ticket 机制**:用户在 CAS Server 登录后拿到一个一次性的 Service Ticket,业务应用用这个 Ticket 去 CAS Server 换取用户身份。 + +```mermaid +sequenceDiagram + participant U as 用户 (浏览器) + participant App as 业务应用 (CAS Client) + participant CAS as CAS Server + + U->>App: 1. 访问受保护页面 + App->>U: 302 → CAS Server + Note right of App: service=https://app.example.com/callback + U->>CAS: 2. 重定向到 CAS 登录页 + CAS->>U: 3. 展示登录表单 + U->>CAS: 4. 提交凭证 + CAS->>CAS: 5. 验证凭证 + CAS->>U: 302 → service URL + Ticket + Note left of CAS: ?ticket=ST-12345-xxxxx + U->>App: 6. 携带 Ticket 回到应用 + App->>CAS: 7. 后端验证 Ticket + Note right of App: GET /serviceValidate?ticket=ST-xxx&service=... + CAS-->>App: 8. 返回用户信息 (XML) + App-->>U: 9. 建立本地 Session,登录成功 +``` + +## Ticket 类型 + +CAS 协议定义了多种 Ticket,各有不同用途: + +| Ticket | 前缀 | 生命周期 | 用途 | +|--------|------|----------|------| +| **TGT** | `TGT-` | 用户会话级 | CAS Server 的登录凭证,存在 Server 端 | +| **ST** | `ST-` | 一次性,< 10s | SP 验证用户身份用,用后即毁 | +| **PT** | `PT-` | 一次性 | Proxy Ticket,代理场景用 | +| **PGT** | `PGT-` | 较长 | Proxy Granting Ticket | +| **PGTIOU** | `PGTIOU-` | 短 | PGT 和 ST 的关联标识 | + +> [!tip] 简化理解 +> 对于 90% 的场景,你只需要关心 **TGT**(用户在 CAS Server 的登录态)和 **ST**(一次性验证凭证)。其他 Ticket 是为代理认证设计的,大多数接入方用不到。 + +### Ticket 流转关系 + +```mermaid +graph TD + A[用户登录] -->|成功| B[CAS Server 颁发 TGT] + B -->|写入 Cookie| C[TGT 存在 CAS Server] + C -->|SP 请求| D[生成 ST] + D -->|一次性验证| E[SP 后端验证 ST] + E -->|返回用户信息| F[SP 建立本地 Session] +``` + +## 关键接口 + +CAS 协议定义的核心端点: + +| 端点 | 说明 | +|------|------| +| `/login` | 登录端点,支持 `service` 参数 | +| `/logout` | 登出端点,支持 `service` 参数回跳 | +| `/serviceValidate` | ST 验证端点(CAS 2.0) | +| `/p3/serviceValidate` | ST 验证端点(CAS 3.0,返回更多用户属性) | +| `/proxyValidate` | PT 验证端点 | +| `/proxy` | 获取 PGT | + +### Ticket 验证响应示例 + +**CAS 2.0:** +```xml + + + zhangsan + + +``` + +**CAS 3.0(支持属性释放):** +```xml + + + zhangsan + + zhangsan@example.com + 张三 + engineering + + + +``` + +## Go 接入示例 + +CAS 接入相对简单,核心逻辑就是拦截请求 + 验证 Ticket: + +```go +// CAS Client 核心逻辑 +func CASServerURL = "https://cas.example.com" +func ServiceURL = "https://app.example.com" + +func CASMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 1. 检查本地 Session + if session, _ := store.Get(r, "session"); session.Values["user"] != nil { + next.ServeHTTP(w, r) + return + } + + // 2. 检查 URL 中的 Ticket + ticket := r.URL.Query().Get("ticket") + if ticket == "" { + // 3. 没有 Ticket → 重定向到 CAS + loginURL := CASServerURL + "/login?service=" + url.QueryEscape(ServiceURL+r.URL.Path) + http.Redirect(w, r, loginURL, http.StatusFound) + return + } + + // 4. 后端验证 Ticket + validateURL := fmt.Sprintf( + "%s/p3/serviceValidate?ticket=%s&service=%s", + CASServerURL, ticket, url.QueryEscape(ServiceURL), + ) + resp, err := http.Get(validateURL) + if err != nil { + http.Error(w, "CAS validation failed", 500) + return + } + defer resp.Body.Close() + + // 5. 解析 XML 响应,提取用户名 + // ... 解析 logic ... + + // 6. 建立本地 Session + session, _ := store.Get(r, "session") + session.Values["user"] = parsedUser + session.Save(r, w) + + // 7. 重定向去掉 Ticket 参数 + cleanURL := strings.Split(r.URL.String(), "?")[0] + http.Redirect(w, r, cleanURL, http.StatusFound) + }) +} +``` + +## 常见陷阱与最佳实践 + +**Ticket 不能重复验证** +- ST 验证一次后即失效,不要缓存 Ticket 验证结果 +- 用户刷新页面时 Ticket 已经失效,应该走 Session 而不是重新验证 + +**service 参数必须严格匹配** +- CAS Server 会校验 service 参数是否在注册列表中 +- 不要拼接用户可控的内容到 service 参数 + +**登出的局限性** +- CAS 的 `/logout` 只能清除 TGT(CAS Server 端的登录态) +- 各 SP 的本地 Session 需要各自处理,CAS 通过回调通知 SP(Back-channel 或 Front-channel) +- 实际部署中,很多 SP 不实现登出回调,导致用户以为登出了但 SP Session 仍有效 + +**CAS vs OIDC 的选择** +- 如果你的系统只需要内网 Web 应用 SSO,CAS 足够 +- 如果需要支持移动端、SPA、第三方集成,直接上 OIDC +- 很多现代 CAS Server(如 Apereo CAS 6.x)同时支持 CAS 协议和 OIDC/SAML + +> [!tip] Apereo CAS Server 的现代化 +> Apereo CAS Server 6.x+ 已经不仅仅是 CAS 协议服务器了。它同时支持 CAS / OIDC / SAML / REST API,可以作为统一身份网关使用。如果你需要一个开源自建的 SSO 平台,值得考虑。 diff --git a/technical/sso/sso-comparison.md b/technical/sso/sso-comparison.md new file mode 100644 index 0000000..7ac0ea3 --- /dev/null +++ b/technical/sso/sso-comparison.md @@ -0,0 +1,133 @@ +--- +tags: [sso, architecture, auth] +create time: 2026-07-13 10:03 +--- + +# SSO 协议对比与选型指南 + +## 概述 + +本文横向对比 CAS、SAML 2.0、OAuth 2.0、OIDC 四种主流 SSO 协议,帮助你在实际项目中做出合理的选型决策。 + +## 横向对比总览 + +| 维度 | CAS | SAML 2.0 | OAuth 2.0 | OIDC | +|------|-----|----------|-----------|------| +| **诞生年份** | 2000 | 2005 | 2012 | 2014 | +| **数据格式** | 自定义 XML | XML | JSON | JSON (JWT) | +| **传输层** | HTTP 重定向 | HTTP 重定向 + POST | HTTPS API | HTTPS API | +| **核心目的** | 认证 (SSO) | 认证 (SSO) | 授权 | 认证 + 授权 | +| **Token 类型** | Ticket (ST/TGT) | Assertion | access_token | id_token + access_token | +| **移动端支持** | 差 | 差 | 好 | 优秀 | +| **SPA 支持** | 差 | 差 | 好 (PKCE) | 优秀 (PKCE) | +| **实现复杂度** | 低 | 高 | 中 | 中 | +| **生态丰富度** | 一般 | 丰富 (企业级) | 极丰富 | 极丰富 | +| **主要用户** | 高企内部系统 | 企业 B2B SaaS | 第三方登录 | 现代 Web/移动应用 | + +## 各协议适用场景 + +### 选择 CAS 的场景 + +- 企业内部多个传统 Web 系统需要统一登录 +- 已有 Apereo CAS Server 基础设施 +- 团队对 OAuth/JWT 不熟悉,需要快速落地 +- 不涉及移动端或第三方接入 + +### 选择 SAML 2.0 的场景 + +- 与企业客户的 IdP(AD FS、Okta)对接 +- 已有 AD/LDAP 用户体系,需要对外暴露 SSO +- B2B SaaS 产品,客户要求 SAML 集成 +- 合规要求(如 FedRAMP、SOC 2)强制使用 SAML + +### 选择 OAuth 2.0 的场景 + +- 第三方应用需要有限度地访问你的 API +- 「用微信/GitHub 登录」这类需求 +- 纯 API 服务的访问控制(客户端凭证模式) +- 服务间的 M2M 认证 + +### 选择 OIDC 的场景 + +- **新项目的首选**——现代、通用、生态好 +- SPA / 移动端 / 微服务架构 +- 需要标准化的用户信息接口 +- 需要和主流 IdP(Keycloak、Auth0、Azure AD)对接 + +## 技术栈对比 + +### 数据格式 + +| 格式 | 优点 | 缺点 | +|------|------|------| +| XML (SAML) | 严谨、标准完善 | 冗长、解析慢、安全风险多 | +| JSON (OAuth/OIDC) | 轻量、开发者友好 | 规范相对灵活,需自行约束 | +| JWT (OIDC ID Token) | 自包含、可离线验证 | 无法即时撤销、Payload 可见 | + +### 签名与加密 + +| 协议 | 签名方式 | 加密支持 | +|------|----------|----------| +| CAS | 可选 (HMAC) | 不支持 | +| SAML | XML Digital Signature(RSA/EC) | XML Encryption(RSA/AES) | +| OAuth 2.0 | 无标准(靠 HTTPS) | 无标准 | +| OIDC | JWT Signature(RS256/ES256) | JWE(可选) | + +> [!info] 为什么 OIDC 通常只签名不加密? +> JWT 的 Payload 通常是用户 ID、邮箱等非敏感信息。签名保证完整性(不被篡改),HTTPS 保证传输安全。如果需要隐藏 Payload,使用 JWE 加密。 + +## 选型决策树 + +```mermaid +graph TD + A[需要 SSO] --> B{有移动端/SPA?} + B -->|是| C[OIDC] + B -->|否| D{对接企业客户 IdP?} + D -->|是| E{客户要求?} + E -->|SAML| F[SAML 2.0] + E -->|OIDC| C + E -->|无要求| C + D -->|否| G{内部系统?} + G -->|是| H{已有基础设施?} + H -->|CAS Server| I[CAS] + H -->|Keycloak| C + H -->|无| C + G -->|否| J{第三方授权?} + J -->|是| K[OAuth 2.0] + J -->|否| C +``` + +> [!tip] 默认选 OIDC +> 如果你没有强烈的理由选择其他协议,**OIDC 是最安全的默认选择**。它覆盖了 SSO + 授权的完整场景,生态支持最好,学习资源最丰富。 + +## 接入成本评估 + +| 维度 | CAS | SAML | OAuth 2.0 | OIDC | +|------|-----|------|-----------|------| +| **Go 库成熟度** | 一般 | 一般 | 优秀 | 优秀 | +| **调试难度** | 低 | 高(XML 解析坑多) | 中 | 中 | +| **文档质量** | 一般 | 规范详尽但晦涩 | 优秀 | 优秀 | +| **测试工具** | 少 | SAML Tracer 等 | OAuth Proxy | OIDC Debugger | +| **预计接入工时** | 1-3 天 | 3-7 天 | 1-3 天 | 1-3 天 | + +## 推荐开源 IdP + +| 产品 | 支持协议 | 特点 | 适用规模 | +|------|----------|------|----------| +| **Keycloak** | OIDC, SAML | 功能全面,Red Hat 维护 | 中大型 | +| **Auth0** | OIDC, SAML | SaaS 托管,上手快 | 中小型 | +| **Casdoor** | OIDC, SAML, CAS | Go 实现,中文社区 | 中小型 | +| **Apereo CAS** | CAS, OIDC, SAML | 高校/企业经典选择 | 中大型 | +| **Authentik** | OIDC, SAML | Python,现代化 UI | 中小型 | +| **Logto** | OIDC | 开源 Auth0 替代 | 中小型 | + +> [!tip] Keycloak 是自建 SSO 的首选 +> 如果要自建 SSO 平台,Keycloak 是当前综合评分最高的开源方案。支持 OIDC + SAML,管理界面完善,社区活跃。唯一的缺点是基于 Java,内存占用较大。 + +## 关联笔记 + +- [[sso/sso-oauth2|OAuth 2.0 授权码模式]] +- [[sso/sso-oidc|OpenID Connect (OIDC)]] +- [[sso/sso-saml|SAML 2.0]] +- [[sso/sso-cas|CAS 协议]] +- [[sso|SSO 单点登录(主文档)]] diff --git a/technical/sso/sso-oauth2.md b/technical/sso/sso-oauth2.md new file mode 100644 index 0000000..3607f0f --- /dev/null +++ b/technical/sso/sso-oauth2.md @@ -0,0 +1,142 @@ +--- +tags: [sso, oauth2, auth] +create time: 2026-07-13 10:03 +--- + +# OAuth 2.0 授权码模式 + +## 概述 + +OAuth 2.0 是一个**授权框架**(RFC 6749),设计初衷是让第三方应用在不获取用户密码的前提下,有限度地访问用户的资源。授权码模式(Authorization Code Grant)是最安全、最常用的流程,也是构建 SSO 的基础。 + +> [!info] OAuth 2.0 ≠ 认证协议 +> OAuth 2.0 解决的是「**授权**」—— 让第三方拿走你的部分权限。它本身不提供用户身份信息,这也是为什么后来需要 OIDC 来补上这一环。 + +## 核心流程 + +```mermaid +sequenceDiagram + participant U as 用户 (浏览器) + participant App as Client App (SP) + participant Auth as Authorization Server (IdP) + participant Res as Resource Server + + U->>App: 1. 点击「登录」 + App->>U: 2. 302 重定向到 Auth + Note right of App: redirect_uri, scope, state, client_id + U->>Auth: 3. 登录 + 授权确认 + Auth->>U: 4. 302 回调 redirect_uri + Note left of Auth: ?code=AUTH_CODE&state=xxx + U->>App: 5. 携带 code 回到 App + App->>Auth: 6. POST /token(后端请求) + Note right of App: code, client_id, client_secret, redirect_uri + Auth-->>App: 7. 返回 access_token + App->>Res: 8. 用 access_token 请求资源 + Res-->>App: 9. 返回受保护数据 +``` + +关键点:**步骤 2-5 走浏览器重定向,步骤 6-7 走后端 HTTPS 直连**。code 只能用一次,且必须在后端交换,避免 token 暴露在前端。 + +## 四种授权模式对比 + +| 模式 | 流程 | 安全性 | 适用场景 | +|------|------|--------|----------| +| **授权码模式** | 重定向 + 后端换 Token | 最高 | Web 后端应用、SSO | +| 授权码 + PKCE | 授权码 + 代码验证码 | 高 | SPA / 移动端(推荐) | +| 客户端凭证模式 | 直接换 Token | 中 | 服务间调用(M2M) | +| ~~隐式模式~~ | 直接返回 Token | 低 | **已弃用**,仅遗留系统 | + +> [!warning] 隐式模式已被废弃 +> RFC 9700 明确不推荐使用隐式模式。Token 暴露在 URL Fragment 中,容易被中间人和浏览器历史记录泄露。SPA 应使用 **PKCE** 模式替代。 + +## 核心概念速查 + +### 四个关键角色 + +| 角色 | 职责 | 示例 | +|------|------|------| +| Resource Owner | 资源拥有者(用户) | 登录的你 | +| Client | 第三方应用 | 公司的 Web 系统 | +| Authorization Server | 授权服务器 | Keycloak、Auth0 | +| Resource Server | 资源服务器 | 你的 API 服务 | + +### 关键参数 + +| 参数 | 说明 | +|------|------| +| `client_id` | 应用注册时分配的标识 | +| `client_secret` | 应用密钥,仅后端持有 | +| `redirect_uri` | 回调地址,必须预注册 | +| `scope` | 请求的权限范围,如 `openid profile email` | +| `state` | 防 CSRF 的随机字符串,回调时原样返回 | +| `code` | 授权码,一次性,有效期通常 < 10 分钟 | + +### Access Token vs Refresh Token + +```mermaid +graph LR + A[Access Token] -->|有效期短 5-30min| B[访问 API] + C[Refresh Token] -->|有效期长 天/月| D[换取新 Access Token] + C -.->|轮换机制| C2[新 Refresh Token] +``` + +| Token | 用途 | 存储 | 有效期 | +|-------|------|------|--------| +| Access Token | 调用 API | 内存 / 安全 Cookie | 短(5-30 min) | +| Refresh Token | 刷新 Access Token | HttpOnly Cookie / 后端 | 长(天 ~ 月) | + +> [!tip] Refresh Token Rotation +> 每次用 Refresh Token 换新 Token 时,同时颁发新的 Refresh Token 并使旧的失效。这样即使某个 Refresh Token 泄露,攻击者也只能用一次。 + +## Go 实现示例 + +使用 `golang.org/x/oauth2` 实现授权码流程的核心步骤: + +```go +// 配置 OAuth2 客户端 +conf := &oauth2.Config{ + ClientID: "your-client-id", + ClientSecret: "your-client-secret", + Scopes: []string{"openid", "profile", "email"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://idp.example.com/oauth/authorize", + TokenURL: "https://idp.example.com/oauth/token", + }, + RedirectURL: "https://app.example.com/callback", +} + +// 1. 生成授权 URL(附带 state 防 CSRF) +state := generateRandomState() // 需要存入 session +url := conf.AuthCodeURL(state, oauth2.AccessTypeOffline) + +// 2. Callback 处理 — 用 code 换 Token +token, err := conf.Exchange(ctx, code) +if err != nil { + // 授权码过期或无效 + log.Fatal(err) +} + +// 3. 用 Token 请求用户信息 +client := conf.Client(ctx, token) +resp, err := client.Get("https://idp.example.com/userinfo") +``` + +> `oauth2.AccessTypeOffline` 参数会请求颁发 Refresh Token。默认只发 Access Token。 + +## 常见陷阱与最佳实践 + +**CSRF 攻击** +- 必须使用 `state` 参数。生成随机值存入 session,回调时校验一致性 +- 不用 `state` 就像进门不锁门 + +**Token 存储** +- 前端:不要存在 `localStorage`(XSS 可读取),用 HttpOnly Secure Cookie +- 后端:加密存储或存入 Session,不要写进数据库明文字段 + +**redirect_uri 必须严格匹配** +- 不支持通配符,不支持路径前缀匹配 +- 生产环境不允许使用 `http://localhost` + +**scope 最小化原则** +- 只请求你需要的权限,不要 `scope: "all"` +- 用户看到的授权页面 scope 越少,信任度越高 diff --git a/technical/sso/sso-oidc.md b/technical/sso/sso-oidc.md new file mode 100644 index 0000000..3a5f22f --- /dev/null +++ b/technical/sso/sso-oidc.md @@ -0,0 +1,166 @@ +--- +tags: [sso, oidc, jwt, auth] +create time: 2026-07-13 10:03 +--- + +# OpenID Connect (OIDC) + +## 概述 + +OpenID Connect(OIDC)是构建在 OAuth 2.0 之上的**身份认证层**。如果说 OAuth 2.0 解决「你能访问什么」,OIDC 则补充了「你是谁」。它是当前最主流的 SSO 协议,几乎所有现代身份提供商(Keycloak、Auth0、Okta、Azure AD)都支持 OIDC。 + +> [!info] 为什么需要 OIDC? +> OAuth 2.0 的授权码拿到的是 access_token,它可以调 API,但你不知道**登录的用户是谁**。应用只能再去调 `/userinfo` 接口,流程不统一。OIDC 通过引入 **ID Token** 直接告诉应用用户的身份。 + +## OIDC vs OAuth 2.0 的区别 + +| 维度 | OAuth 2.0 | OIDC | +|------|-----------|------| +| 本质 | 授权框架 | 认证 + 授权 | +| Token | access_token, refresh_token | **+ id_token (JWT)** | +| 用户信息 | 需要额外请求 /userinfo | ID Token 自带 + /userinfo 补充 | +| scope | 自定义 | 必须含 `openid`,标准 scope: `profile`, `email`, `address`, `phone` | +| 规范 | RFC 6749 | OpenID Connect Core 1.0 | + +## 核心流程 + +OIDC 的授权码流程和 OAuth 2.0 几乎一致,区别在于 scope 包含 `openid`,返回值多了 `id_token`: + +```mermaid +sequenceDiagram + participant U as 用户 + participant App as Client (RP) + participant IdP as OpenID Provider + + U->>App: 1. 点击登录 + App->>U: 2. 302 → IdP + Note right of App: scope=openid profile email + U->>IdP: 3. 认证 + 授权 + IdP->>U: 4. 302 → redirect_uri + Note left of IdP: code=xxx & state=yyy + U->>App: 5. 回调 + App->>IdP: 6. POST /token + IdP-->>App: 7. { access_token, id_token, refresh_token } + App->>App: 8. 验证 id_token 签名 + claims + App-->>U: 9. 登录成功 +``` + +## ID Token 详解 + +ID Token 是一个 **JWT(JSON Web Token)**,包含用户身份信息,由 IdP 用私钥签名。 + +### 结构 + +JWT 由三部分组成:`Header.Payload.Signature` + +**Header:** +```json +{ + "alg": "RS256", + "typ": "JWT", + "kid": "key-id-2026" +} +``` + +**Payload(Claims):** +```json +{ + "iss": "https://idp.example.com", + "sub": "user-12345", + "aud": "your-client-id", + "exp": 1752374580, + "iat": 1752374280, + "nonce": "random-nonce-value", + "name": "张三", + "email": "zhangsan@example.com", + "email_verified": true, + "picture": "https://cdn.example.com/avatar.jpg" +} +``` + +### 必须校验的 Claims + +> [!danger] 不校验 = 门户大开 +> 每一个 Claim 都有其安全意义,跳过任何一个都可能导致身份伪造。 + +| Claim | 校验规则 | 不校验的风险 | +|-------|----------|-------------| +| `iss` | 必须是你配置的 IdP 地址 | 接受恶意 IdP 签发的 Token | +| `aud` | 必须包含你的 `client_id` | Token 被别的应用盗用 | +| `exp` | 必须未过期 | 过期 Token 仍可使用 | +| `nonce` | 必须和请求时一致 | 重放攻击 | +| `signature` | 用 IdP 公钥验证 | Token 内容被篡改 | + +### 校验流程 + +```go +// 使用 OIDC 库自动校验 ID Token +provider, err := oidc.NewProvider(ctx, "https://idp.example.com") + +verifier := provider.Verifier(&oidc.Config{ + ClientID: "your-client-id", +}) + +// 解析并校验 ID Token(签名、iss、aud、exp、nonce 全自动校验) +idToken, err := verifier.Verify(ctx, rawIDToken) + +// 提取 Claims +var claims struct { + Name string `json:"name"` + Email string `json:"email"` + Picture string `json:"picture"` +} +if err := idToken.Claims(&claims); err != nil { + log.Fatal(err) +} +``` + +> 使用 `coreos/go-oidc` 库时,签名验证会自动从 IdP 的 **JWKS 端点**(`/.well-known/jwks.json`)拉取公钥并缓存。 + +## OIDC Discovery + +每个 OIDC Provider 都暴露一个**发现端点**,让客户端自动获取所有配置: + +``` +GET https://idp.example.com/.well-known/openid-configuration +``` + +返回: +```json +{ + "issuer": "https://idp.example.com", + "authorization_endpoint": "https://idp.example.com/oauth/authorize", + "token_endpoint": "https://idp.example.com/oauth/token", + "userinfo_endpoint": "https://idp.example.com/userinfo", + "jwks_uri": "https://idp.example.com/.well-known/jwks.json", + "scopes_supported": ["openid", "profile", "email"], + "response_types_supported": ["code"], + "grant_types_supported": ["authorization_code", "refresh_token"], + "id_token_signing_alg_values_supported": ["RS256", "ES256"] +} +``` + +> [!tip] Discovery 的好处 +> 客户端只需要知道 IdP 的 URL,所有端点地址和算法支持都可以自动发现。这极大降低了接入成本。 + +## 常见陷阱与最佳实践 + +**永远校验 ID Token 签名** +- 使用 IdP 公钥(通过 JWKS 获取)验证,不要只 Base64 解码就信了 +- 公钥轮换时,库应自动从 JWKS 端点刷新 + +**nonce 必须使用** +- 登录请求生成 nonce 存入 session,ID Token 校验时比对 +- 防止 Token 被截获后重放 + +**UserInfo Endpoint 的定位** +- ID Token 已包含基本身份信息,UserInfo 用于获取额外数据 +- 不要把敏感信息放在 ID Token 里(它可能被前端持有) + +**多租户场景的 `hd` 参数** +- Google OIDC 支持 `hd`(hosted domain)限制只允许特定域名登录 +- 企业 SSO 场景务必限制,防止个人账号混入 + +**Key Rollover** +- IdP 会定期轮换签名密钥,客户端需要支持多 Key(通过 `kid` 匹配) +- 推荐使用自动 JWKS 刷新的库,不要硬编码公钥 diff --git a/technical/sso/sso-saml.md b/technical/sso/sso-saml.md new file mode 100644 index 0000000..890acdc --- /dev/null +++ b/technical/sso/sso-saml.md @@ -0,0 +1,192 @@ +--- +tags: [sso, saml, xml, auth] +create time: 2026-07-13 10:03 +--- + +# SAML 2.0 + +## 概述 + +SAML(Security Assertion Markup Language)2.0 是基于 XML 的**联合身份认证标准**,由 OASIS 于 2005 年发布。它是企业级 SSO 的事实标准,广泛用于 SaaS 应用集成(如 AWS、Salesforce、Workday 等),尤其在已有 LDAP/AD 的企业环境中几乎是必选项。 + +> [!info] SAML vs OIDC 的市场定位 +> SAML 是企业时代的产物——XML 繁重但可靠,适合 B2B 场景。OIDC 是云原生时代的产物——JSON 轻量灵活,适合 B2C 和移动端。两者并存,不是替代关系。 + +## 核心概念 + +### 角色 + +| 角色 | 全称 | 说明 | +|------|------|------| +| **IdP** | Identity Provider | 身份提供商,如 AD FS、Okta、OneLogin | +| **SP** | Service Provider | 业务应用,依赖 IdP 做认证 | +| **Principal** | End User | 发起 SSO 的终端用户 | + +### 关键数据结构 + +**Assertion(断言)** — SAML 的核心数据单元,包含: + +| 字段 | 说明 | +|------|------| +| `Issuer` | 断言签发者(IdP) | +| `Subject` | 被认证的用户 | +| `Conditions` | 有效期、受众限制 | +| `AuthnStatement` | 认证方式、时间 | +| `AttributeStatement` | 用户属性(角色、邮箱等) | + +## 核心流程(SP-Initiated SSO) + +SP 发起的 SSO 是最常见的流程: + +```mermaid +sequenceDiagram + participant U as 用户 + participant SP as Service Provider + participant IdP as Identity Provider + + U->>SP: 1. 访问受保护页面 + SP->>U: 2. 生成 AuthnRequest,302 重定向 + Note right of SP: SAMLRequest (Base64 + Deflate) + U->>IdP: 3. 重定向到 IdP SSO 端点 + IdP->>U: 4. 展示登录页 + U->>IdP: 5. 输入凭证 + IdP->>IdP: 6. 验证凭证 + IdP->>U: 7. 返回 HTML Form(自动提交) + Note left of IdP: SAMLResponse (Base64 XML) + U->>SP: 8. POST 到 ACS URL + SP->>SP: 9. 验证签名 + 解析 Assertion + SP-->>U: 10. 建立 Session,跳转目标页 +``` + +> [!tip] 为什么用 POST 而不是 GET? +> SAMLResponse 是一段大的 Base64 XML,超过 URL 长度限制。所以 IdP 返回一个自动提交的 HTML Form,用 POST 送到 SP 的 ACS(Assertion Consumer Service)端点。 + +### AuthnRequest 示例 + +```xml + + https://app.example.com + + +``` + +### SAMLResponse 示例(简化) + +```xml + + https://idp.example.com + ... + + + + + https://idp.example.com + ... + + user@example.com + + + + + + + https://app.example.com + + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport + + + + + + admin + + + + +``` + +## SP 配置要点 + +接入 SAML 时,你需要向 IdP 提供以下信息: + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| **Entity ID** | SP 的唯一标识 | `https://app.example.com` | +| **ACS URL** | 接收 SAMLResponse 的端点 | `https://app.example.com/saml/acs` | +| **NameID Format** | 用户标识格式 | `emailAddress` / `persistent` | +| **证书** | SP 的公钥(用于加密 Assertion) | PEM 格式证书 | + +从 IdP 获取的配置: + +| 配置项 | 说明 | +|--------|------| +| **SSO URL** | IdP 的登录端点 | +| **SLO URL** | 单点登出端点(可选) | +| **IdP 证书** | 用于验证 SAMLResponse 签名 | + +## Go 接入方案 + +Go 生态中 SAML 库相对 OIDC 较少,推荐: + +```go +// 使用 crewjam/saml 库 +import "github.com/crewjam/saml/samlsp" + +// 创建 SAML 中间件 +samlSP, _ := samlsp.New(samlsp.Options{ + EntityID: "https://app.example.com", + URL: *rootURL, + Key: tlsCert.PrivateKey.(crypto.Signer), + Certificate: tlsCert.Leaf, + IDPMetadata: idpMetadataURL, // IdP 的 Metadata XML URL +}) + +// 注册路由 +http.Handle("/saml/", samlSP) // SAML 端点(ACS、Metadata) +http.Handle("/protected", samlSP.RequireAccount(handler)) // 受保护路由 +``` + +## 常见陷阱与最佳实践 + +**XML Signature Wrapping 攻击** +- SAML 最大的安全风险。攻击者在 XML 中插入额外的 Assertion,利用解析器的逻辑漏洞 +- 校验签名后,必须**重新提取** Subject 和 Conditions,不要信任解析位置 + +**时钟偏移** +- SAML Assertion 的有效期通常只有 5 分钟,IdP 和 SP 之间时钟不同步会导致验证失败 +- 建议允许 30 秒 - 2 分钟的时钟偏移容差 + +**Logout 的复杂性** +- SAML SLO(Single Logout)需要所有 SP 协同,实际部署中经常不工作 +- 很多企业选择**仅清除本地 Session**,不做真正的 SLO + +**Metadata 自动刷新** +- IdP 的签名证书会轮换,SP 应定期拉取 IdP Metadata XML 更新证书 +- 不要硬编码 IdP 证书 + +**NameID 的选择** + +| Format | 特点 | 适用场景 | +|--------|------|----------| +| `emailAddress` | 可读,但邮箱会变 | 外部用户 | +| `persistent` | 不可读的随机 ID | 需要隐私保护 | +| `transient` | 每次登录不同 | 一次性访问 | + +> [!warning] 不要用 NameID 做业务关联 +> NameID 是认证标识,不是业务 ID。如果用户邮箱变了,NameID 变了,你的业务系统不应该因此丢失用户数据。用一个稳定的内部 ID 做映射。