编程爱好者联盟 2016-09-29
一、背景知识:
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 主要包括三个方面:
1.认证申明。表明用户是否已经认证,通常用于单点登录。
2.属性申明。表明 某个Subject 的属性。
3.授权申明。表明 某个资源的权限。
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,笔者偷懒,简单做了修改)
此图片说明了以下步骤。
生成报文示例代码:
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); } }