【JAVA】基於Token的用戶驗證

背景


傳統的用戶驗證是基於session自身的特性實現,當用戶提交登陸請求,後臺驗證通過後,會在session中留下用戶的信息,用於識別當前用戶在客戶端登陸了。通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大。因爲認證的記錄是保存在內存中,意味着用戶下次請求還必須要請求在這臺服務器上,這樣才能拿到授權的資源,這樣在分佈式的應用上,相應的限制了負載均衡器的能力。這也意味着限制了應用的擴展能力。

一、基於Token的用戶驗證


基於token的鑑權機制類似於http協議也是無狀態的,它不需要在服務端去保留用戶的認證信息或者會話信息。這就意味着基於token認證機制的應用不需要去考慮用戶在哪一臺服務器登錄了,這就爲應用的擴展提供了便利。

常見驗證流程:

用戶提交用戶名、密碼到服務器後臺
後臺驗證用戶信息的正確性
若用戶驗證通過,服務器端生成Token,返回到客戶端
客戶端保存Token,再下一次請求資源時,附帶上Token信息
服務器端(一般在攔截器中進行攔截)驗證Token是否由服務器簽發的
若Token驗證通過,則返回需要的資源


二、JSON Web Token(JWT)


JWT是一種通用的規範,它定義了Token的生成方式。

2.1 JWT的格式
一個完整的Token的形式:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1bmlzaW1zIiwicGVyaW9kIjo2MDAwMCwiZXhwIjoxNTMxODI5NzI5LCJ1c2VySWQiOiIwZjg3LTQxYzI2ZGExMzUxNjA2MDhkNTYtIiwiaWF0IjoxNTMxODI5NjA5fQ.RZCUmxfaDgPxhocCkomSDcUOwLNYUW3Hgu-ufi0mJZNlurGSQHex0CokiUqRTfhQo0G8VJuYDzjeUklHN2pAdA

由“.”符號拼接,共有三部分信息組成。

JWT由三部分組成:Header、Payload、Signature。

JWT:{
    header:{ // 頭部
        alg:'HS256' // 算法聲明
    },
    payload:{ // 數據
        exp:'1532180906', // 過期時間
        userId:'xxxx',
        iss:'xxx'
    },
    signature:'' // 簽名
}


2.2 Header(頭部)
jwt的頭部承載兩部分信息:

聲明類型,這裏是jwt
聲明加密的算法 通常直接使用 HMAC SHA256
2.3 Payload(數據)
這部分包含了一些通用的信息,以及用戶自定義的信息。通常在做用戶驗證時,會放置userId等信息。 
通用信息組成(不強制全部使用):

iss: jwt簽發者
sub: jwt所面向的用戶
aud: 接收jwt的一方
exp: jwt的過期時間,這個過期時間必須要大於簽發時間
nbf: 定義在什麼時間之前,該jwt都是不可用的.
iat: jwt的簽發時間
jti: jwt的唯一身份標識,主要用來作爲一次性token,從而回避重放攻擊。
2.4 Signature(簽證)
這部分信息是一個簽證信息,由算法進行加密,它由三部分信息組成:

Header(base64編碼後的字符串)
Payload(base64編碼後的字符串)
Secret
簽證信息的生成方式:

將編碼後的Header、Payload由“.”符號進行拼接,將拼接後的字符串使用加密算法進行加密

加密算法的選擇:儘量選擇哈希散列解密方式,避免被容易的暴力破解。 
備註:密鑰需要妥善保存,這是驗證Token有效性的唯一標識。

2.5 Token的生成方式
{{base64Encode(Header)}}.{{base64Encode(Payload)}}.{{Signature}}

將編碼後的Header、編碼後的Payload以及Signature由符號“.”進行拼接,形成最終的Token。

三、JWT的實現


JWT提供了Token的生成規範,沒有現成的JAR包可以引用。可以自己手動實現,也可以使用開源項目。推薦一個開源的JWT實現,使用起來挺方便的。Java版本的JWT實現3.1 生成一個Token

private Key getKey(){
        if(ObjectUtils.isEmpty(key)){
            key = MacProvider.generateKey();
        }
        return key;
    }
@Override
    public String generatorToken(String userId){
        Key key = getKey();

        Long currentTime = System.currentTimeMillis();
        Long activeTime = 30 * 60000L; //

        Map<String, Object> claims = new HashMap<>();
        claims.put(USERID, userId);
        claims.put(PERIOD, activeTime / 2); // 有效時間

        String compactJws = Jwts.builder()
                .setClaims(claims)  // 自定義數據
                .setSubject("unisims")  //  
                .setIssuedAt(new Date(currentTime)) // 簽發時間
                .setExpiration(new Date(currentTime + activeTime)) // 超時時間
                .signWith(SignatureAlgorithm.HS512, key) // 簽名
                .compact();

        return compactJws;
    }   


3.2 驗證Token

