SAML2.0详解,助力企业级SSO

前言

在国内,提到认证授权、单点登录(SSO),第一时间想到的大多是 OAuth 2.0。OAuth 2.0 是一个开放标准,在国内的技术社区中得到了广泛的支持和推广,许多互联网公司以及开发者积极采用。还有一点就是国内的许多场景更关注于授权而非认证,OAuth 2.0 专注于授权机制,允许用户授权第三方应用程序访问其存储在另一个服务提供者上的资源,而无需共享用户名和密码,这种机制非常适合于社交媒体、移动应用、开放平台等场景。

今天我想说的是一种不一样的声音,今日的主角并不是 OAuth 2.0,而是 SAML 2.0

SAML

SAML(Security Assertion Markup Language)安全断言标记语言,是一个基于XML的开源标准数据格式。它用于在不同的安全域之间交换身份验证和授权数据,尤其是在身份提供者和服务提供者之间。

SAML 的核心是一组协议,这些协议可以用来传输安全声明,这些声明是关于用户身份验证、授权和属性(例如姓名、电子邮件地址和电话号码)的信息。这些声明以 XML 格式编码,并可以在网络上从一个实体传输到另一个实体。因此,SAML允许跨不同系统和平台实现单点登录(SSO)和联合身份认证。

简单了解下,SAML 也是经历了几个版本的变化。

  • SAML 1.0:这是SAML的最初版本,于 2002 年发布。它定义了基本的 SAML 断言格式和处理流程,但相对于后续版本来说功能较为有限。
  • SAML 1.1:SAML 1.1 版本在 SAML 1.0 的基础上进行了一些改进和扩展。它增加了对更多场景和用例的支持,并提供了更好的互操作性和安全性。然而,SAML 1.0 和 1.1 版本之间并不完全兼容。
  • SAML 2.0 :SAML 2.0 是 SAML 协议的最重要和广泛使用的版本。它引入了许多新特性和改进,包括更灵活的断言格式、增强的安全性机制、更好的互操作性以及对更多身份联合场景的支持。SAML 2.0 是目前被广泛应用和支持的标准,并且与之前的版本不兼容。本文要说的就是 SAML 2.0。

SAML 2.0 工作流程

先来粗略的看一下 SAML 2.0 的工作流程,里边涉及到的一些概念后边会加以说明。SAML 2.0 会涉及到非常多的概念我们慢慢往下看。

  1. 用户访问服务提供者:用户尝试访问服务提供者(SP)提供的某个受保护资源,但尚未经过身份验证。
  2. 重定向到身份提供者:由于用户尚未认证,服务提供者会生成一个 SAML 认证请求(AuthnRequest),并将用户重定向到身份提供者(IdP)。这个重定向可以通过多种绑定协议完成,如 HTTP Redirect 或 POST 请求。
  3. 用户身份验证:在 IDP 的页面,用户输入其凭据(如用户名和密码)进行验证。如果凭据成功,身份提供者会生成一个包含用户身份信息的 SAML 响应(Response)。
  4. 发送 SAML 响应:身份提供者将 SAML 响应发送回服务提供者,同样可以通过多种绑定协议完成。响应中包含了用户的身份信息、任何额外的属性(如角色、组成员身份等)以及认证时间和有效期等安全声明。
  5. 服务提供者验证并接受响应:服务提供者收到 SAML 响应后,会进行验证,以确保响应的真实性和完整性。如果验证通过,验证通过后,SP 根据断言中的信息确定用户身份,并允许用户访问受保护的资源。
  6. 用户访问受保护资源:用户已经成功认证,并可以访问服务提供者提供的受保护资源。

通过流程可以看到主要涉及两个关键实体,SP 和 IDP,我们先来分析下这两个实体。

SP 和 IDP

身份提供者(Identity Provider) : IdP 是负责验证用户身份的实体,通常是一个集中式的身份管理系统。当用户试图访问依赖于 SAML 的资源时,会首先与IdP进行交互来证明自己的身份。IdP验证用户凭证并通过SAML断言的形式向服务提供者传达用户身份和授权信息。

服务提供者(Service Provider) :SP 是实际提供在线服务的应用程序或系统,它依赖于 IdP 来进行用户的身份验证。SP 接收来自 IdP 的 SAML 断言,并基于这些断言决定是否允许用户访问其服务。

简单理解 SP 是用户要访问的系统,IDP 就是验证凭据的系统。SP 和 IDP 交互是通过 SAML 请求(SAMLRequest)和 SAML 响应 (SAMLResponse)来完成的。

SAMLRequest

SAMLRequest 就是 SP 向 IDP 发起的请求。

来看一个 SP 请求 IDP 对用户进行身份验证的 XML 内容。

xml 复制代码
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="SP_809707a0130a5d00120c9d9df97Z627afe9ddc24" Version="2.0" IssueInstant="2024-02-08T23:52:45Z" Destination="http://idp.saml.com/sso" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://sp.saml.com/test?acs">
  <saml:Issuer>http://sp.saml.com/test/</saml:Issuer>
  <samlp:NameIDPolicy Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate="true"/>
  <samlp:RequestedAuthnContext Comparison="exact">
    <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef>
  </samlp:RequestedAuthnContext>
