vault backup: 2026-07-13 11:11:37
This commit is contained in:
+173
-115
@@ -7,186 +7,244 @@ create time: 2026-07-13 10:03
|
||||
|
||||
## 概述
|
||||
|
||||
SAML(Security Assertion Markup Language)2.0 是基于 XML 的**联合身份认证标准**,由 OASIS 于 2005 年发布。它是企业级 SSO 的事实标准,广泛用于 SaaS 应用集成(如 AWS、Salesforce、Workday 等),尤其在已有 LDAP/AD 的企业环境中几乎是必选项。
|
||||
假设你的公司采购了一个 SaaS 项目管理工具,员工需要用公司账号登录。SaaS 厂商不可能直接连到你公司的用户数据库,那怎么办?
|
||||
|
||||
> [!info] SAML vs OIDC 的市场定位
|
||||
> SAML 是企业时代的产物——XML 繁重但可靠,适合 B2B 场景。OIDC 是云原生时代的产物——JSON 轻量灵活,适合 B2C 和移动端。两者并存,不是替代关系。
|
||||
**SAML(Security Assertion Markup Language,安全断言标记语言)2.0** 就是解决这个问题的协议:你公司的认证中心(IdP)给 SaaS 系统(SP)签发一份**带签名的「身份证明」**,SaaS 系统验证签名后就信任这份证明。
|
||||
|
||||
## 核心概念
|
||||
> [!info] 什么是「联合身份认证」?
|
||||
> **联合(Federation)** 的意思是:多个独立的组织互相约定「我信任你认证过的用户」。就像你用大学学生证去图书馆——图书馆不是大学开的,但它认大学发的证件。SAML 就是这个「证件」的标准格式。
|
||||
>
|
||||
> SAML 诞生于 2005 年,当时 JSON 还不流行,企业软件的标配是 XML。所以 SAML 天然使用 XML 格式——虽然冗长,但标准成熟、工具链完善。这正是 SAML 在企业 B2B 场景中仍然是主流的原因。
|
||||
|
||||
### 角色
|
||||
## 三个核心角色
|
||||
|
||||
| 角色 | 全称 | 说明 |
|
||||
| 角色 | 全称 | 说明 | 类比 |
|
||||
|------|------|------|------|
|
||||
| **IdP** | Identity Provider | 身份提供商,负责认证用户 | 大学校园卡中心 |
|
||||
| **SP** | Service Provider | 业务应用,依赖 IdP 做认证 | 图书馆 |
|
||||
| **Principal** | End User | 终端用户 | 拿学生证借书的你 |
|
||||
|
||||
## Assertion(断言):SAML 的核心
|
||||
|
||||
SAML 的一切围绕 **Assertion(断言)** 展开。断言就是 IdP 签发的一份 XML 格式的「身份证明文件」,相当于一个**带公章的工作证**:
|
||||
|
||||
> 「我是认证中心(Issuer),我证明这个人(Subject)叫张三(NameID),他的邮箱是 zhangsan@example.com(Attributes),认证时间是 2026-07-13(AuthnStatement),这份证明有效期 5 分钟(Conditions),只对图书馆有效(AudienceRestriction)。」
|
||||
|
||||
断言里的核心字段:
|
||||
|
||||
| 字段 | 说明 | 类比 |
|
||||
|------|------|------|
|
||||
| **IdP** | Identity Provider | 身份提供商,如 AD FS、Okta、OneLogin |
|
||||
| **SP** | Service Provider | 业务应用,依赖 IdP 做认证 |
|
||||
| **Principal** | End User | 发起 SSO 的终端用户 |
|
||||
| `Issuer` | 谁签发的(IdP 地址) | 公章上的单位名 |
|
||||
| `Subject` | 被认证的用户 | 工作证上的姓名 |
|
||||
| `Conditions` | 有效期、只允许谁用 | 证件有效期 + 使用范围 |
|
||||
| `AuthnStatement` | 认证方式和时间 | 「通过密码认证,时间 14:03」 |
|
||||
| `AttributeStatement` | 用户属性(角色、邮箱等) | 部门、职位等附加信息 |
|
||||
|
||||
### 关键数据结构
|
||||
## 核心流程(SP 发起)
|
||||
|
||||
**Assertion(断言)** — SAML 的核心数据单元,包含:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `Issuer` | 断言签发者(IdP) |
|
||||
| `Subject` | 被认证的用户 |
|
||||
| `Conditions` | 有效期、受众限制 |
|
||||
| `AuthnStatement` | 认证方式、时间 |
|
||||
| `AttributeStatement` | 用户属性(角色、邮箱等) |
|
||||
|
||||
## 核心流程(SP-Initiated SSO)
|
||||
|
||||
SP 发起的 SSO 是最常见的流程:
|
||||
最常见的是 **SP-Initiated** 流程——用户先访问 SP,SP 再把用户送到 IdP 登录:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant SP as Service Provider
|
||||
participant IdP as Identity Provider
|
||||
participant U as 用户 (浏览器)
|
||||
participant SP as 业务应用 (SP)
|
||||
participant IdP as 认证中心 (IdP)
|
||||
|
||||
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,跳转目标页
|
||||
SP->>U: 2. 「你还没登录,去认证中心吧」<br/>生成 AuthnRequest,浏览器 302 跳转
|
||||
Note right of SP: AuthnRequest 被压缩 + 编码后<br/>放在 URL 参数中(SAMLRequest)
|
||||
U->>IdP: 3. 浏览器跳到 IdP 登录页
|
||||
IdP->>U: 4. 展示登录表单
|
||||
U->>IdP: 5. 输入账号密码
|
||||
IdP->>IdP: 6. 验证凭证,生成 SAMLResponse
|
||||
IdP->>U: 7. 返回一个自动提交的 HTML 表单
|
||||
Note left of IdP: 表单里藏着 SAMLResponse<br/>(Base64 编码的 XML 断言)
|
||||
U->>SP: 8. 浏览器自动提交表单到 SP
|
||||
SP->>SP: 9. 验证签名 + 解析断言 + 提取用户信息
|
||||
SP-->>U: 10. 登录成功
|
||||
```
|
||||
|
||||
> [!tip] 为什么用 POST 而不是 GET?
|
||||
> SAMLResponse 是一段大的 Base64 XML,超过 URL 长度限制。所以 IdP 返回一个自动提交的 HTML Form,用 POST 送到 SP 的 ACS(Assertion Consumer Service)端点。
|
||||
> [!info] 为什么 IdP 返回的是 HTML 表单而不是直接跳转?
|
||||
> SAMLResponse(断言 XML)体积很大,超过浏览器 URL 长度限制(通常 2KB)。所以 IdP 返回一个隐藏的 HTML 表单,用 JavaScript 自动 POST 到 SP。用户通常感知不到这个过程。
|
||||
|
||||
### AuthnRequest 示例
|
||||
## SP 配置清单
|
||||
|
||||
```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>
|
||||
```
|
||||
接入 SAML 前,你需要和 IdP 管理员交换以下配置:
|
||||
|
||||
### SAMLResponse 示例(简化)
|
||||
**你需要提供给 IdP 的:**
|
||||
|
||||
| 配置项 | 说明 | 示例 |
|
||||
|--------|------|------|
|
||||
| **Entity ID** | SP 的唯一标识,相当于 SP 的「身份证号」 | `https://app.example.com` |
|
||||
| **ACS URL** | 接收 SAMLResponse 的端点(Assertion Consumer Service) | `https://app.example.com/saml/acs` |
|
||||
| **NameID Format** | 你希望 IdP 用什么格式标识用户 | `emailAddress`(用邮箱标识) |
|
||||
| **SP 证书** | 你的公钥,IdP 用它加密发给你的断言 | PEM 格式证书 |
|
||||
|
||||
**你需要从 IdP 获取的:**
|
||||
|
||||
| 配置项 | 说明 |
|
||||
|--------|------|
|
||||
| **SSO URL** | IdP 的登录端点,SP 把用户重定向到这里 |
|
||||
| **IdP 证书** | IdP 的公钥,你用它验证 SAMLResponse 的签名 |
|
||||
| **IdP Metadata URL** | IdP 的配置文件 URL,包含以上所有信息 |
|
||||
|
||||
> [!tip] Metadata 文件是什么?
|
||||
> IdP 和 SP 各自暴露一个 **Metadata XML** 文件,描述自己的端点、证书、支持的功能。通过交换 Metadata URL 就能完成大部分配置,不需要手动复制粘贴各项参数。大多数 SAML 库支持自动拉取 Metadata。
|
||||
|
||||
## XML 结构拆解
|
||||
|
||||
SAML 的 XML 很长,但结构是有规律的。下面把 SAMLResponse 拆成一块块来讲:
|
||||
|
||||
### 第一层:Response 信封
|
||||
|
||||
```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 数字签名 -->
|
||||
Destination="https://app.example.com/saml/acs" <!-- SP 的 ACS 地址 -->
|
||||
InResponseTo="_abc123"> <!-- 对应哪个 AuthnRequest -->
|
||||
<saml:Issuer>https://idp.example.com</saml:Issuer> <!-- IdP 标识 -->
|
||||
<ds:Signature>...</ds:Signature> <!-- 整个 Response 的签名 -->
|
||||
```
|
||||
|
||||
### 第二层:状态码
|
||||
|
||||
```xml
|
||||
<samlp:Status>
|
||||
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
|
||||
<!-- 状态码:Success 表示认证成功,还有各种错误码 -->
|
||||
</samlp:Status>
|
||||
```
|
||||
|
||||
### 第三层:Assertion(断言)—— 核心数据
|
||||
|
||||
```xml
|
||||
<saml:Assertion ID="_def456" IssueInstant="2026-07-13T02:03:05Z">
|
||||
<saml:Issuer>https://idp.example.com</saml:Issuer>
|
||||
<ds:Signature>...</ds:Signature>
|
||||
<saml:Issuer>https://idp.example.com</saml:Issuer> <!-- 再次声明签发者 -->
|
||||
<ds:Signature>...</ds:Signature> <!-- 断言自身的签名 -->
|
||||
```
|
||||
|
||||
### 第四层:Subject(用户身份)
|
||||
|
||||
```xml
|
||||
<saml:Subject>
|
||||
<saml:NameID Format="...emailAddress">user@example.com</saml:NameID>
|
||||
<saml:NameID Format="urn:...: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"/>
|
||||
NotOnOrAfter="2026-07-13T02:08:05Z" <!-- 这份证明 5 分钟后过期 -->
|
||||
Recipient="https://app.example.com/saml/acs" <!-- 只能发给这个地址 -->
|
||||
InResponseTo="_abc123"/> <!-- 只响应这个请求 -->
|
||||
</saml:SubjectConfirmation>
|
||||
</saml:Subject>
|
||||
```
|
||||
|
||||
### 第四层:Conditions(使用条件)
|
||||
|
||||
```xml
|
||||
<saml:Conditions NotBefore="..." NotOnOrAfter="...">
|
||||
<saml:AudienceRestriction>
|
||||
<saml:Audience>https://app.example.com</saml:Audience>
|
||||
<!-- 只有这个 SP 能用这份断言 -->
|
||||
</saml:AudienceRestriction>
|
||||
</saml:Conditions>
|
||||
<saml:AuthnStatement AuthnInstant="...">
|
||||
```
|
||||
|
||||
### 第四层:AuthnStatement(认证方式)
|
||||
|
||||
```xml
|
||||
<saml:AuthnStatement AuthnInstant="2026-07-13T02:03:00Z">
|
||||
<saml:AuthnContext>
|
||||
<saml:AuthnContextClassRef>
|
||||
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
|
||||
<!-- 认证方式:密码认证(通过 HTTPS 传输) -->
|
||||
</saml:AuthnContextClassRef>
|
||||
</saml:AuthnContext>
|
||||
</saml:AuthnStatement>
|
||||
```
|
||||
|
||||
### 第四层:AttributeStatement(用户属性)
|
||||
|
||||
```xml
|
||||
<saml:AttributeStatement>
|
||||
<saml:Attribute Name="role">
|
||||
<saml:AttributeValue>admin</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
<saml:Attribute Name="email">
|
||||
<saml:AttributeValue>user@example.com</saml:AttributeValue>
|
||||
</saml:Attribute>
|
||||
</saml:AttributeStatement>
|
||||
</saml:Assertion>
|
||||
</samlp:Response>
|
||||
```
|
||||
|
||||
## SP 配置要点
|
||||
## Go 接入示例
|
||||
|
||||
接入 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 生态中 SAML 库较少,推荐使用 `crewjam/saml`:
|
||||
|
||||
```go
|
||||
// 使用 crewjam/saml 库
|
||||
import "github.com/crewjam/saml/samlsp"
|
||||
|
||||
// 创建 SAML 中间件
|
||||
// 1. 从 IdP 的 Metadata URL 自动获取端点和证书
|
||||
idpMetadataURL, _ := url.Parse("https://idp.example.com/metadata")
|
||||
|
||||
// 2. 创建 SAML 中间件
|
||||
rootURL, _ := url.Parse("https://app.example.com")
|
||||
samlSP, _ := samlsp.New(samlsp.Options{
|
||||
EntityID: "https://app.example.com",
|
||||
EntityID: "https://app.example.com", // SP 的唯一标识
|
||||
URL: *rootURL,
|
||||
Key: tlsCert.PrivateKey.(crypto.Signer),
|
||||
Certificate: tlsCert.Leaf,
|
||||
IDPMetadata: idpMetadataURL, // IdP 的 Metadata XML URL
|
||||
Key: tlsCert.PrivateKey.(crypto.Signer), // SP 的私钥(用于解密)
|
||||
Certificate: tlsCert.Leaf, // SP 的证书
|
||||
IDPMetadata: idpMetadataURL, // IdP 的 Metadata URL
|
||||
})
|
||||
|
||||
// 注册路由
|
||||
http.Handle("/saml/", samlSP) // SAML 端点(ACS、Metadata)
|
||||
http.Handle("/protected", samlSP.RequireAccount(handler)) // 受保护路由
|
||||
// 3. 注册路由
|
||||
http.Handle("/saml/", samlSP) // SAML 相关端点
|
||||
http.Handle("/protected", samlSP.RequireAccount(handler)) // 需要登录的页面
|
||||
```
|
||||
|
||||
> 中间件会自动处理:收到 SAMLResponse → 验证签名 → 解析 Assertion → 创建 Session。
|
||||
|
||||
## NameID:用户标识格式
|
||||
|
||||
NameID 是 IdP 用来标识用户的字段,有不同的格式:
|
||||
|
||||
| Format | 长什么样 | 适用场景 |
|
||||
|--------|----------|----------|
|
||||
| `emailAddress` | `user@example.com` | 最常用,可读,但邮箱可能变 |
|
||||
| `persistent` | `a1b2c3d4-e5f6-7890-abcd-ef1234567890` | 随机 UUID,不暴露隐私,跨登录不变 |
|
||||
| `transient` | `临时随机字符串` | 每次登录都不同,SP 无法跨次追踪用户 |
|
||||
|
||||
> [!warning] 不要用 NameID 做业务主键
|
||||
> NameID 是**认证标识**,不是业务 ID。如果用户换了邮箱,`emailAddress` 格式的 NameID 就变了。你的业务系统应该用内部生成的稳定 ID 做关联,NameID 只用于登录时匹配用户。
|
||||
|
||||
## 常见陷阱与最佳实践
|
||||
|
||||
**XML Signature Wrapping 攻击**
|
||||
- SAML 最大的安全风险。攻击者在 XML 中插入额外的 Assertion,利用解析器的逻辑漏洞
|
||||
- 校验签名后,必须**重新提取** Subject 和 Conditions,不要信任解析位置
|
||||
### XML Signature Wrapping 攻击
|
||||
|
||||
**时钟偏移**
|
||||
- SAML Assertion 的有效期通常只有 5 分钟,IdP 和 SP 之间时钟不同步会导致验证失败
|
||||
- 建议允许 30 秒 - 2 分钟的时钟偏移容差
|
||||
这是 SAML 最大的安全风险。攻击原理:
|
||||
|
||||
**Logout 的复杂性**
|
||||
- SAML SLO(Single Logout)需要所有 SP 协同,实际部署中经常不工作
|
||||
- 很多企业选择**仅清除本地 Session**,不做真正的 SLO
|
||||
1. 正常的 SAMLResponse 里有一个 Assertion,签名校验通过
|
||||
2. 攻击者在 XML 中**插入另一个 Assertion**(用同一个签名),利用 XML 解析器的位置逻辑漏洞,让 SP 读到被篡改的 Assertion 但签名验证却通过
|
||||
|
||||
**Metadata 自动刷新**
|
||||
- IdP 的签名证书会轮换,SP 应定期拉取 IdP Metadata XML 更新证书
|
||||
- 不要硬编码 IdP 证书
|
||||
**防御**:签名验证后,必须从签名**引用的位置**提取 Assertion,不要信任 XML 的默认解析顺序。
|
||||
|
||||
**NameID 的选择**
|
||||
### 时钟偏移
|
||||
|
||||
| Format | 特点 | 适用场景 |
|
||||
|--------|------|----------|
|
||||
| `emailAddress` | 可读,但邮箱会变 | 外部用户 |
|
||||
| `persistent` | 不可读的随机 ID | 需要隐私保护 |
|
||||
| `transient` | 每次登录不同 | 一次性访问 |
|
||||
SAML Assertion 的有效期通常只有 5 分钟。如果 IdP 和 SP 的服务器时钟差超过 5 分钟,验证就会失败。
|
||||
|
||||
> [!warning] 不要用 NameID 做业务关联
|
||||
> NameID 是认证标识,不是业务 ID。如果用户邮箱变了,NameID 变了,你的业务系统不应该因此丢失用户数据。用一个稳定的内部 ID 做映射。
|
||||
**建议**:允许 30 秒 ~ 2 分钟的时钟偏移容差。
|
||||
|
||||
### Logout 很难做好
|
||||
|
||||
SAML 的 SLO(Single Logout)需要所有 SP 协同——用户在 A 系统登出时,要通知 B、C、D 系统也登出。实际部署中,很多 SP 不实现 SLO 回调。
|
||||
|
||||
**务实的做法**:很多企业只清除本地 Session,不做真正的 SLO。或者用 OIDC 的 Session Management 替代。
|
||||
|
||||
### 证书轮换
|
||||
|
||||
IdP 的签名证书会定期更换。SP 应**定期拉取 IdP Metadata** 更新证书,不要硬编码。
|
||||
|
||||
> [!tip] 调试工具
|
||||
> - **SAML Tracer**(Firefox 插件):捕获浏览器中的 SAML 请求和响应
|
||||
> - **samltool.com**:在线解码和验证 SAML XML
|
||||
> - 开启 SP 的 SAML 调试日志,打印收到的 SAMLResponse XML
|
||||
|
||||
Reference in New Issue
Block a user