一、背景知識:
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);
}
}

