最近在使用JWT做一個單點登錄與接口鑒權的功能,正好可以對JWT有深一步的了解。
一、JWT使用場景:
1. 授權:用戶登錄后,每個請求都包含JWT,允許用戶訪問該令牌允許的路由、服務和資源。單點登錄是現在廣泛使用的JWT地一個特性,因為它開銷小,並且可以輕松地跨域使用。
2. 信息交換: 對於安全的在各方之間傳輸信息而言,JWT無疑是一種很好的方式。因為JWT可以被簽名,例如,用公鑰/私鑰對,可以確定發送人就是它們所說的那個人。另外,由於簽名是使用頭和有效負載計算的,還可以驗證內容有沒有被篡改。
使用依賴:
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>7.1</version>
</dependency>
核心代碼:
/** * 采用HS256算法生成token * * @param payLoadMap 荷載 * @return * @throws JOSEException */ public static String createTokenHS256(Map<String, Object> payLoadMap) throws JOSEException { JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256); Payload payload = new Payload(new JSONObject(payLoadMap)); JWSObject jwsObject = new JWSObject(jwsHeader, payload); log.info("header : " + jwsHeader); log.info("payload : " + payload); JWSSigner jwsSigner = new MACSigner(TokenConstants.SECRET); jwsObject.sign(jwsSigner); return jwsObject.serialize(); } /** * 解析token * * @param token * @return * @throws ParseException * @throws JOSEException */ public static Map<String, Object> parseTokenHS256(String token) throws ParseException, JOSEException { JWSObject jwsObject = JWSObject.parse(token); JWSVerifier jwsVerifier = new MACVerifier(TokenConstants.SECRET); return verify(jwsObject, jwsVerifier); } /** * 驗證token * * @param jwsObject * @param jwsVerifier * @return * @throws JOSEException */ private static Map<String, Object> verify(JWSObject jwsObject, JWSVerifier jwsVerifier) throws JOSEException { Map<String, Object> resultMap = new HashMap<>(); Payload payload = jwsObject.getPayload(); if (jwsObject.verify(jwsVerifier)) { resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_SUCCESS); JSONObject jsonObject = payload.toJSONObject(); resultMap.put(TokenConstants.DATA, jsonObject); if (jsonObject.containsKey(TokenConstants.EXPIRE_TIME)) { Long expireTime = Long.valueOf(jsonObject.get(TokenConstants.EXPIRE_TIME).toString()); Long nowTime = System.currentTimeMillis(); log.info("nowTime : " + nowTime); if (nowTime > expireTime) { resultMap.clear(); resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_EXPIRED); } } } else { resultMap.put(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED); } return resultMap; }
單元測試:
@Test public void createTokenTest() { String appId = "appId"; String appSecret = "appSecret"; Map<String, Object> map = new HashMap<>(); map.put("appId", appId); map.put("appSecret", appSecret); Long createTime = System.currentTimeMillis(); map.put("createTime", createTime); map.put("expireTime", createTime + 60 * 60 * 2); try { String token = TokenHS256Util.createTokenHS256(map); System.out.println(token); log.info("token : " + token); } catch (JOSEException e) { log.error("create token failed. "); e.printStackTrace(); } } @Test public void ValidTokenTest() { String token = "eyJhbGciOiJIUzI1NiJ9.eyJhcHBTZWNyZXQiOiJhcHBTZWNyZXQiLCJleHBpcmVUaW1lIjoxNTU3NDczMDI2OTg3LCJjcmVhdGVUaW1lIjoxNTU3NDczMDE5Nzg3LCJhcHBJZCI6ImFwcElkIn0.mNwTFBLOtc3hD90SI7gKV1YlahulOOartZFaLFbqK0Q"; if (token != null) { try { Map<String, Object> validMap = TokenHS256Util.parseTokenHS256(token); Integer result = (Integer) validMap.getOrDefault(TokenConstants.RESULT, TokenConstants.TOKEN_PARSE_FAILED); switch (result) { case TokenConstants.TOKEN_PARSE_SUCCESS: log.info("token parse success."); JSONObject jsonObject = (JSONObject) validMap.getOrDefault(TokenConstants.DATA, StringUtils.EMPTY); log.info("appId = " + jsonObject.getOrDefault("appId", StringUtils.EMPTY)); log.info("appSecret = " + jsonObject.getOrDefault("appSecret", StringUtils.EMPTY)); log.info("sta = " + jsonObject.getOrDefault("sta", StringUtils.EMPTY)); log.info("expire = " + jsonObject.getOrDefault("expire", StringUtils.EMPTY)); break; case TokenConstants.TOKEN_EXPIRED: log.error("token has expired. "); break; default: log.error("token parse failed. "); break; } } catch (ParseException | JOSEException e) { e.printStackTrace(); } } }
二、JWT組成
JWT由三部分組成,它們之間用圓點(.)連接
我們項目里的header內容固定是:
{"alg":"HS256"}
表示使用的是HS256算法,然后用base64多對這個json編碼就得到JWT的第一部分,即Header。
payload包含聲明(要求),聲明是關於實體(通常是用戶)和其他數據的聲明。聲明有三種類型:registered,public,private
registered claims: 這里有一組預定義的聲明,它們不是強制的,但是推薦。比如iss(issure), exp(expiration time), sub(subject), aud(audience)等
public claims: 可以隨意定義
private claims: 用於在同意使用它們的各方之間共享信息,並且不是注冊的或公開的聲明。
例如:
{"appSecret":"appSecret","expireTime":1558059574173,"createTime":1558059566973,"appId":"appId"}
對payload進行base64編碼就得到JWT的第二部分
注意:不要在JWT的payload或header中放置敏感信息,除非它們是加密的。
Signature:為了得到簽名部分,你必須有編碼過的header、編碼過的payload、一個密鑰,簽名算法是header中指定的那個,然后對他們簽名即可。
簽名是用於驗證消息在傳遞過程中有沒有被更改,並且對於使用私鑰簽名的token,它還可以驗證JWT的發送方是否為它所稱的發送方。
三、官網的debugger (https://jwt.io/)
(每次修改verify signature里面的內容,生成的token是會改動的,所以可以驗證消息在傳遞過程中是否有被修改)
四、JWT的工作原理
在認證時,當用戶用他們的憑證成功登陸以后,一個JSON Web Token將會被返回。此后,token就是用戶憑證了,需要非常小心以防出現安全問題。一般而言,保存令牌時不應該超過你所需要它的時間。
無論何時用戶想要訪問受保護的路由或者資源的時候,用戶代理(通常是瀏覽器)都應該帶上JWT,典型的,通常放在Authorization header中,用Bearer schema。
header 應該看起來是這樣的:
Authorization:Bearer <token>
服務器上的受保護的路由將會檢查Authorization
header中的JWT是否有效,如果有效,則用戶可以訪問受保護的資源。
如果JWT包含足夠多的必須的數據,那么就可以減少對某些操作的數據庫查詢的需要,盡管可能並不總是如此。
如果token是在授權頭(Authorization header)中發送的,那么跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。
第一步:請求授權接口,獲取授權碼token
第二步:使用授權碼token訪問受保護的資源(比如:API)
五、基於服務器端session的身份認證
HTTP協議是無狀態的,即如果我們已經認證了一個用戶,那么下一次請求時,服務器不知道來者何人,需要再次進行認證。
傳統的做法是將已經認證過的用戶信息存儲在服務器上,比如session,用戶下次請求時帶着session ID,然后服務器以此檢查用戶是否認證過。
缺陷:
1. session: 每次用戶認證通過以后,服務器需要創建一條記錄保存用戶信息,通常是在內存中,隨着認證通過的用戶越來越多,服務器在此處的開銷就越大。 2. scalability: 由於session是在內存中,擴展不靈活。 3. CORS: 當想要擴展我們的應用,讓我們的數據被多個移動設備使用時,我們必須考慮跨資源共享問題。當使用Ajax調用從另一個域名下獲取資源時,可能會遇到禁止請求的問題。 4. CSRF: 用戶很容易受到CSRF攻擊。
六、基於token的身份認證
基於token的身份認證是無狀態的,服務器或者session中不會存儲任何用戶信息,沒有會話信息意味着應用程序可以根據需要擴展和添加更多的機器,而不必擔心用戶登錄的位置。
主要流程:
1. 用戶攜帶用戶名、密碼請求訪問 2. 服務器校驗用戶憑證 3. 應用提供一個token給客戶端 4. 客戶端存儲token,並且在隨后的每一次請求中都帶着它 5. 服務器校驗token並返回數據
注意:每一次請求都要token;token應該放在請求header中,我們還需要將服務器設置為接受來自所有域的請求,用Access-Control-Allow-Origin:*
使用token的優勢: 1. 無狀態和可擴展性 2. 安全,token不是cookie,每次請求的時候token都會被發送,而且由於沒有cookie被發送,還有助於防止CSRF攻擊。 即使在你的實現中將token存儲在客戶端的cookie中,這個cookie也只是一種存儲機制,而非身份認證機制,沒有基於會話的操作。 token在一段時間以后會過期,這個時候用戶需要重新登錄,這有助於我們保持安全。還有一個概念叫token撤銷,它允許我們根據相同的授權許可使特定的token甚至一組token無效。