OpenSAML

一、背景知识:

      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,笔者偷懒,简单做了修改)

此图片说明了以下步骤。

  1. 用户尝试访问WebApp1。
  2. WebApp1 生成一个 SAML 身份验证请求。SAML 请求将进行编码并嵌入到SSO 服务的网址中。包含用户尝试访问的 WebApp1 应用程序的编码网址的 RelayState 参数也会嵌入到 SSO 网址中。该 RelayState 参数作为不透明标识符,将直接传回该标识符而不进行任何修改或检查。
  3. WebApp1将重定向发送到用户的浏览器。重定向网址包含应向SSO 服务提交的编码 SAML 身份验证请求。
  4. SSO(统一认证中心或叫Identity Provider)解码 SAML 请求,并提取 WebApp1的 ACS(声明客户服务)网址以及用户的目标网址(RelayState 参数)。然后,统一认证中心对用户进行身份验证。统一认证中心可能会要求提供有效登录凭据或检查有效会话 Cookie 以验证用户身份。
  5. 统一认证中心生成一个 SAML 响应,其中包含经过验证的用户的用户名。按照 SAML 2.0 规范,此响应将使用统一认证中心的 DSA/RSA 公钥和私钥进行数字签名。
  6. 统一认证中心对 SAML 响应和 RelayState 参数进行编码,并将该信息返回到用户的浏览器。统一认证中心提供了一种机制,以便浏览器可以将该信息转发到 WebApp1 ACS。
  7. WebApp1使用统一认证中心的公钥验证 SAML 响应。如果成功验证该响应,ACS 则会将用户重定向到目标网址。
  8. 用户将重定向到目标网址并登录到 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);
  }
}