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