193 lines
6.8 KiB
Markdown
193 lines
6.8 KiB
Markdown
|
|
---
|
|||
|
|
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 做映射。
|