</samlp:AuthnRequest>

感觉有点小复杂,怎么这么多元素。我们来分析下,看看这些元素的含义。

  • ID: 请求的唯一标识符,主要用于后续消息处理的匹配和关联。
  • Version: 表明请求遵循的SAML版本,上文提到过的 SAML 版本号,这里是 2.0。
  • IssueInstant: 请求发出的时间。
  • Destination: 身份提供者的 URL,表明请求应该被送达的目标地址。
  • Issuer: 提供此请求的 SP 的唯一标识符,通常是一个 URL,以便身份提供者识别请求来源。
  • NameIDPolicy: 可选参数,指示身份提供者应该如何在断言中构造用户的名称ID。
  • ForceAuthn: 可选参数,指示是否强制用户重新进行身份验证,即使他们已经处于已验证的会话中。
  • ProtocolBinding: 表明身份验证响应应当采用哪种绑定方式进行传输。
  • AssertionConsumerServiceURL: 服务提供者希望接收身份验证响应的位置(Assertion Consumer Service URL)。
  • RequestedAuthnContext: 可选参数,指定了预期的身份验证上下文,例如要求特定级别的身份验证强度。

AuthnRequest 也可以通过 Signature 标签来发送签名消息。

SAMLResponse

SAMLResponse 就是 IDP 向 SP 发起的请求。

来看一个 IDP 向 SP 的 AssertionConsumerServiceURL 发送的 XML 内容。

xml 复制代码
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="IDP_8e8dc5f69a98cc4c1ff3427e5ce34606fd672f91e6" Version="2.0" IssueInstant="2023-07-17T01:01:48Z" Destination="http://sp.saml.com/test?acs" InResponseTo="SP_809707a0130a5d00120c9d9df97Z627afe9ddc24">
  <saml:Issuer>http://idp.saml.com</saml:Issuer>
  <samlp:Status>
    <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </samlp:Status>
  <saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2023-07-17T01:01:48Z">
    <saml:Issuer>http://idp.saml.com</saml:Issuer>
    <saml:Subject>
      <saml:NameID SPNameQualifier="http://sp.saml.com/test" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
      <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.saml.com/test/acs" InResponseTo="SP_809707a0130a5d00120c9d9df97Z627afe9ddc24"/>
      </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z">
      <saml:AudienceRestriction>
        <saml:Audience>http://sp.saml.com/test</saml:Audience>
      </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93">
      <saml:AuthnContext>
        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
      </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
      <saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">test@saml.com</saml:AttributeValue>
      </saml:Attribute>
      <saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
        <saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue>
        <saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue>
      </saml:Attribute>
    </saml:AttributeStatement>
  </saml:Assertion>
</samlp:Response>
  • ID:响应的唯一标识符。
  • InResponseTo:引用之前发出的请求的ID。
  • Version:表明请求遵循的SAML版本,上文提到过的 SAML 版本号,这里是 2.0。
  • IssueInstant:响应创建的时间戳。
  • Destination:预期接收此响应的服务提供者的URL。
  • Status:包含响应状态码,指示操作是否成功。
  • Issuer:标识发件人(身份提供者)的URI。
  • Assertion:包含实际的身份验证声明和用户属性声明。
  • Subject:关于被认证用户的详细信息,包括NameID。
  • Conditions:声明的有效条件,比如有效期和受众限制。
  • AuthnStatement:记录身份验证事件,如何时何地进行了身份验证。
  • AttributeStatement:提供关于用户的属性信息,如姓名、姓氏等。

Binding

SAML 的 Binding 是指在SAML生态系统中传输和接收SAML消息的协议规范和格式。介绍几种 SAML 2.0 中常见的 Binding。

  • HTTP Redirect Binding:将 SAML 消息编码为 URL 参数,并通过 HTTP 重定向发送。这种方式简单易用,但受限于 URL 长度,适用于传输较小的 SAML 消息,如 AuthnRequest 和 LogoutRequest。
  • HTTP POST Binding:将 SAML 消息作为 HTML 表单的隐藏字段,通过 HTTP POST 方法发送。这种方式可以传输更大的 SAML 消息,更安全,因为它不会将消息暴露在 URL 中。
  • HTTP Artifact Binding:将 SAML 消息的一个引用(称为 Artifact)通过 HTTP 传输。这种方式允许将实际的 SAML 消息存储在更高效的后端存储中,并减少传输的数据量。但是,它需要一个额外的步骤来检索实际的 SAML 消息。
  • SOAP Binding:将 SAML 消息嵌入到 SOAP 消息中,通过 SOAP 协议传输。这种方式适用于需要在客户端和服务器之间进行复杂交互的场景,但增加了实现的复杂性。

实际使用时要根据实际场景选择合适的 Binding 类型。

