基於JWT的單點登入
阿新 • • 發佈:2019-01-28
什麼是SSO
SSO(Single Sign On): 單點登入。
意思是講,在多個應用系統中,使用者只要登入一次,就可以訪問所有相互信任的應用。
就比如天貓和淘寶。
什麼是JWT
JWT(Json Web Tokens): 是一種規範。
其實我更願意把它認為是一種規範的原因是,它本身是一種Token生成機制。
但是內部採用了更為結構化的格式來進行認證(Authentication)。
JWT的資料結構
它是一個很長的字串,中間用點(.)分隔成三個部分。
JWT 的三個部分依次如下:
- Header(頭部)
- Payload(負載)
- Signature(簽名)
寫成一行,就是下面的樣子。
Header.Payload.Signature
使用JWT實現SSO的原理
這裡盜用一幅網上很易懂的漫畫。
- 首先,伺服器應用(下面簡稱“應用”)讓使用者通過Web表單將自己的使用者名稱和密碼傳送到伺服器的介面。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感資訊被嗅探。
- 接下來,應用和資料庫核對使用者名稱和密碼。
- 核對使用者名稱和密碼成功後,應用將使用者的id(圖中的user_id)作為JWT Payload的一個屬性,將其與頭部分別進行Base64URL編碼拼接後簽名,形成一個JWT。這裡的JWT就是一個形同lll.zzz.xxx的字串。
- 應用將JWT字串作為該請求Cookie的一部分返回給使用者。注意,在這裡必須使用HttpOnly屬性來防止Cookie被JavaScript讀取,從而避免跨站指令碼攻擊(XSS攻擊)。
- 在Cookie失效或者被刪除前,使用者每次訪問應用,應用都會接受到含有jwt的Cookie。從而應用就可以將JWT從請求中提取出來。
- 應用通過一系列任務檢查JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
- 應用在確認JWT有效之後,JWT進行Base64解碼(可能在上一步中已經完成),然後在Payload中讀取使用者的id值,也就是user_id屬性。這裡使用者的id為1025。
- 應用從資料庫取到id為1025的使用者的資訊,載入到記憶體中,進行ORM之類的一系列底層邏輯初始化。
- 應用根據使用者請求進行響應。
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失效即可。