添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

一、背景知识:

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