LogoutRequest

LogoutRequest 是 SP 向 IdP 发送的请求,用于通知 IdP 用户已经登出 SP 系统,请求 IdP 终止用户的会话并撤销其授权。

终止会话就要用到 LogoutRequest 了,XML 示例如下。

xml 复制代码
<saml2p:LogoutRequest  
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol"  
    ID="SP_b37a441e-5720-4d3d-be3d-795292604840"  
    Version="2.0"  
    IssueInstant="2023-04-01T12:00:00Z"  
    Destination="https://idp.saml.com/SingleSignOutService">  

    <saml:Issuer>https://sp.saml.com</saml:Issuer>
    
    <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">john.doe</saml2:NameID>  
    
    <saml2p:SessionIndex>SP_8c5ea886-d2e8-45e6-8554-887372e0d626</saml2p:SessionIndex>  
</saml2p:LogoutRequest>

元素上文基本都说过,重点说一下 SessionIndex 。SessionIndex(可选)是一个索引值,用于标识在 IdP 侧要终止的特定会话。这对于处理同一用户在 IdP 上有多个会话的情况时是很有用的

LogoutResponse

LogoutResponse 是 SAML 协议中与 LogoutRequest 对应的响应消息,它由 IdP 向 SP 发送,以通知SP注销请求已经被处理。

来看下 LogoutResponse 的 XML 示例。

xml 复制代码
<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
                      xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
                      ID="IDP_b123aaa1e-5720-4d1d-be3d-795292602340"
                      InResponseTo="SP_b37a441e-5720-4d3d-be3d-795292604840"
                      Version="2.0"
                      IssueInstant="2024-02-18T12:34:56Z">
    <saml:Issuer>https://idp.saml.com/SAML2</saml:Issuer>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
</samlp:LogoutResponse>

Status 表示注销操作的状态信息。

当然这些 SAML 请求和响应,为了增强安全性,通常也会被签名,以确保响应的完整性和来源的可靠性。

MetaData

元数据(Metadata)是 SAML 协议中非常关键的一部分,它包含了关于 SAML 通信各方(如身份提供者IDP和服务提供者SP)的配置信息。这些配置信息对于 SAML 实体的互操作和通信至关重要。

SP 和 IDP 通常通过交换元数据的方式来互相接入。

XML 复制代码
<!-- IDP 元数据示例 -->
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
                     validUntil="2024-02-01T12:34:56Z"
                     entityID="https://idp.saml.com/SAML2">
    <md:IDPSSODescriptor WantAuthnRequestsSigned="true"
                          protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
        <md:KeyDescriptor use="signing">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>...</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <md:KeyDescriptor use="encryption">
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:X509Data>
                    <ds:X509Certificate>...</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
        </md:KeyDescriptor>
        <!-- 指定支持的 NameID 格式 -->
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</md:NameIDFormat>
        <!-- 单点登录服务配置 -->
        <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
                                Location="https://idp.saml.com/SAML2/SSO/Redirect"/>
        <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                                Location="https://idp.saml.com/SAML2/SSO/POST"/>
        <!-- 单点注销服务配置 -->
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
                                Location="https://idp.saml.com/SAML2/SingleLogout/Redirect"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
                                Location="https://idp.saml.com/SAML2/SingleLogout/POST"/>
    </md:IDPSSODescriptor>
</md:EntityDescriptor>

SP Metadata也是类似,这里就不贴了,就是将 <md:IDPSSODescriptor> 标签换成 <md:SPSSODescriptor>,里边内容差不多。

总结

来张图方便理解。

SAML 提供了一种强大且灵活的方法,用于在网络应用程序和服务之间安全地交换身份信息,从而简化了身份验证和授权的过程,提高了用户体验,并增强了安全性。理解 SAML 的各种元素是灵活使用 SAML 的关键。

相关推荐
Rhi63712 分钟前
从零搭建项目:React 19 + Vite 8 + Tailwind CSS v4 实战配置
前端
Vane117 分钟前
从零开发一个AI插件,经历了什么?
人工智能·后端
竹林81819 分钟前
用Viem替代ethers.js:从一次签名失败到完整迁移的实战记录
前端·javascript
之歆24 分钟前
DAY08_CSS浮动与行内块布局实战指南(上)
前端·css
DogDaoDao28 分钟前
【GitHub】andrej-karpathy-skills:让 AI 编程助手告别三大通病
人工智能·深度学习·程序员·大模型·github·ai编程·andrej-karpathy
9523639 分钟前
SpringBoot统一功能处理
java·spring boot·后端
light blue bird1 小时前
主子端台二分法任务汇总组件
前端·数据库·.net·桌面端winform
rleS IONS1 小时前
SpringBoot中自定义Starter
java·spring boot·后端
DevilSeagull1 小时前
MySQL(2) 客户端工具和建库
开发语言·数据库·后端·mysql·服务
jeffwang2 小时前
我做了个让 AI 看屏幕跑测试的工具,因为 Playwright 测不了我的 Flutter Web
前端