vault backup: 2026-07-13 10:08:46

This commit is contained in:
2026-07-13 10:08:46 +08:00
parent 6ff67e3fa3
commit 9865fbf3b2
6 changed files with 907 additions and 0 deletions
+95
View File
@@ -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|协议对比与选型指南]]
+179
View File
@@ -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
<cas:serviceResponse>
<cas:authenticationSuccess>
<cas:user>zhangsan</cas:user>
</cas:authenticationSuccess>
</cas:serviceResponse>
```
**CAS 3.0(支持属性释放):**
```xml
<cas:serviceResponse>
<cas:authenticationSuccess>
<cas:user>zhangsan</cas:user>
<cas:attributes>
<cas:email>zhangsan@example.com</cas:email>
<cas:displayName>张三</cas:displayName>
<cas:department>engineering</cas:department>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>
```
## 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 平台,值得考虑。
+133
View File
@@ -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 单点登录(主文档)]]
+142
View File
@@ -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 越少,信任度越高
+166
View File
@@ -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 刷新的库,不要硬编码公钥
+192
View File
@@ -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
<samlp:AuthnRequest
ID="_abc123"
Version="2.0"
IssueInstant="2026-07-13T02:03:00Z"
Destination="https://idp.example.com/sso/saml"
AssertionConsumerServiceURL="https://app.example.com/saml/acs">
<saml:Issuer>https://app.example.com</saml:Issuer>
<samlp:NameIDPolicy
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress"
AllowCreate="true"/>
</samlp:AuthnRequest>
```
### SAMLResponse 示例(简化)
```xml
<samlp:Response
Destination="https://app.example.com/saml/acs"
InResponseTo="_abc123">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature>...</ds:Signature> <!-- XML 数字签名 -->
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="_def456" IssueInstant="2026-07-13T02:03:05Z">
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature>...</ds:Signature>
<saml:Subject>
<saml:NameID Format="...emailAddress">user@example.com</saml:NameID>
<saml:SubjectConfirmation>
<saml:SubjectConfirmationData
NotOnOrAfter="2026-07-13T02:08:05Z"
Recipient="https://app.example.com/saml/acs"
InResponseTo="_abc123"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions NotBefore="..." NotOnOrAfter="...">
<saml:AudienceRestriction>
<saml:Audience>https://app.example.com</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement AuthnInstant="...">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
```
## 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 做映射。