單點(diǎn)登錄 - SAML協(xié)議

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)流程

image.png

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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容