@Override
    public JWTCheckResult validateToken(String token) {
        JWTCheckResult result = new JWTCheckResult();

        if(StringUtils.isEmpty(token)){
            result.setStatus(JWTEnum.EMPTY.getStatus());
            result.setMessage(JWTEnum.EMPTY.getMessage());
            return result;
        }

        try {
            Key key = getKey();
            Jws<Claims> jwt = Jwts.parser().setSigningKey(key).parseClaimsJws(token);

        } catch (SignatureException e) {
            result.setStatus(JWTEnum.ERROR.getStatus());
            result.setMessage(JWTEnum.ERROR.getMessage());

            System.out.println(e);
        }catch(ExpiredJwtException e){
            result.setStatus(JWTEnum.TIMEOUT.getStatus());
            result.setMessage(JWTEnum.TIMEOUT.getMessage());

            System.out.println(e);
        }catch(Exception e){
            result.setStatus(JWTEnum.ERROR.getStatus());
            result.setMessage(JWTEnum.ERROR.getMessage());
            System.out.println(e);
        }
        return result;
    }


四、Token在用戶驗證中真正的使用


第三節中講了Token的生成與簡單驗證,但是,在項目中真正真正使用時,僅有這些是不夠的。

4.1 Token的第一次簽發
當服務端收到登陸請求,需要在後臺驗證賬戶信息。若是驗證通過,則生成Token,除了固定的信息外,還會放置一些與用戶相關的信息,通常就是放置用戶的ID。
將生成的Token返回前端,一般是將Token放置在response的header中。同時,瀏覽器在下一次請求也是將Token放置在請求頭中。
4.2 Token的驗證
客戶端請求資源時,需要對客戶端的用戶進行驗證。有些請求是不需要的驗證用戶(比如登陸請求,不需要驗證用戶,此時本就沒有用戶登陸),有些資源是需要驗證的,所以需要對URL進行區分。

Teken的驗證我選擇在攔截器中實現(HandlerInterceptor),在這裏對URL進行攔截。首先判斷是否是需要驗證的URL,然後對Token進行驗證。對於Token的驗證分爲成功驗證、無效驗證、超時驗證、刷新處理、主動失效處理。

Token驗證方式的選擇:

第一種驗證方式:利用session 
在服務器端,生成Token的同時將Token存入session。當收到客戶端上傳的Token時,將它與session中的進行比對,一致則合法,驗證通過。否則,返回驗證失敗,前端跳轉到登陸頁面。

第二種驗證方式:算法驗證 
當收到客戶端上傳的Token時,對Token的前兩部分進行加密,比對加密結果是否與第三部分相同,相同則驗證通過。否則,返回驗證失敗,前端跳轉到登陸頁面。

備註:爲了項目的擴展性考慮,採用第二種方式進行驗證。第一種方式依賴於Session,做負載均衡時,當請求被轉發到另一臺服務器時,由於Session中沒有Token信息,會造成成驗證不通過。

成功驗證: 
保證Token是正確的有兩個因素:第一,這個Token能夠正確被Token識別,即這個Token的確是由自身的後臺簽發的;第二,這個Toen沒有過期。

一般的,只要這個能夠被後臺使用自身的密鑰+算法正確解密,即可認爲這個Token是由自身簽發的。

Token的失效:

一般都會爲Token添加有效時間區間,即在某個時間區間這個Token是有效的,過了這個時間點後,即認爲這是一個不可信任的Token。

上文講述到的那個開源項目中,已經做好了無效、超時Token驗證的接口,不需要我們手動去實現。

無效驗證: 
不能被正確解析的Token、以及超時的Token,既是驗證不通過。

超時驗證: 
默認每一個Token都會爲它添加有效時間,當超過生效時間,則認爲此Token驗不通過。 
一般的,在生成Token時,會在Payload中設置過期時間,在攔截器中,根據這個時間驗證是否超時。

上面提到 開源項目中已經自動處理的超時的處理,不需要認爲的再次處理,僅僅只需要設置一個過期時間即可。

刷新處理: 
因爲業務的需要,不能要求每次Token超時後,用戶再次登陸。所以,需要在用戶無感知情況下自動刷新Token,避免用戶再次發起登陸請求。

第一種方式:通過算法計算 
將過期週期認爲是2T,當在T-2T時間區間時,認爲此時需要刷新Token。當此Token驗證通過,則在後臺生成新的Token,隨着header返回到前端,前端自己去刷新Token。

這種方式有個缺點,即只要不是超時的Token_A,依然可以正常的請求後臺資源。即,假如在T-2T時間區間刷新Token,產生了新的Token_B,此時Token_A依然是有效的Token。

第二種方式:配合內存、數據庫刷新Token 
預先將生成的Token存儲在內存、或數據庫中,在攔截器中驗證時,多加一個驗證,即前端上報的Token必須與內存或數據庫中Token保持一致,否則驗證不通過。

備註:第一種方式雖然會造成舊的Token可以一直使用,但是有利於分佈式部署。另外存在數據庫可是也可以,對分佈式沒有影響(使用同一個數據庫的話)。但是當存儲在內存當中(比如Session)時,對分佈式就不太友好了。

主動失效: 
一般管理系統都會有用戶註銷功能,這時,需要主動讓Token失效。這時需要配合內存、數據庫實現,即在內存、數據庫中保存 一份最新的、有效的Token,當註銷時,將存儲的Token清空。同時在攔截器驗證時,發現內存、數據庫的Token是空值時,則驗證不通過。
--------------------- 
作者:萌太隆 
來源:CSDN 
原文:https://blog.csdn.net/swl979623074/article/details/81150184 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章