本篇文章来源于IBM:http://www.ibm.com/developerworks/cn/websphere/library/techarticles/1111_luol_sso/1111_luol_sso.html
SAML(Security Assertion Markup Language) 安全断言标记语言是由标识化组织 OASIS 提出的用于安全互操作的标准。SAML 是一个 XML 框架,由一组协议组成,用来传输安全声明。SAML 获得了广泛的行业认可,并被诸多主流厂商所支持。SAML 的初始版本 1.0 最初于 2002 年发布,发展数年后,于 2005 年推出 SAML 2.0。实际上,SAML 2.0 是由三个原有的认证联邦标准:SAML1.1,ID-FF (Identity Federation Framework) 1.2 和 Shibboleth 构成。
SAML 2.0 是在以下几个技术标准的基础上构建的:
SAML 2.0 的核心内容被涵盖在官方文档 SAMLConform, SAMLCore, SAMLBind 和 SAMLProf 中。SAML 2.0 规范说明书主要包含以下四方面内容:
在介绍 SAML 四大内容之前,简单介绍下在 SAML 协议标准中出现的两个角色,一个是 Identity Provider(IdP),通常 IdP 负责创建、维护和管理用户认证。一个是 Service Provider(SP),通常 SP 控制用户是否能够使用该 SP 提供的服务和资源。
SAML 断言定义了一系列 XML 编码格式安全断言的语法和语义规范,通常断言由 SAML IdP 端生成并发送到 SAML SP 端,由 SP 端来分析和处理断言。断言内容中可能包含三类声明(statements),声明是 SP 端用来分析并判断用户能否接入服务或资源的依据:
SAML Protocols 描述了 SAML 元素(包括断言)如何被打包到 SAML 请求和响应元素中,并规定 SAML 实体(IdP、SP 等)处理这些元素时必须遵守的处理规则。在大多数情况下,SAML Protocols 就是一个简单的请求 - 响应协议。
SAML Bindings( 绑定 ) 是 SAML Protocols 信息到一个标准信息格式或者通信协议的映射过程。例如 SAML SOAP 绑定就定义了一个 SAML 消息如何被封装到 SOAP envelope 中。
SAML Profile( 使用框架 ) 描述了如何使用 SAML 协议信息和断言来处理特定的业务用户实例。SAML 2.0 较之 SAML1.1 提供了更多使用框架,不过目前最常被使用的依然是 Web Browser SSO。本文下一章将重点介绍 Web Browser SSO Profile。
回页首
Web Browser SSO 定义了如何使用 SAML 消息和绑定来支持 web SSO。该使用框架提供了多种选项,不过首先需要做的两个决定是:一,该消息流是由 IdP 发起还是由 SP 发起。二,在 Idp 和 SP 之间用哪类 SAML Binding 来传递消息。SAML 支持两类消息流来实现 web SSO 信息交互,最常用的 web SSO 信息交互方式是由 SP 发起,用户选择一个浏览器标签或者点击一个链接,然后用户将被转到他们需要连入的 SP 应用。但是,这个时候用户在 SP 端并没有获得任何认证许可,因此 SP 将用户转向 IdP 来获得认证,Idp 构建一个 SAML Assertion 断言来代表用户在 IdP 侧的认证,然后将带有断言的用户实体转向到 SP 端。由 SP 来处理断言并决定是否授予该用户接入资源的权限。另一种 web SSO 消息交互是由 IdP 发起的,这需要用户先访问 IdP 然后点击一个链接到 SP。同样 IdP 需要构建一个断言发送到 SP 端,由 SP 端决定用户的授权。这个方法在某些场景非常实用,但它需要 Idp 配置一个内部转换链接到 SP 站点。本文根据生产环境实际应用场景,将主要介绍由 IdP 发起的 web SSO Profile。
SAML 2.0 HTTP POST binding 定义了由 HTML 表单控制( Form Control)来传送 SAML 协议消息的机制。这个绑定可能和 HTTP 重定向绑定(Redirect Binding)结合使用。HTTP Post Binding 是适用于当 SAML 请求者和响应者需要通过 HTTP 用户代理来通信时。该绑定具体需要传送的数据可以通过 SAML 2.0 的文档描述文件中找到。需要注意的是该绑定要求传送的 HTTP 表单数据使用 base64 编码。在下一节将具体介绍 Web Browser SSO Profile 定义的如何使用 HTTP Post Binding 来传送 SAML 消息。
上文中有提到 Web Browser SSO 可以由 IdP 端发起,也可以由 SP 端发起。本文将介绍由 IdP 端发起的 Web Browser SSO。
在由 IdP 发起的 SSO 用户场景中,IdP 端配置了一个专门的链接来指向请求的 SP。这些链接实际上指向的是本地 IdP 的 SSO 服务,并传递参数到该 SSO 服务来鉴定远程 SP。在该场景中,用户并不是直接访问 SP 端服务。而是连到 IdP 站点点击其中的链接来获得远程 SP 服务的接入权限。这个链接触发了 SAML 断言的生成,在本案例中,将使用 HTTP POST Binding 来传送信息到服务端:
<form method="post" action="https://sp.example.com/SAML2/SSO/POST" ...> <input type="hidden" name="SAMLResponse" value="response" /> <input type="hidden" name="RelayState" value="token" />... <input type="submit" value="Submit" /></form>
其中 SAMLResponse 参数的值是基于 Base64 编码。
POST /SAML2/SSO/POST HTTP/1.1 Host: sp.example.com Content-Type: application/x-www-form-urlencoded Content-Length: nnn SAMLResponse=response&RelayState=token
SP 端的 ACS 从 HTML FORM 中获得 <Response> 消息来处理,ACS 必须首先验证在 SAML 断言中的数字签名,然后再对断言的内容进行处理以便在 SP 端给用户创建一个本地的登录安全上下文。一旦这个过程完成,SP 将取到 RelayState(如果有的话)来决定用户期望访问的应用资源 URL,并发送一个 HTTP 重定向响应给浏览器,将用户定向到所请求的资源。
回页首
在上一章节,介绍了 SAML 2.0 中为实现 web SSO 所定义的 Web SSO Browser Profile。我们知道如果需要实现客户端到服务端的 Web SSO,首先需要在 IdP 端实现 SSO 服务。而在 SAML 2.0 标准中,由于引入了联邦认证的概念,在一个庞大的 IT 系统环境中,IdP 有可能作为一个独立的第三方服务出现。但在本文中使用的 IdP,是由客户一端自有 IT 认证系统提供的服务,即在服务提供商与客户之间没有引入第三方的 IdP,因此略去了 SP 与客户需要在 IdP 注册服务的过程。在下文中客户端 SSO 服务将指代 IdP 的认证服务。
这里简单介绍下文中使用的 Web SSO 用户场景:终端用户点击企业内部网站某个链接或某个菜单选项时,客户端 Web SSO 服务将被调用并发送包含一个 SAML Assertion 的 HTTP FORM 请求给服务端,该 SAML 断言携带了断言创建时间、用户 ID 等信息,当服务端从表单中提取断言信息后分析认为该断言在有效期内,那么取得用户 ID 信息后,在服务端用户管理服务(例如 LDAP、DB)中查找该用户 ID,如果找到匹配的用户 ID,那么认为该服务请求有效,并将用户重定向到请求的资源。该过程并不需要用户再次输入用户名密码等信息。但是在客户端需要产生一个携带 SAML 断言的表单请求。下面的内容就将介绍如何实现客户端的请求以及服务端的 Assertion Consumer Service。
回页首
在本小节,将详细介绍如何使用 OpenSAML 库来实现客户端 SSO 服务。当客户与 SP 协商好使用 HTTP POST Binding 来传递服务,并由客户一端(IdP)来发起该 web SSO 之后,需要实现 web SSO 的客户端部分,可以理解为当终端用户在点击企业内部网站某个链接或某个菜单选项时,能够发送包含 SAML 断言在内的 <SAMLResponse> 表单给服务端。包含 HTML FORM 表单的 html 文件的编写是一个相对简单的过程,读者可以参考网路上的资源。
<form method="post" action="https://sp.example.com/SAML2/SSO/POST" ...> <input type="hidden" name="SAMLResponse" value="response" /> <input type="hidden" name="RelayState" value="token" /> ... <input type="submit" value="Submit" /> </form>
其中控件名为 SAMLResponse 的值如何生成是最关键的部分,是下面将要具体介绍的。上述表单代码中名为 SAMLResponse 表单元素的值为“response”,未来我们将使用经过 base64 编码后的 <Response> 消息的字符串来代替。注意,由于 SAML 2.0 标准的规定,携带 SAML 2.0 断言的 <Response> 必须命名为”SAMLResponse”。尽管如此,并不意味着它是一个响应消息,在 SP 端看来,这是一个 SAML 2.0 SSO 的 HTTP 请求中携带一个字段信息。
根据 SAML 2.0 标准的要求,当一个 SAML Response 包含 0 个或多个断言时,需要使用 <Response> 消息元素。这里给出 <Response> 的一个实例,我们将基于这个实例来讨论它的实现过程。
<samlp:Response ID="_0dac9fb0c5fedaae24e26d2eb4ffe8a4" IssueInstant="2011-08-23T08:28:09.109Z" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0: assertion">http://mycom.com/issuer</saml:Issuer> <samlp:Status> <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> </samlp:Status> <saml:Assertion ID="_9fa97d8e3552f2d4ae1fc001c887c614" IssueInstant="2011-08-23T08:28:09.078Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> <saml:Issuer>http://mycom.com/issuer</saml:Issuer> <saml:Subject> <saml:NameID Format="urn:oasis:names:tc:SAML:2.0: nameid-format:emailAddress">****@cn.ibm.com</saml:NameID> <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> <saml:SubjectConfirmationData NotOnOrAfter="2011-08-23T08:58:06.578Z" Recipient="http://ip:port/SSO/serviceA" /> </saml:SubjectConfirmation> </saml:Subject> <saml:Conditions> <saml:AudienceRestriction> <saml:Audience> https://saml.example.com </saml:Audience> </saml:AudienceRestriction> </saml:Conditions> </saml:Assertion> </samlp:Response>
通常,需要对 SAML 断言进行数字签名处理,这里为了使 Response 内容简洁清晰,仅给出数字签名前的 <Response> 消息,在后面的代码实现中将提供数字签名。在上面给出 Response 消息实例中,包含一个 Assertion,其中包含了用户 ID 信息:<saml:NameID Format="urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress">****@cn.ibm.com</saml:NameID>,并标识该 NameID 格式为邮件地址。<NameID> 元素是封装在 < Subject > 中的一个子元素。根据 SAML 2.0 标准规定,进行认证请求的断言中,<Assertion> 元素必须包含一个具有 <Subject> 元素的 <AuthnStatement>, 并且该 <Subject> 元素必须包含一个 <SubjectConfirmation>。而对不同的目的的 Assertion,必须携带的信息字段不同,具体请参考 SAML 2.0 标准规定。下面是使用 OpenSAML 库实现的 Subject 字段和 Assertion 字段的构建代码:
public Subject createSubject (String username, String format, String confirmationMethod) { NameID nameID = create (NameID.class, NameID.DEFAULT_ELEMENT_NAME); nameID.setValue (username); if (format != null) nameID.setFormat (format); Subject subject = create (Subject.class, Subject.DEFAULT_ELEMENT_NAME); subject.setNameID (nameID); if (confirmationMethod != null) { SubjectConfirmation confirmation = create (SubjectConfirmation.class, SubjectConfirmation.DEFAULT_ELEMENT_NAME); confirmation.setMethod (CM_PREFIX + confirmationMethod); DateTime now=new DateTime(); DateTime afterTime=now.plusMinutes(30); SubjectConfirmationData subConData=create (SubjectConfirmationData.class,SubjectConfirmationData.DEFAULT_ELEMENT_NAME); subConData.setNotOnOrAfter(afterTime); subConData.setRecipient(recipient); confirmation.setSubjectConfirmationData(subConData); subject.getSubjectConfirmations ().add (confirmation); } return subject; }
完成 <Subject> 和 <Assertion> 的构建之后,需要对 <Assertion> 进行数字签名之后再构建 <Response> 信息。由于数字签名所涉及的内容不在本文讨论范围之内,因此不在此赘述。读者可以参考本文所提供的代码实现数字签名部分。在完成 <Assertion> 的数字签名之后,才能完成最终的 <Response> 信息的构建:
/** 根据给定的状态码,状态消息和查询 ID 来构建一个 Response */ public Response createResponse(String statusCode, String message, String inResponseTo) { Response response = create (Response.class, Response.DEFAULT_ELEMENT_NAME); response.setID (generator.generateIdentifier ()); if (inResponseTo != null) response.setInResponseTo (inResponseTo); DateTime now = new DateTime (); response.setIssueInstant (now); if (issuerURL != null) response.setIssuer (spawnIssuer ()); StatusCode statusCodeElement = create (StatusCode.class, StatusCode.DEFAULT_ELEMENT_NAME); statusCodeElement.setValue (statusCode); Status status = create (Status.class, Status.DEFAULT_ELEMENT_NAME); status.setStatusCode (statusCodeElement); response.setStatus (status); if (message != null) { StatusMessage statusMessage = create (StatusMessage.class, StatusMessage.DEFAULT_ELEMENT_NAME); statusMessage.setMessage (message); status.setStatusMessage (statusMessage); } return response; }
根据 SAML 2.0 标准规定,我们提供给 SP 的 HTTP FORM 中,名为”SAMLResponse”表单控件的值需要经过 Base64 编码后发送给 SP 端,因此需要对最终构建完成的 <Response> 进行 Base64 编码,网上有很多类似的资源,读者也可以参考本文提供的 Sample Code 来实现,这里将不做介绍。
回页首
当客户端 Web SSO 服务发送生成的 HTTP 表单请求到服务端后,服务端的 Web SSO 服务需要处理来自客户端的请求,解析 SAMLResponse 参数值并从中获得用户 ID 字段信息,当 SP 认为该用户 ID 合法,则将用户重定向到所请求的资源。本文服务端 Web SSO 将基于 WAS 的 Trust Association Interceptor 实现。
回页首
简单来说,WAS Trust Association Interceptor(TAI) 信任联合拦截器会拦截所有来自客户端的请求,并对 http 请求内容进行分析,如果满足 TAI 的认证要求,将认为用户是合法的使用者,并将创建一个用户信息对象传递给 WAS 继续处理,根据应用配置的不同,通过 TAI 的认证后展现的内容不同。本章将简单介绍 WAS TAI,重点介绍如何利用 OpenSAML 库来解析来自客户端的 SAMLResponse 请求。如果读者对 TAI 的定制化实现感兴趣,可以参考 IBM 提供的 Websphere Application Server 官方文档。
WAS TAI 接口的两个关键方法如下:
TAIResult 类有三个静态方法来创建一个 TAIResult。三个静态方法都将一个 int 类型值作为第一个参数。这个参数应该是一个合法的 HTTP 请求返回代码。例如 HttpServletResponse.SC_OK 告诉 WAS TAI 完成协商,WAS 将使用在 TAIResult 中的信息来创建用户身份。读者可以在本文提供的实例代码中看到如何实现自己定制化的 TAI。这里不再详细介绍。在使用 isTargetInterceptor () 方法判断来自客户端的 SSO 属于 SAML SSO 请求后,将使用 negotiateValidateandEstablishTrust() 方法来分析用户的 SAML 2.0 SSO 请求,并根据认证的结果创建一个 TAIResult 对象。在该方法中调用了方法 TAISSOConsumer() 来解析 SAML 2.0Web SSO 请求。下面将介绍如何实现 TAISSOConsumer()( 即 SAML 2.0 服务端的 Assertion Consumer Service 部分 ).
TAISSOCoumsumer() 方法是实现 SAML 2.0 ACS 对 SAML Response 的认证处理。如果来自客户端 SAML2 Response 经过加密与数字签名处理,那么需要先解密 Assertion 然后对其进行数字签名的验证。上文的案例中对 Response 提供了数字签名并经过 Base64 编码,因此在本文中需要对 Response 先进行 Base64 解码处理,然后进行认证数字签名。下面是实现的代码片段:
public String parseResponse(String RspStr,JKSKeyData jksdata) { String NameId = null; //decode the response string String SResponse=new String(Base64.decode(RspStr)); //get the Credential information char[] password =jksdata.getPassword(); …… BasicX509Credential credential = new BasicX509Credential(); credential.setEntityCertificate(certificate); credential.setPublicKey(certificate.getPublicKey()); …… // Initialize the library DefaultBootstrap.bootstrap(); // Get parser pool manager BasicParserPool ppMgr = new BasicParserPool(); ppMgr.setNamespaceAware(true); // Parse metadata file ...... // Get apropriate unmarshaller UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(metadataRoot); // Unmarshall using the document root element …… Response rsp=(Response)messageContext.getInboundMessage(); // Verify issue time, make sure the assertion time is valid DateTime time = rsp.getIssueInstant(); …… //verify response signature Signature rspSignature = rsp.getSignature(); …… rspv.validate(rspSignature); …… //verify assertion signature …… //get the name identifier after verify successfully NameId=samlAssertion.getSubject().getNameID().getValue(); NameID nid=samlAssertion.getSubject().getNameID(); return NameId; }
由 TAISSOCoumsumer() 返回的字符串 NameId 将被作为关键字,用来在 LDAP、DB 或者其他存储用户信息的地方查询,如果该用户能够被查询到,则整个 TAI 认证过程完成并返回成功的 HTTP 响应给用户,如果没有查询到该用户 ID,则认为 TAI 认证失败,将返回一个失败的 HTTP 响应或者错误页面给用户。
回页首
本文通过介绍 SAML 2.0 标准以及其中被普遍使用的 Web Browser SSO 使用框架,向读者展示了如何依据 SAML 2.0 的使用框架并利用 OpenSAML 库来实现客户端与服务端的 Web SSO。本文中所涉及的 SAML 2.0 Web SSO 流程是一个被广泛使用的框架,如果用户希望实现更为复杂的使用框架,可以参考 SAML 2.0 的官方文档与 OpenSAML 的官方站点。