使用JWT實現單點登入(完全跨域方案)單點登入SSO技術選型
首先介紹一下什麼是JSON Web Token(JWT)?
官方文件是這樣解釋的:JSON Web Token(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON物件安全地傳輸資訊。此資訊可以通過數字簽名進行驗證和信任。JWT可以使用祕密(使用HMAC演算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。
雖然JWT可以加密以在各方之間提供保密,但只將專注於簽名令牌。簽名令牌可以驗證其中包含的宣告的完整性,而加密令牌則隱藏其他方的宣告。當使用公鑰/私鑰對簽署令牌時,簽名還證明只有持有私鑰的一方是簽署私鑰的一方。
通俗來講,JWT是一個含簽名並攜帶使用者相關資訊的加密串,頁面請求校驗登入介面時,請求頭中攜帶JWT串到後端服務,後端通過簽名加密串匹配校驗,保證資訊未被篡改。校驗通過則認為是可靠的請求,將正常返回資料。
什麼情況下使用JWT比較適合?
- 授權:這是最常見的使用場景,解決單點登入問題。因為JWT使用起來輕便,開銷小,服務端不用記錄使用者狀態資訊(無狀態),所以使用比較廣泛;
- 資訊交換:JWT是在各個服務之間安全傳輸資訊的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。
JWT的結構體是什麼樣的?
JWT由三部分組成,分別是頭資訊、有效載荷、簽名,中間以(.)分隔,如下格式:
xxx.yyy.zzz
- 1
header(頭資訊)
由兩部分組成,令牌型別(即:JWT)、雜湊演算法(HMAC、RSASSA、RSASSA-PSS等),例如:
{
"alg": "HS256",
"typ": "JWT"
}
- 1
- 2
- 3
- 4
然後,這個JSON被編碼為Base64Url,形成JWT的第一部分。
Payload(有效載荷)
JWT的第二部分是payload,其中包含claims。claims是關於實體(常用的是使用者資訊)和其他資料的宣告,claims有三種類型: registered, public, and private claims。
Registered claims:這些是一組預定義的claims,非強制性的,但是推薦使用, iss(發行人), exp(到期時間), sub(主題), aud(觀眾)等;
Public claims:自定義claims,注意不要和JWT登錄檔中屬性衝突,
Private claims:這些是自定義的claims,用於在同意使用這些claims的各方之間共享資訊,它們既不是Registered claims,也不是Public claims。
以下是payload示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
- 1
- 2
- 3
- 4
- 5
然後,再經過Base64Url編碼,形成JWT的第二部分;
注意:對於簽名令牌,此資訊雖然可以防止篡改,但任何人都可以讀取。除非加密,否則不要將敏感資訊放入到Payload或Header元素中。
Signature
要建立簽名部分,必須採用編碼的Header,編碼的Payload,祕鑰,Header中指定的演算法,並對其進行簽名。
例如,如果要使用HMAC SHA256演算法,將按以下方式建立簽名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- 1
- 2
- 3
- 4
簽名用於驗證訊息在此過程中未被篡改,並且,在使用私鑰簽名令牌的情況下,它還可以驗證JWT的請求方是否是它所宣告的請求方。
輸出是三個由點分隔的Base64-URL字串,可以在HTML和HTTP環境中輕鬆傳遞,與SAML等基於XML的標準相比更加緊湊。
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- 1
- 2
- 3
JWT工作機制?
在身份驗證中,當用戶使用其憑據成功登入時,將返回JSON Web Token(即:JWT)。由於令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,不應將令牌保留的時間超過要求。理論上超時時間越短越好。
每當使用者想要訪問受保護的路由或資源時,使用者代理應該使用Bearer模式傳送JWT,通常在Authorization header中。標題內容應如下所示:
Authorization: Bearer <token>
- 1
在某些情況下,這可以作為無狀態授權機制。伺服器的受保護路由將檢查Authorization header中的有效JWT ,如果有效,則允許使用者訪問受保護資源。如果JWT包含必要的資料,則可以減少查詢資料庫或快取資訊。
如果在Authorization header中傳送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。
注意:使用簽名令牌,雖然他們無法更改,但是令牌中包含的所有資訊都會向用戶或其他方公開。這意味著不應該在令牌中放置敏感資訊。
使用JWT的好處是什麼?
相比Simple Web Tokens (SWT)(簡單Web令牌)andSecurity Assertion Markup Language Tokens (SAML)(安全斷言標記語言令牌);
- JWT比SAML更簡潔,在HTML和HTTP環境中傳遞更方便;
- 在安全方面,SWT只能使用HMAC演算法通過共享金鑰對稱簽名。但是,JWT和SAML令牌可以使用X.509證書形式的公鑰/私鑰對進行簽名。與簽名JSON的簡單性相比,使用XML數字簽名可能會存在安全漏洞;
- JSON解析成物件相比XML更流行、方便。
以下是我實際專案中的應用分析
首先看一下大致的架構及流程圖:
主要有以下三步:
- 專案一開始我先封裝了一個JWTHelper工具包(GitHub下載),主要提供了生成JWT、解析JWT以及校驗JWT的方法,其他還有一些加密相關操作,稍後我會以程式碼的形式介紹下程式碼。工具包寫好後我將打包上傳到私服,能夠隨時依賴下載使用;
- 接下來,我在客戶端專案中依賴JWTHelper工具包,並新增Interceptor攔截器,攔截需要校驗登入的介面。攔截器中校驗JWT有效性,並在response中重新設定JWT的新值;
- 最後在JWT服務端,依賴JWT工具包,在登入方法中,需要在登入校驗成功後呼叫生成JWT方法,生成一個JWT令牌並且設定到response的header中。
以下是部分程式碼分享:
- JwtHelper工具類:
/**
* @Author: Helon
* @Description: JWT工具類
* 參考官網:https://jwt.io/
* JWT的資料結構為:A.B.C三部分資料,由字元點"."分割成三部分資料
* A-header頭資訊
* B-payload 有效負荷 一般包括:已註冊資訊(registered claims),公開資料(public claims),私有資料(private claims)
* C-signature 簽名信息 是將header和payload進行加密生成的
* @Data: Created in 2018/7/19 14:11
* @Modified By:
*/
public class JwtHelper {
private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);
/**
* @Author: Helon
* @Description: 生成JWT字串
* 格式:A.B.C
* A-header頭資訊
* B-payload 有效負荷
* C-signature 簽名信息 是將header和payload進行加密生成的
* @param userId - 使用者編號
* @param userName - 使用者名稱
* @param identities - 客戶端資訊(變長引數),目前包含瀏覽器資訊,用於客戶端攔截器校驗,防止跨域非法訪問
* @Data: 2018/7/28 19:26
* @Modified By:
*/
public static String generateJWT(String userId, String userName, String ...identities) {
//簽名演算法,選擇SHA-256
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//獲取當前系統時間
long nowTimeMillis = System.currentTimeMillis();
Date now = new Date(nowTimeMillis);
//將BASE64SECRET常量字串使用base64解碼成位元組陣列
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);
//使用HmacSHA256簽名演算法生成一個HS256的簽名祕鑰Key
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//新增構成JWT的引數
Map<String, Object> headMap = new HashMap<>();
/*
Header
{
"alg": "HS256",
"typ": "JWT"
}
*/
headMap.put("alg", SignatureAlgorithm.HS256.getValue());
headMap.put("typ", "JWT");
JwtBuilder builder = Jwts.builder().setHeader(headMap)
/*
Payload
{
"userId": "1234567890",
"userName": "John Doe",
}
*/
//加密後的客戶編號
.claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))
//客戶名稱
.claim("userName", userName)
//客戶端瀏覽器資訊
.claim("userAgent", identities[0])
//Signature
.signWith(signatureAlgorithm, signingKey);
//新增Token過期時間
if (SecretConstant.EXPIRESSECOND >= 0) {
long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;
Date expDate = new Date(expMillis);
builder.setExpiration(expDate).setNotBefore(now);
}
return builder.compact();
}
/**
* @Author: Helon
* @Description: 解析JWT
* 返回Claims物件
* @param jsonWebToken - JWT
* @Data: 2018/7/28 19:25
* @Modified By:
*/
public static Claims parseJWT(String jsonWebToken) {
Claims claims = null;
try {
if (StringUtils.isNotBlank(jsonWebToken)) {
//解析jwt
claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))
.parseClaimsJws(jsonWebToken).getBody();
}else {
logger.warn("[JWTHelper]-json web token 為空");
}
} catch (Exception e) {
logger.