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

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 單點流程

image.png

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

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

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