SAML 即安全斷言標記語言,英文全稱是 Security Assertion Markup Language。它是一個基于 XML 的標準,用于在不同的安全域(security domain)之間交換認證和授權數(shù)據(jù)。在 SAML 標準定義了身份提供者 (identity provider) 和服務提供者 (service provider),可以用來傳輸安全聲明。
最近在集成客戶單點,正好用到了SAML協(xié)議,SAML在單點登錄中一旦用戶身份被主網(wǎng)站(身份鑒別服務器,Identity Provider,IDP)認證過后,該用戶再去訪問其他在主站注冊過的應用(服務提供者,Service Providers,SP)時,都可以直接登錄,而不用再輸入身份和口令。
一: SAML 單點流程

1: 用戶打開瀏覽器請求SP的受保護資源
2: SP收到請求后發(fā)現(xiàn)沒有登錄態(tài),則生成saml request,同時請求IDP
3: IDP收到請求后,解析saml request(如果沒有登錄態(tài)) 然后重定向到登錄頁面
4: 用戶在認證頁面完成認證,再由IDP重定向到SP 的回調(diào)接口上
5: SP收到回掉信息后對response 解析校驗,成功后生成SP側(cè)的登錄態(tài)
二: IDP側(cè)需要搭建SAML協(xié)議的服務端
常見的有:ADFS, AZURE 當然也有自研的
Adfs, Azure 的搭建可以參考微軟的官方文檔
https://support.freshservice.com/support/solutions/articles/226938-configuring-adfs-for-freshservice-with-saml-2-0
三:SAML request構造
SAMLRequest就是SAML認證請求消息。因為SAML是基于XML的比較長需要壓縮和編碼,在壓縮和編碼之前,SAMLRequest格式如下:
<samlp:AuthnRequest
AssertionConsumerServiceURL="https://www.xxx.cn/authentication/saml/idp/call_back"
Destination=""
ID="_c0c38877-6966-488f-8196-7dd6afd2a958"
IssueInstant="2020-11-17T03:18:46Z"
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Version="2.0"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:Issuer>https://www.xxx.cn</saml:Issuer>
<samlp:NameIDPolicy AllowCreate="true" Format=""/>
</samlp:AuthnRequest>
在這個xml中,我們需要填充3個信息:
1: AssertionConsumerServiceURL(IDP 認證完成后的重定向地址)
2: Destination(IDP 的目的地址)
3: saml:Issuer(請求方的信息) ;
填充完成后再對xml進行Deflater編碼 + Base64編碼
填充xml的方法:
public static String fillDestination(String destination) {
SAXReader reader = new SAXReader();
Document document = null;
try {
//document = reader.read(ResourceUtils.getFile("classpath:samlXml/samlRquestXml.xml"));
//上面這種方式在idea上運行是可以的,但是打成jar包是會報文件找不到的異常
ClassPathResource classPathResource = new ClassPathResource("samlXml/samlRquestXml.xml");
document = reader.read(classPathResource.getInputStream());
Element rootNode = document.getRootElement();
List<Attribute> attributes = rootNode.attributes();
for (Attribute a : attributes) {
if (a.getName().equalsIgnoreCase("Destination")) {
a.setValue(destination);
break;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException | DocumentException e) {
e.printStackTrace();
}
return document.asXML();
}
Deflater編碼 + Base64編碼
private static String getString(String samlRequest) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
DeflaterOutputStream deflater = new DeflaterOutputStream(outputStream, new Deflater(Deflater.DEFLATED, true));
try {
deflater.write(samlRequest.getBytes(StandardCharsets.UTF_8));
deflater.finish();
// 2.Base64
return Base64.encode(outputStream.toByteArray());
} catch (Exception ex) {
} finally {
if (!ObjectUtils.isEmpty(outputStream)) {
outputStream.close();
}
if (!ObjectUtils.isEmpty(deflater)) {
deflater.close();
}
}
return null;
}
注:Deflater默認是zlib壓縮(會在xml上再加一層header) 同時,第2個參數(shù)要設置為true,因為這個字段為true則Deflater執(zhí)行壓縮的時候就不會把header信息序列化到xml 原文如下:
If 'nowrap' is true then the ZLIB header and checksum fields will not be used in order to support the compression format used in both GZIP and PKZIP.
四:發(fā)送請求
https://adfs.xxxx.com/adfs/ls?SAMLRequest={第三步中生成的請求參數(shù)}
當IDP收到請求校驗通過后就會重定向到自己的login頁面,登錄完成后再call back需要免登的應用服務
五:response解析
IDP驗證完成后會生成response給我的應用服務,同時我們需要再對response進行相應的Base64 decode 和 inflater
public static String xmlInflater(String samlRequest) throws IOException {
byte[] decodedBytes = Base64.decode(samlRequest);
StringBuilder stringBuffer = new StringBuilder();
ByteArrayInputStream bytesIn = new ByteArrayInputStream(decodedBytes);
InflaterInputStream inflater = new InflaterInputStream(bytesIn, new Inflater(true));
try {
byte[] b = new byte[1024];
int len = 0;
while (-1 != (len = inflater.read(b))) {
stringBuffer.append(new String(b, 0, len));
}
} catch (Exception e) {
//write log
} finally {
if (!ObjectUtils.isEmpty(inflater)) {
inflater.close();;
}
if (!ObjectUtils.isEmpty(bytesIn)) {
bytesIn.close();
}
}
return stringBuffer.toString();
}
最后再介紹一款在線的samltool
https://www.samltool.com/url.php