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

1: 用戶打開瀏覽器請(qǐng)求SP的受保護(hù)資源
2: SP收到請(qǐng)求后發(fā)現(xiàn)沒有登錄態(tài),則生成saml request,同時(shí)請(qǐng)求IDP
3: IDP收到請(qǐng)求后,解析saml request(如果沒有登錄態(tài)) 然后重定向到登錄頁(yè)面
4: 用戶在認(rèn)證頁(yè)面完成認(rèn)證,再由IDP重定向到SP 的回調(diào)接口上
5: SP收到回掉信息后對(duì)response 解析校驗(yàn),成功后生成SP側(cè)的登錄態(tài)
二: IDP側(cè)需要搭建SAML協(xié)議的服務(wù)端
常見的有:ADFS, AZURE 當(dāng)然也有自研的
Adfs, Azure 的搭建可以參考微軟的官方文檔
https://support.freshservice.com/support/solutions/articles/226938-configuring-adfs-for-freshservice-with-saml-2-0
三:SAML request構(gòu)造
SAMLRequest就是SAML認(rèn)證請(qǐng)求消息。因?yàn)镾AML是基于XML的比較長(zhǎng)需要壓縮和編碼,在壓縮和編碼之前,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>
在這個(gè)xml中,我們需要填充3個(gè)信息:
1: AssertionConsumerServiceURL(IDP 認(rèn)證完成后的重定向地址)
2: Destination(IDP 的目的地址)
3: saml:Issuer(請(qǐng)求方的信息) ;
填充完成后再對(duì)xml進(jìn)行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上運(yùn)行是可以的,但是打成jar包是會(huì)報(bào)文件找不到的異常
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默認(rèn)是zlib壓縮(會(huì)在xml上再加一層header) 同時(shí),第2個(gè)參數(shù)要設(shè)置為true,因?yàn)檫@個(gè)字段為true則Deflater執(zhí)行壓縮的時(shí)候就不會(huì)把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ā)送請(qǐng)求
https://adfs.xxxx.com/adfs/ls?SAMLRequest={第三步中生成的請(qǐng)求參數(shù)}
當(dāng)IDP收到請(qǐng)求校驗(yàn)通過(guò)后就會(huì)重定向到自己的login頁(yè)面,登錄完成后再call back需要免登的應(yīng)用服務(wù)
五:response解析
IDP驗(yàn)證完成后會(huì)生成response給我的應(yīng)用服務(wù),同時(shí)我們需要再對(duì)response進(jìn)行相應(yīng)的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