Files
Qiniu/technical/sso/sso-saml.md
T

6.8 KiB
Raw Blame History

tags, create time
tags create time
sso
saml
xml
auth
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 是最常见的流程:

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 示例

<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 示例(简化)

<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 较少,推荐:

// 使用 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 做映射。