一、背景知识:
SAML即安全断言标记语言,英文全称是Security Assertion Markup Language。它是一个基于XML的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在SAML标准定义了身份提供者(identity provider)和服务提供者(service provider),这两者构成了前面所说的不同的安全域。 SAML是OASIS组织安全服务技术委员会(Security Services Technical Committee)的产品。
SAML(Security Assertion Markup Language)是一个XML框架,也就是一组协议,可以用来传输安全声明。比如,两台远程机器之间要通讯,为了保证安全,我们可以采用加密等措施,也可以采用SAML来传输,传输的数据以XML形式,符合SAML规范,这样我们就可以不要求两台机器采用什么样的系统,只要求能理解SAML规范即可,显然比传统的方式更好。SAML 规范是一组Schema 定义。
可以这么说,在Web Service 领域,schema就是规范,在Java领域,API就是规范。
SAML 作用
SAML 主要包括三个方面:
1.认证申明。表明用户是否已经认证,通常用于单点登录。
2.属性申明。表明 某个Subject 的属性。
3.授权申明。表明 某个资源的权限。
SAML框架
SAML就是客户向服务器发送SAML 请求,然后服务器返回SAML响应。数据的传输以符合SAML规范的XML格式表示。
SAML 可以建立在SOAP上传输,也可以建立在其他协议上传输。
因为SAML的规范由几个部分构成:SAML Assertion,SAML Prototol,SAML binding等
安全
由于SAML在两个拥有共享用户的站点间建立了信任关系,所以安全性是需考虑的一个非常重要的因素。SAML中的安全弱点可能危及用户在目标站点的个人信息。SAML依靠一批制定完善的安全标准,包括SSL和X.509,来保护SAML源站点和目标站点之间通信的安全。源站点和目标站点之间的所有通信都经过了加密。为确保参与SAML交互的双方站点都能验证对方的身份,还使用了证书。
应用
目前SAML已经在很多商业/开源产品得到应用推广,主要有:
IBM Tivoli Access Manager
Weblogic
Oblix NetPoint
SunONE Identity Server
Baltimore, SelectAccess
Entegrity Solutions AssureAccess
Internet2 OpenSAML
Yale CAS 3
Netegrity SiteMinder
Sigaba Secure Messaging Solutions
RSA Security ClearTrust
VeriSign Trust Integration Toolkit
Entrust GetAccess 7
二、基于 SAML的SSO
下面简单介绍使用基于SAML的SSO登录到WebApp1的过程(下图源自SAML 的 Google Apps SSO,笔者偷懒,简单做了修改)
此图片说明了以下步骤。
- 用户尝试访问WebApp1。
- WebApp1 生成一个 SAML 身份验证请求。SAML 请求将进行编码并嵌入到SSO 服务的网址中。包含用户尝试访问的 WebApp1 应用程序的编码网址的 RelayState 参数也会嵌入到 SSO 网址中。该 RelayState 参数作为不透明标识符,将直接传回该标识符而不进行任何修改或检查。
- WebApp1将重定向发送到用户的浏览器。重定向网址包含应向SSO 服务提交的编码 SAML 身份验证请求。
- SSO(统一认证中心或叫Identity Provider)解码 SAML 请求,并提取 WebApp1的 ACS(声明客户服务)网址以及用户的目标网址(RelayState 参数)。然后,统一认证中心对用户进行身份验证。统一认证中心可能会要求提供有效登录凭据或检查有效会话 Cookie 以验证用户身份。
- 统一认证中心生成一个 SAML 响应,其中包含经过验证的用户的用户名。按照 SAML 2.0 规范,此响应将使用统一认证中心的 DSA/RSA 公钥和私钥进行数字签名。
- 统一认证中心对 SAML 响应和 RelayState 参数进行编码,并将该信息返回到用户的浏览器。统一认证中心提供了一种机制,以便浏览器可以将该信息转发到 WebApp1 ACS。
- WebApp1使用统一认证中心的公钥验证 SAML 响应。如果成功验证该响应,ACS 则会将用户重定向到目标网址。
- 用户将重定向到目标网址并登录到 WebApp1。
生成报文示例代码:
package test; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.common.xml.*; import org.opensaml.common.SAMLVersion; import org.joda.time.DateTime; import org.opensaml.saml2.core.*; import org.opensaml.saml2.core.impl.*; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.util.XMLHelper; import org.w3c.dom.Element; import java.io.*; import java.math.BigInteger; import java.security.SecureRandom; public class OpenSaml { static { try { DefaultBootstrap.bootstrap(); } catch (ConfigurationException e) { e.printStackTrace(); } } public void generateRequestURL() throws Exception { String consumerServiceUrl = "http://localhost:8080/consume.jsp"; // Set this for your app String website = "https://www.efesco.com"; // Set this for your app AuthnRequestBuilder authRequestBuilder = new AuthnRequestBuilder(); AuthnRequest authnRequest = authRequestBuilder.buildObject(SAMLConstants.SAML20P_NS, "AuthnRequest", "samlp"); authnRequest.setIsPassive(false); authnRequest.setIssueInstant(new DateTime()); authnRequest.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); authnRequest.setAssertionConsumerServiceURL(consumerServiceUrl); authnRequest.setID(new BigInteger(130, new SecureRandom()).toString(42)); authnRequest.setVersion(SAMLVersion.VERSION_20); IssuerBuilder issuerBuilder = new IssuerBuilder(); Issuer issuer = issuerBuilder.buildObject(SAMLConstants.SAML20_NS, "Issuer", "samlp" ); issuer.setValue(website); authnRequest.setIssuer(issuer); NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder(); NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject(); nameIdPolicy.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"); nameIdPolicy.setAllowCreate(true); authnRequest.setNameIDPolicy(nameIdPolicy); RequestedAuthnContextBuilder requestedAuthnContextBuilder = new RequestedAuthnContextBuilder(); RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject(); requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT); AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder(); AuthnContextClassRef authnContextClassRef = authnContextClassRefBuilder.buildObject(SAMLConstants.SAML20_NS, "AuthnContextClassRef", "saml"); authnContextClassRef.setAuthnContextClassRef("urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"); requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef); authnRequest.setRequestedAuthnContext(requestedAuthnContext); Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(authnRequest); Element authDOM = marshaller.marshall(authnRequest); StringWriter requestWriter = new StringWriter(); XMLHelper.writeNode(authDOM, requestWriter); String messageXML = requestWriter.toString(); System.out.println(messageXML); } public static void main(String[] args) throws Exception { OpenSaml openSaml = new OpenSaml(); openSaml.generateRequestURL(); } }
解析报文示例代码:
import org.apache.commons.codec.binary.Base64; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.saml2.core.*; import org.opensaml.saml2.core.impl.*; import org.opensaml.xml.io.*; import org.opensaml.xml.security.x509.BasicX509Credential; import org.w3c.dom.*; import org.opensaml.xml.*; import org.apache.commons.codec.binary.Base64; import java.io.*; import java.security.*; import java.security.cert.*; import java.security.spec.*; import javax.xml.parsers.*; public class SAMLResponseHandler { private static final String certificateS = "MIIENTCCAx2gAwIBAgIUDFWeXo2US+Je8Erqdc2IvREy8IswDQYJKoZIhvcNAQEF" + "BQAwYjELMAkGA1UEBhMCVVMxGzAZBgNVBAoMEkNvbm5lY3RpZmllciwgSW5jLjEV" + "MBMGA1UECwwMT25lTG9naW4gSWRQMR8wHQYDVQQDDBZPbmVMb2dpbiBBY2NvdW50" + "BhMCVVMxGzAZBgNVBAoMEkNvbm5lY3RpZmllciwgSW5jLjEVMBMGA1UECwwMT25l" + "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3ymFFiFfvDY/YsHFNg7sLON3luGo" + "TG9naW4gSWRQMR8wHQYDVQQDDBZPbmVMb2dpbiBBY2NvdW50IDQ1NTAxMIIBIjAN" + "I84UQx3N8nwl5ayfOJM3KC4AvExeWQQxfc2nO01SPrgJEy/DLr8OeFIXEVVBPVFe" + "MKa2TnOARRImshLFzehOu0S+3AcrTWUnQccjpdpC/VUY8z65ntfm0W0XHtJ3HkVW" + "uUMPl63X/OU7RLm0ALKahMs9+WV7LcwP/CkDGYUr2UcXz1Ehrcqh6x8FGx90OJCl" + "Ws06mWpZYMSlMhNnT2cjN2+50HpU+51mearoZ6uKhD9SwpU4WkIFvfG1GGqj3ZS2" + "mTvw1V7RZ28XV7ou5TUEf5YfpsWZ8FMAisiPZpO/mJCBqTSi2KjWN6P/rwIDAQAB" + "IDQ1NTAxMB4XDTE0MDgwMzIxNDcyMloXDTE5MDgwNDIxNDcyMlowYjELMAkGA1UE" + "o4HiMIHfMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFFwXtgC2NizDcjsi2SM+Jzt5" + "cMt/MIGfBgNVHSMEgZcwgZSAFFwXtgC2NizDcjsi2SM+Jzt5cMt/oWakZDBiMQsw" + "FAxVnl6NlEviXvBK6nXNiL0RMvCLMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B" + "CQYDVQQGEwJVUzEbMBkGA1UECgwSQ29ubmVjdGlmaWVyLCBJbmMuMRUwEwYDVQQL" + "d0Ld0d2Dt6Gvsczba6fsbdmka9sdjLAfkA9dasdA3sFkasyqoiMN09123jJAooAI" + "AQUFAAOCAQEA0FiaxTnK6D9HwirzOcQ0a7/lqqXHnm9nOw6bUS9TKlMNkoV0CqIq" + "I6r8zWcB1CqsvrPsB4c3jB0Uc3u8hl+mOkvPUsMOsfM1fV+iGMFl4bYpd/HxQOpv" + "tWMpi0TPat/WrbNOEPikahZwMK/XycoZ09VaXFoooSpYoOAaS4pAEwfabneAt1Pu" + "O0IS6PrERgRFOe0ww2K9SNImvDLpH1rd239PUXKFFAtasuZhw6ol+kJwgylcyEHU" + "SHHfYGDkRCVStrFN5uzPOurZKEfa9NETAKN5p2VetJ6+G9xPV05ONjDNZQLpo+VY" + "eewqdHDL2SDOiEAblF1hYy5dDb/Fjc3W0Q=="; public void handle(String responseMessage) { // Read certificate CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); InputStream inputStream = new ByteArrayInputStream(Base64.decodeBase64(certificateS.getBytes("UTF-8"))); X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream); inputStream.close(); BasicX509Credential credential = new BasicX509Credential(); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(certificate.getPublicKey().getEncoded()); PublicKey key = keyFactory.generatePublic(publicKeySpec); credential.setPublicKey(key); // Parse response byte[] base64DecodedResponse = Base64.decodeBase64(responseMessage); ByteArrayInputStream is = new ByteArrayInputStream(base64DecodedResponse); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); Document document = docBuilder.parse(is); Element element = document.getDocumentElement(); UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); XMLObject responseXmlObj = unmarshaller.unmarshall(element); Response responseObj = (Response) responseXmlObj; Assertion assertion = responseObj.getAssertions().get(0); String subject = assertion.getSubject().getNameID().getValue(); String issuer = assertion.getIssuer().getValue(); String audience = assertion.getConditions().getAudienceRestrictions().get(0).getAudiences().get(0).getAudienceURI(); String statusCode = responseObj.getStatus().getStatusCode().getValue(); org.opensaml.xml.signature.Signature sig = assertion.getSignature(); org.opensaml.xml.signature.SignatureValidator validator = new org.opensaml.xml.signature.SignatureValidator(credential); validator.validate(sig); } }