1. 程式人生 > >基於JWT的單點登入

基於JWT的單點登入

什麼是SSO

SSO(Single Sign On): 單點登入。
意思是講,在多個應用系統中,使用者只要登入一次,就可以訪問所有相互信任的應用。
就比如天貓和淘寶。

什麼是JWT

JWT(Json Web Tokens): 是一種規範。
其實我更願意把它認為是一種規範的原因是,它本身是一種Token生成機制。
但是內部採用了更為結構化的格式來進行認證(Authentication)。

JWT的資料結構

它是一個很長的字串,中間用點(.)分隔成三個部分。
JWT 的三個部分依次如下:

  • Header(頭部)
  • Payload(負載)
  • Signature(簽名)
    寫成一行,就是下面的樣子。
    Header.Payload.Signature

JWT資料結構

使用JWT實現SSO的原理

這裡盜用一幅網上很易懂的漫畫。

  • 首先,伺服器應用(下面簡稱“應用”)讓使用者通過Web表單將自己的使用者名稱和密碼傳送到伺服器的介面。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感資訊被嗅探。
    jauth1
  • 接下來,應用和資料庫核對使用者名稱和密碼。
    jauth2
  • 核對使用者名稱和密碼成功後,應用將使用者的id(圖中的user_id)作為JWT Payload的一個屬性,將其與頭部分別進行Base64URL編碼拼接後簽名,形成一個JWT。這裡的JWT就是一個形同lll.zzz.xxx的字串。
    jauth3
  • 應用將JWT字串作為該請求Cookie的一部分返回給使用者。注意,在這裡必須使用HttpOnly屬性來防止Cookie被JavaScript讀取,從而避免跨站指令碼攻擊(XSS攻擊)。
    jauth4
  • 在Cookie失效或者被刪除前,使用者每次訪問應用,應用都會接受到含有jwt的Cookie。從而應用就可以將JWT從請求中提取出來。
    jauth5
  • 應用通過一系列任務檢查JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
    jauth6
  • 應用在確認JWT有效之後,JWT進行Base64解碼(可能在上一步中已經完成),然後在Payload中讀取使用者的id值,也就是user_id屬性。這裡使用者的id為1025。
    jauth7
  • 應用從資料庫取到id為1025的使用者的資訊,載入到記憶體中,進行ORM之類的一系列底層邏輯初始化。
    jauth8
  • 應用根據使用者請求進行響應。
    jauth9

Tips

  • 注意Base64演算法與Base64URL演算法是不一致的。
  • 這裡可以加入兩個token: token和refreshToken。如果使用者在refreshToken過期之前繼續進行了操作,可以延長token和refreshToken的有效期。
  • JWT可以儲存在 Cookie 裡面(可以指定 httponly,來防止被Javascript讀取,也可以指定secure,來保證token只在HTTPS下傳輸,並且xsrf隨機數有完善的應用機制),
    也可以儲存在 localStorage(容易受到XSS攻擊),
    或者瀏覽器Header的Authorization欄位裡面放置Token(適用於ajax請求或者api請求,可以方便的設定auth頭,方便跨域請求)。
  • 客戶端每次使用的時候都帶上這個token進行認證。

原始碼層次分析JWT如何生成

在io.jsonwebtoken包的具體實現類DefaultJwtBuilder中,提供一個完整的JWT生成方法compact()。

        Header header = ensureHeader();

        Key key = this.key;
        if (key == null && !Objects.isEmpty(keyBytes)) {
            key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
        }

        JwsHeader jwsHeader;

        if (header instanceof JwsHeader) {
            jwsHeader = (JwsHeader)header;
        } else {
            jwsHeader = new DefaultJwsHeader(header);
        }

        if (key != null) {
            // 生成資訊,"alg"->"HS256"
            jwsHeader.setAlgorithm(algorithm.getValue());
        } else {
            //no signature - plaintext JWT:
            jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
        }
        // jwsHeader進行Base64URL加密,生成Header部分
        String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");

        String base64UrlEncodedBody;

        if (compressionCodec != null) {

            byte[] bytes;
            try {
                bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
            } catch (JsonProcessingException e) {
                throw new IllegalArgumentException("Unable to serialize claims object to json.");
            }

            base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));

        } else {
            // claims包含了使用者資訊,{id,obj.str_time,end_time},由於是可逆演算法,最好不要講隱私資訊存放到payload中。
            // 進行Base64URL加密,生成Payload部分。
            base64UrlEncodedBody = this.payload != null ?
                    TextCodec.BASE64URL.encode(this.payload) :
                    base64UrlEncode(claims, "Unable to serialize claims object to json.");
        }
        
        // 定義拼接規則
        String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;
        
        // 對前兩個部分資料進行簽名,防止篡改,key金鑰是事先約定好的,儲存在服務端。
        if (key != null) { //jwt must be signed:

            JwtSigner signer = createSigner(algorithm, key);

            String base64UrlSignature = signer.sign(jwt);
            
            // 最終生成JWT
            jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;
        } else {
            // no signature (plaintext), but must terminate w/ a period, see
            // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1
            jwt += JwtParser.SEPARATOR_CHAR;
        }

其他問題總結

  • 關於Cookie的設定
        Cookie cookie = new Cookie(name, value);
        cookie.setSecure(false); // 設定是否只能通過https來傳遞此條cookie,預設是false
        cookie.setHttpOnly(true);// 防禦XSS攻擊
        cookie.setMaxAge(-1);// 規定cookie多長時間之後過期.負值(預設值)表明cookie僅僅用於當前瀏覽會話,並不儲存到磁碟上.
        cookie.setDomain("localhost");// 可以訪問此cookie的域名
        cookie.setPath("/");// 可在同一應用伺服器內共享cookie
        response.addCookie(cookie);
  • Ajax請求,後臺重定向失效的問題。
    每次發起ajax請求, ajax請求的結果就是這個頁面(具體的說,這次請求返回的就是登入頁面的原始碼), 
    所以瀏覽器不會發生跳轉.
    還是隻能從responseText中獲取跳轉資訊使用js跳轉
    這個需要深入瞭解一下Ajax請求和response.sendRedirect的機制。

  • 使用者退出登入的時候,設定Cookie失效即可。

原始碼,歡迎Star