Files

11 KiB
Raw Permalink Blame History

tags, create time
tags create time
sso
saml
xml
auth
2026-07-13 10:03

SAML 2.0

概述

假设你的公司采购了一个 SaaS 项目管理工具,员工需要用公司账号登录。SaaS 厂商不可能直接连到你公司的用户数据库,那怎么办?

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)。」

断言里的核心字段:

字段 说明 类比
Issuer 谁签发的(IdP 地址) 公章上的单位名
Subject 被认证的用户 工作证上的姓名
Conditions 有效期、只允许谁用 证件有效期 + 使用范围
AuthnStatement 认证方式和时间 「通过密码认证,时间 14:03」
AttributeStatement 用户属性(角色、邮箱等) 部门、职位等附加信息

核心流程(SP 发起)

最常见的是 SP-Initiated 流程——用户先访问 SP,SP 再把用户送到 IdP 登录:

sequenceDiagram
    participant U as 用户 (浏览器)
    participant SP as 业务应用 (SP)
    participant IdP as 认证中心 (IdP)

    U->>SP: 1. 访问受保护页面
    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. 登录成功

[!info] 为什么 IdP 返回的是 HTML 表单而不是直接跳转? SAMLResponse(断言 XML)体积很大,超过浏览器 URL 长度限制(通常 2KB)。所以 IdP 返回一个隐藏的 HTML 表单,用 JavaScript 自动 POST 到 SP。用户通常感知不到这个过程。

SP 配置清单

接入 SAML 前,你需要和 IdP 管理员交换以下配置:

你需要提供给 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 信封

<samlp:Response
    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 的签名 -->

第二层:状态码

    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
        <!-- 状态码:Success 表示认证成功,还有各种错误码 -->
    </samlp:Status>

第三层:Assertion(断言)—— 核心数据

    <saml:Assertion ID="_def456" IssueInstant="2026-07-13T02:03:05Z">
        <saml:Issuer>https://idp.example.com</saml:Issuer>  <!-- 再次声明签发者 -->
        <ds:Signature>...</ds:Signature>                      <!-- 断言自身的签名 -->

第四层:Subject(用户身份)

        <saml:Subject>
            <saml:NameID Format="urn:...:emailAddress">
                user@example.com                            <!-- 用户标识 -->
            </saml:NameID>
            <saml:SubjectConfirmation>
                <saml:SubjectConfirmationData
                    NotOnOrAfter="2026-07-13T02:08:05Z"     <!-- 这份证明 5 分钟后过期 -->
                    Recipient="https://app.example.com/saml/acs"  <!-- 只能发给这个地址 -->
                    InResponseTo="_abc123"/>                  <!-- 只响应这个请求 -->
            </saml:SubjectConfirmation>
        </saml:Subject>

第四层:Conditions(使用条件)

        <saml:Conditions NotBefore="..." NotOnOrAfter="...">
            <saml:AudienceRestriction>
                <saml:Audience>https://app.example.com</saml:Audience>
                <!-- 只有这个 SP 能用这份断言 -->
            </saml:AudienceRestriction>
        </saml:Conditions>

第四层:AuthnStatement(认证方式)

        <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(用户属性)

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

Go 接入示例

Go 生态中 SAML 库较少,推荐使用 crewjam/saml:

import "github.com/crewjam/saml/samlsp"

// 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",  // SP 的唯一标识
    URL:         *rootURL,
    Key:         tlsCert.PrivateKey.(crypto.Signer),  // SP 的私钥(用于解密)
    Certificate: tlsCert.Leaf,                         // SP 的证书
    IDPMetadata: idpMetadataURL,                       // IdP 的 Metadata URL
})

// 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 最大的安全风险。攻击原理:

  1. 正常的 SAMLResponse 里有一个 Assertion,签名校验通过
  2. 攻击者在 XML 中插入另一个 Assertion(用同一个签名),利用 XML 解析器的位置逻辑漏洞,让 SP 读到被篡改的 Assertion 但签名验证却通过

防御:签名验证后,必须从签名引用的位置提取 Assertion,不要信任 XML 的默认解析顺序。

时钟偏移

SAML Assertion 的有效期通常只有 5 分钟。如果 IdP 和 SP 的服务器时钟差超过 5 分钟,验证就会失败。

建议:允许 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