溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

發(fā)布時(shí)間:2020-07-25 17:10:18 來(lái)源:網(wǎng)絡(luò) 閱讀:2839 作者:ZeroOne01 欄目:編程語(yǔ)言

[TOC]


有狀態(tài) VS 無(wú)狀態(tài)

幾乎絕大部分的應(yīng)用都需要實(shí)現(xiàn)認(rèn)證與授權(quán),例如用戶(hù)使用賬戶(hù)密碼登錄就是一個(gè)認(rèn)證過(guò)程,認(rèn)證登錄成功后系統(tǒng)才會(huì)允許用戶(hù)訪問(wèn)其賬戶(hù)下的相關(guān)資源,這就是所謂的授權(quán)。而復(fù)雜點(diǎn)的情況就是用戶(hù)會(huì)有角色概念,每個(gè)角色所擁有的權(quán)限不同,給用戶(hù)賦予某個(gè)角色的過(guò)程也是一個(gè)授權(quán)過(guò)程。

用戶(hù)的登錄態(tài)在服務(wù)器端分為有狀態(tài)和無(wú)狀態(tài)兩種模式,在單體分布式架構(gòu)的時(shí)代,我們?yōu)榱四茏孲ession信息在多個(gè)Tomcat實(shí)例之間共享,通常的解決方案是將Session存儲(chǔ)至一個(gè)緩存數(shù)據(jù)庫(kù)中。即下圖中的Session Store,這個(gè)Session Store可以是Redis也可以是MemCache,這種模式就是有狀態(tài)的:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

之所以說(shuō)是有狀態(tài),是因?yàn)榉?wù)端需要維護(hù)、存儲(chǔ)這個(gè)Session信息,即用戶(hù)的登錄態(tài)實(shí)際是在服務(wù)端維護(hù)的,所以對(duì)服務(wù)端來(lái)說(shuō)可以隨時(shí)得知用戶(hù)的登錄態(tài),并且對(duì)用戶(hù)的Session有比較高的控制權(quán)。有狀態(tài)模式的缺點(diǎn)主要是在于這個(gè)Session Store上,如果作為Session Store的服務(wù)只有一個(gè)節(jié)點(diǎn)的話,當(dāng)業(yè)務(wù)擴(kuò)展、用戶(hù)量增多時(shí)就會(huì)有性能瓶頸問(wèn)題,而且數(shù)據(jù)遷移也比較麻煩。當(dāng)然也可以選擇去增加節(jié)點(diǎn),只不過(guò)就需要投入相應(yīng)的機(jī)器成本了。

另一種無(wú)狀態(tài)模式,指的是服務(wù)器端不去記錄用戶(hù)的登錄狀態(tài),也就是服務(wù)器端不再去維護(hù)一個(gè)Session。而是在用戶(hù)登錄成功的時(shí)候,頒發(fā)一個(gè)token給客戶(hù)端,之后客戶(hù)端的每個(gè)請(qǐng)求都需要攜帶token。服務(wù)端會(huì)對(duì)客戶(hù)端請(qǐng)求時(shí)所攜帶的token進(jìn)行解密,校驗(yàn)token是否合法以及是否已過(guò)期等等。token校驗(yàn)成功后則認(rèn)為用戶(hù)是具有登錄態(tài)的,否則認(rèn)為用戶(hù)未登錄:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

注:token通常會(huì)存儲(chǔ)用戶(hù)的唯一ID,解密token就是為了獲取用戶(hù)ID然后去緩存或者數(shù)據(jù)庫(kù)中查詢(xún)用戶(hù)數(shù)據(jù)。當(dāng)然也可以選擇將用戶(hù)數(shù)據(jù)都保存在token中,只不過(guò)這種方式可能會(huì)有安全問(wèn)題或數(shù)據(jù)一致性問(wèn)題

無(wú)狀態(tài)模式下的token其實(shí)和有狀態(tài)模式下的session作用是類(lèi)似的,都是判斷用戶(hù)是否具有登錄態(tài)的一個(gè)憑證。只不過(guò)在無(wú)狀態(tài)模式下,服務(wù)器端不需要再去維護(hù)、存儲(chǔ)一個(gè)Session,只需要對(duì)客戶(hù)端攜帶的token進(jìn)行解密和校驗(yàn)。也就是說(shuō)存儲(chǔ)實(shí)際是交給了客戶(hù)端完成,所以無(wú)狀態(tài)的優(yōu)點(diǎn)恰恰就是彌補(bǔ)了有狀態(tài)的缺點(diǎn)。但是無(wú)狀態(tài)的缺點(diǎn)也很明顯,因?yàn)橐坏┌裻oken交給客戶(hù)端后,服務(wù)端就無(wú)法去控制這個(gè)token了。例如想要強(qiáng)制下線某個(gè)用戶(hù)在無(wú)狀態(tài)的模式下就比較難以實(shí)現(xiàn)。

有狀態(tài)與無(wú)狀態(tài)各有優(yōu)缺點(diǎn),只不過(guò)目前業(yè)界趨勢(shì)更傾向于無(wú)狀態(tài):

優(yōu)缺點(diǎn) 有狀態(tài) 無(wú)狀態(tài)
優(yōu)點(diǎn) 服務(wù)端控制能力強(qiáng) 去中心化,無(wú)存儲(chǔ),簡(jiǎn)單,任意擴(kuò)容、縮容
缺點(diǎn) 存在中心點(diǎn),雞蛋放在一個(gè)籃子里,遷移麻煩。服務(wù)端存儲(chǔ)數(shù)據(jù),加大了服務(wù)端壓力 服務(wù)端控制能力相對(duì)弱

微服務(wù)認(rèn)證方案

微服務(wù)認(rèn)證方案有很多種,需要根據(jù)實(shí)際的業(yè)務(wù)需求定制適合自己業(yè)務(wù)的方案,這里簡(jiǎn)單列舉一下業(yè)界內(nèi)常用的微服務(wù)認(rèn)證方案。

1、“處處安全” 方案:

所謂“處處安全” 方案,就是考慮了微服務(wù)認(rèn)證中的方方面面,這種方案主流是使用OAuth3協(xié)議進(jìn)行實(shí)現(xiàn)。這種方案的優(yōu)點(diǎn)是安全性好,但是實(shí)現(xiàn)的成本及復(fù)雜性比較高。另外,多個(gè)微服務(wù)之間互相調(diào)用需要傳遞token,所以會(huì)發(fā)生多次認(rèn)證,有一定的性能開(kāi)銷(xiāo)

OAuth3的代表實(shí)現(xiàn)框架:

  • Spring Cloud Security
  • Jboss Keycloak

參考文章:

  • OAuth3實(shí)現(xiàn)單點(diǎn)登錄SSO
  • OAuth 2.0系列教程

2、外部無(wú)狀態(tài),內(nèi)部有狀態(tài)方案:

這種方案雖然看著有些奇葩,但是也許多公司在使用。在該方案下,網(wǎng)關(guān)不存儲(chǔ)Session,而是接收一個(gè)token和JSESSIONID,網(wǎng)關(guān)僅對(duì)token進(jìn)行解密、校驗(yàn),然后將JSESSIONID轉(zhuǎn)發(fā)到其代理的微服務(wù)上,這些微服務(wù)則是通過(guò)JSESSIONID從Session Store獲取共享Session。如下圖:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

這種方案主要是出現(xiàn)在內(nèi)部有舊的系統(tǒng)架構(gòu)的情況,在不重構(gòu)或者沒(méi)法全部重構(gòu)的前提下為了兼容舊的系統(tǒng),就可以采用該方案。而且也可以將新舊系統(tǒng)分為兩塊,網(wǎng)關(guān)將token和JSESSIONID一并轉(zhuǎn)發(fā)到下游服務(wù),這樣無(wú)狀態(tài)模式的系統(tǒng)則使用token,有狀態(tài)模式的系統(tǒng)則使用Session,然后再慢慢地將舊服務(wù)進(jìn)行重構(gòu)以此實(shí)現(xiàn)一個(gè)平滑過(guò)渡。如下圖:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

3、“網(wǎng)關(guān)認(rèn)證授權(quán),內(nèi)部裸奔” 方案:

在該方案下,認(rèn)證授權(quán)在網(wǎng)關(guān)完成,下游的微服務(wù)不需要進(jìn)行認(rèn)證授權(quán)。網(wǎng)關(guān)接收到客戶(hù)端請(qǐng)求所攜帶的token后,對(duì)該token進(jìn)行解密和校驗(yàn),然后將解密出來(lái)的用戶(hù)信息轉(zhuǎn)發(fā)給下游微服務(wù)。這種方案的優(yōu)點(diǎn)是實(shí)現(xiàn)簡(jiǎn)單、性能也好,缺點(diǎn)是一旦網(wǎng)關(guān)被攻破,或者能越過(guò)網(wǎng)關(guān)訪問(wèn)微服務(wù)就會(huì)有安全問(wèn)題。如下圖:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

4、“內(nèi)部裸奔” 改進(jìn)方案:

上一個(gè)方案的缺陷比較明顯,我們可以對(duì)該方案進(jìn)行一些改進(jìn),例如引入一個(gè)認(rèn)證授權(quán)中心服務(wù),讓網(wǎng)關(guān)不再做認(rèn)證和授權(quán)以及token的解密和解析。用戶(hù)的登錄請(qǐng)求通過(guò)網(wǎng)關(guān)轉(zhuǎn)發(fā)到認(rèn)證授權(quán)中心完成登錄,登錄成功后由認(rèn)證授權(quán)中心頒發(fā)token給客戶(hù)端。客戶(hù)端每次請(qǐng)求都攜帶token,而每個(gè)微服務(wù)都需要對(duì)token進(jìn)行解密和解析,以確定用戶(hù)的登錄態(tài)。改進(jìn)之后所帶來(lái)的好處就是網(wǎng)關(guān)不再關(guān)注業(yè)務(wù),而是單純的請(qǐng)求做轉(zhuǎn)發(fā),可以在一定程度上解耦業(yè)務(wù),并且也更加安全,因?yàn)槊總€(gè)微服務(wù)不再裸奔而是都需要驗(yàn)證請(qǐng)求中所攜帶的token。如下圖:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

5、方案的對(duì)比與選擇:

以上所提到的常見(jiàn)方案只是用于拋磚引玉,沒(méi)有哪個(gè)方案是絕對(duì)普適的。而且實(shí)際開(kāi)發(fā)中通常會(huì)根據(jù)業(yè)務(wù)改進(jìn)、組合這些方案演變出不同的變種,所以應(yīng)該要學(xué)會(huì)活學(xué)活用而不是局限于某一種方案。下面簡(jiǎn)單整理了一下這幾種方案,以便做對(duì)比:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

6、訪問(wèn)控制模型

了解了常見(jiàn)的微服務(wù)認(rèn)證方案后,我們來(lái)簡(jiǎn)單看下訪問(wèn)控制模型。所謂訪問(wèn)控制,就是用戶(hù)需要滿(mǎn)足怎么樣的條件才允許訪問(wèn)某個(gè)系統(tǒng)資源,即控制系統(tǒng)資源的訪問(wèn)權(quán)限。訪問(wèn)控制模型主要有以下幾種:

  1. Access Control List(ACL,訪問(wèn)控制列表):

    在該模型下的一個(gè)系統(tǒng)資源會(huì)包含一組權(quán)限列表,該列表規(guī)定了哪些用戶(hù)擁有哪些操作權(quán)限。例如有一個(gè)系統(tǒng)資源包含的權(quán)限列表為:[Alice: read, write; Bob: read];那么就表示Alice這個(gè)用戶(hù)對(duì)該資源擁有read和write權(quán)限,而B(niǎo)ob這個(gè)用戶(hù)則對(duì)該資源擁有read權(quán)限。該模型通常用于文件系統(tǒng)

  2. Role-based access control(RBAC,基于角色的訪問(wèn)控制):

    即用戶(hù)需關(guān)聯(lián)一個(gè)預(yù)先定義的角色,而不同的角色擁有各自的權(quán)限列表。用戶(hù)登錄后只需要查詢(xún)其關(guān)聯(lián)的角色就能查出該用戶(hù)擁有哪些權(quán)限。例如用戶(hù)A關(guān)聯(lián)了一個(gè)名為觀察者的角色,該角色下包含接口A和接口B的訪問(wèn)權(quán)限,那么就表示用戶(hù)A僅能夠訪問(wèn)A和接口B。該模型在業(yè)務(wù)系統(tǒng)中使用得最多

  3. Attribute-based access control(ABAC,基于屬性的訪問(wèn)控制):

    在該模型下,用戶(hù)在訪問(wèn)某個(gè)系統(tǒng)資源時(shí)會(huì)攜帶一組屬性值包括自身屬性、主題屬性、資源屬性以及環(huán)境屬性等。然后系統(tǒng)通過(guò)動(dòng)態(tài)計(jì)算用戶(hù)所攜帶的屬性來(lái)判斷是否滿(mǎn)足具有訪問(wèn)某個(gè)資源的權(quán)限。屬性通常來(lái)說(shuō)分為四類(lèi):用戶(hù)屬性(如用戶(hù)年齡),環(huán)境屬性(如當(dāng)前時(shí)間),操作屬性(如讀取)以及對(duì)象屬性等。

    為了能讓系統(tǒng)進(jìn)行權(quán)限控制,在該模型下需要以特定的格式定義權(quán)限規(guī)則,例如:IF 用戶(hù)是管理員; THEN 允許對(duì)敏感數(shù)據(jù)進(jìn)行讀/寫(xiě)操作。在這條規(guī)則中“管理員”是用戶(hù)的角色屬性,而“讀/寫(xiě)”是操作屬性,”敏感數(shù)據(jù)“則是對(duì)象屬性。

    ABAC有時(shí)也被稱(chēng)為PBAC(Policy-Based Access Control,基于策略的訪問(wèn)控制)或CBAC(Claims-Based Access Control,基于聲明的訪問(wèn)控制)。該模型由于比較復(fù)雜,使用得不多,k8s也因?yàn)锳BAC太復(fù)雜而在1.8版本改為使用RBAC模型

  4. Rules-based access control(RBAC,基于規(guī)則的訪問(wèn)控制):

    在該模型下通過(guò)對(duì)某個(gè)系統(tǒng)資源事先定義一組訪問(wèn)規(guī)則來(lái)實(shí)現(xiàn)訪問(wèn)控制,這些規(guī)則可以是參數(shù)、時(shí)間、用戶(hù)信息等。例如:只允許從特定的IP地址訪問(wèn)或拒絕從特定的IP地址訪問(wèn)

  5. Time-based access control list(TBACL,基于時(shí)間的訪問(wèn)控制列表):

    該模型是在ACL的基礎(chǔ)上添加了時(shí)間的概念,可以設(shè)置ACL權(quán)限在特定的時(shí)間才生效。例如:只允許某個(gè)系統(tǒng)資源在工作日時(shí)間內(nèi)才能被外部訪問(wèn),那么就可以將該資源的ACL權(quán)限的有效時(shí)間設(shè)置為工作日時(shí)間內(nèi)


JWT

之前提到過(guò)無(wú)狀態(tài)模式下,服務(wù)器端需要生成一個(gè)Token頒發(fā)給客戶(hù)端,而目前主流的方式就是使用JWT的標(biāo)準(zhǔn)來(lái)生成Token,所以本小節(jié)我們來(lái)簡(jiǎn)單了解下JWT及其使用。

JWT簡(jiǎn)介:

JWT是JSON Web Token的縮寫(xiě),JWT實(shí)際是一個(gè)開(kāi)放標(biāo)準(zhǔn)(RFC 7519),用來(lái)在各方之間安全地傳輸信息,是目前最流行的跨域認(rèn)證解決方案。JWT可以被驗(yàn)證和信任,因?yàn)樗菙?shù)字簽名的。官網(wǎng):https://jwt.io/

JWT的組成結(jié)構(gòu):

組成 作用 內(nèi)容示例
Header(頭) 記錄Token類(lèi)型、簽名的算法等 {"alg": "HS256", "type": "JWT"}
Payload(有效載荷) 攜帶一些用戶(hù)信息及Token的過(guò)期時(shí)間等 {"user_id": "1", "iat": 1566284273, "exp": 1567493873}
Signature(簽名) 簽名算法生成的數(shù)字簽名,用于防止Token被篡改、確保Token的安全性 WV5Hhymti3OgIjPprLJKJv3aY473vyxMLeM8c7JLxSk

JWT生成Token的公式:

Token = Base64(Header).Base64(Payload).Base64(Signature)

示例:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjYyODIyMjMsImV4cCI6MTU2NzQ5MTgyM30.OtCOFqWMS6ZOzmwCs7NC7hs9u043P-09KbQfZBov97E

簽名是使用Header里指定的簽名算法生成的,公式如下:

Signature = 簽名算法((Base64(Header).Base64(Payload), 秘鑰))


使用JWT:

1、目前Java語(yǔ)言有好幾個(gè)操作JWT的第三方庫(kù),這里采用其中較為輕巧的jjwt作為演示。首先添加依賴(lài)如下:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-api</artifactId>
  <version>0.10.7</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-impl</artifactId>
  <version>0.10.7</version>
  <scope>runtime</scope>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt-jackson</artifactId>
  <version>0.10.7</version>
  <scope>runtime</scope>
</dependency>

2、編寫(xiě)一個(gè)工具類(lèi),將JWT的操作都抽取出來(lái),方便在項(xiàng)目中的使用。具體代碼如下:

package com.zj.node.usercenter.util;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;

/**
 * JWT 工具類(lèi)
 *
 * @author 01
 * @date 2019-08-20
 **/
@Slf4j
@Component
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
public class JwtOperator {
    /**
     * 秘鑰
     * - 默認(rèn)5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ
     */
    @Value("${jwt.secret:5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ}")
    private String secret;
    /**
     * 有效期,單位秒
     * - 默認(rèn)2周
     */
    @Value("${jwt.expire-time-in-second:1209600}")
    private Long expirationTimeInSecond;

    /**
     * 從token中獲取claim
     *
     * @param token token
     * @return claim
     */
    public Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(this.secret.getBytes())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException |
                MalformedJwtException | IllegalArgumentException e) {
            log.error("token解析錯(cuò)誤", e);
            throw new IllegalArgumentException("Token invalided.");
        }
    }

    /**
     * 獲取token的過(guò)期時(shí)間
     *
     * @param token token
     * @return 過(guò)期時(shí)間
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token)
                .getExpiration();
    }

    /**
     * 判斷token是否過(guò)期
     *
     * @param token token
     * @return 已過(guò)期返回true,未過(guò)期返回false
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 計(jì)算token的過(guò)期時(shí)間
     *
     * @return 過(guò)期時(shí)間
     */
    private Date getExpirationTime() {
        return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
    }

    /**
     * 為指定用戶(hù)生成token
     *
     * @param claims 用戶(hù)信息
     * @return token
     */
    public String generateToken(Map<String, Object> claims) {
        Date createdTime = new Date();
        Date expirationTime = this.getExpirationTime();

        byte[] keyBytes = secret.getBytes();
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)
                // 你也可以改用你喜歡的算法
                // 支持的算法詳見(jiàn):https://github.com/jwtk/jjwt#features
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    /**
     * 判斷token是否非法
     *
     * @param token token
     * @return 未過(guò)期返回true,否則返回false
     */
    public Boolean validateToken(String token) {
        return !isTokenExpired(token);
    }
}

3、若默認(rèn)的配置不符合需求,可以通過(guò)在配置文件中添加如下配置進(jìn)行自定義:

jwt:
  # 秘鑰
  secret: 5d1IB9SiWd5tjBx&EMi^031CtigL!6jJ
  # jwt有效期,單位秒
  expire-time-in-second: 1209600

4、完成以上步驟后,就可以在項(xiàng)目中使用JWT了,這里提供了一個(gè)比較全面的測(cè)試用例,可以參考測(cè)試用例來(lái)使用該工具類(lèi)。代碼如下:

package com.zj.node.usercenter.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.security.SignatureException;
import org.apache.tomcat.util.codec.binary.Base64;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.HashMap;
import java.util.Map;

/**
 * JwtOperator 測(cè)試用例
 *
 * @author 01
 * @date 2019-08-20
 **/
@SpringBootTest
@RunWith(SpringRunner.class)
public class JwtOperatorTests {

    @Autowired
    private JwtOperator jwtOperator;

    private String token = "";

    @Before
    public void generateTokenTest() {
        // 設(shè)置用戶(hù)信息
        Map<String, Object> objectObjectHashMap = new HashMap<>();
        objectObjectHashMap.put("id", "1");

        // 測(cè)試1: 生成token
        this.token = jwtOperator.generateToken(objectObjectHashMap);
        // 會(huì)生成類(lèi)似該字符串的內(nèi)容: eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE1NjU1ODk4MTcsImV4cCI6MTU2Njc5OTQxN30.27_QgdtTg4SUgxidW6ALHFsZPgMtjCQ4ZYTRmZroKCQ
        System.out.println(this.token);
    }

    @Test
    public void validateTokenTest() {
        // 測(cè)試2: 如果能token合法且未過(guò)期,返回true
        Boolean validateToken = jwtOperator.validateToken(this.token);
        System.out.println("token校驗(yàn)結(jié)果:" + validateToken);
    }

    @Test
    public void getClaimsFromTokenTest() {
        // 測(cè)試3: 解密token,獲取用戶(hù)信息
        Claims claims = jwtOperator.getClaimsFromToken(this.token);
        System.out.println(claims);
    }

    @Test
    public void decodeHeaderTest() {
        // 獲取Header,即token的第一段(以.為邊界)
        String[] split = this.token.split("\\.");
        String encodedHeader = split[0];

        // 測(cè)試4: 解密Header
        byte[] header = Base64.decodeBase64(encodedHeader.getBytes());
        System.out.println(new String(header));
    }

    @Test
    public void decodePayloadTest() {
        // 獲取Payload,即token的第二段(以.為邊界)
        String[] split = this.token.split("\\.");
        String encodedPayload = split[1];

        // 測(cè)試5: 解密Payload
        byte[] payload = Base64.decodeBase64(encodedPayload.getBytes());
        System.out.println(new String(payload));
    }

    @Test(expected = SignatureException.class)
    public void validateErrorTokenTest() {
        try {
            // 測(cè)試6: 篡改原本的token,因此會(huì)報(bào)異常,說(shuō)明JWT是安全的
            jwtOperator.validateToken(this.token + "xx");
        } catch (SignatureException e) {
            e.printStackTrace();
            throw e;
        }
    }
}

若希望了解各類(lèi)的JWT庫(kù),可以參考如下文章:

  • 各類(lèi)JWT庫(kù)(java)的使用與評(píng)價(jià)

使用JWT實(shí)現(xiàn)認(rèn)證授權(quán)

了解了JWT后,我們來(lái)使用JWT實(shí)現(xiàn)一個(gè)認(rèn)證授權(quán)Demo,首先定義一個(gè)DTO,其結(jié)構(gòu)如下:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRespDTO {
    /**
     * 昵稱(chēng)
     */
    private String userName;

    /**
     * token
     */
    private String token;

    /**
     * 過(guò)期時(shí)間
     */
    private Long expirationTime;
}

然后編寫(xiě)Service,提供模擬登錄和模擬檢查用戶(hù)登錄態(tài)的方法。具體代碼如下:

@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final JwtOperator jwtOperator;

    /**
     * 模擬用戶(hù)登錄
     */
    public LoginRespDTO login(String userName, String password) {
        String defPassword = "123456";
        if (!defPassword.equals(password)) {
            return null;
        }

        // 密碼驗(yàn)證通過(guò)頒發(fā)token
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("userName", userName);
        String token = jwtOperator.generateToken(userInfo);

        return LoginRespDTO.builder()
                .userName(userName)
                .token(token)
                .expirationTime(jwtOperator.getExpirationDateFromToken(token).getTime())
                .build();
    }

    /**
     * 模擬登錄態(tài)驗(yàn)證
     */
    public String checkLoginState(String token) {
        if (jwtOperator.validateToken(token)) {
            Claims claims = jwtOperator.getClaimsFromToken(token);
            String userName = claims.get("userName").toString();

            return String.format("用戶(hù) %s 的登錄態(tài)驗(yàn)證通過(guò),允許訪問(wèn)", userName);
        }

        return "登錄態(tài)驗(yàn)證失敗,token無(wú)效或過(guò)期";
    }
}

接著是Controller層,開(kāi)放相應(yīng)的Web接口。代碼如下:

@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping("/login")
    public LoginRespDTO login(@RequestParam("userName") String userName,
                              @RequestParam("password") String password) {
        return userService.login(userName, password);
    }

    @GetMapping("/checkLoginState")
    public String checkLoginState(@RequestParam("token") String token) {
        return userService.checkLoginState(token);
    }
}

用戶(hù)登錄成功,返回Token和用戶(hù)基本信息:
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

校驗(yàn)登錄態(tài):
微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(上)

Tips:

本小節(jié)只是給出了一個(gè)極簡(jiǎn)的例子,目的是演示如何使用JWT實(shí)現(xiàn)用戶(hù)登錄成功后頒發(fā)Token給客戶(hù)端以及通過(guò)Token驗(yàn)證用戶(hù)的登錄態(tài),這樣大家完全可以通過(guò)之前提到過(guò)的方案進(jìn)行拓展。通常來(lái)說(shuō)Token頒發(fā)給客戶(hù)端后,客戶(hù)端在后續(xù)的請(qǐng)求中是將Token放在HTTP Header里進(jìn)行傳遞的,而不是示例中的參數(shù)傳遞。微服務(wù)之間的Token傳遞也是如此,一個(gè)微服務(wù)在向另一個(gè)微服務(wù)發(fā)請(qǐng)求之前,需要先將Token放進(jìn)本次請(qǐng)求的HTTP Header里。另外,驗(yàn)證Token的邏輯一般是放在一個(gè)全局的過(guò)濾器或者攔截器中,這樣就不需要每個(gè)接口都寫(xiě)一遍驗(yàn)證邏輯。


后續(xù):

  • 微服務(wù)的用戶(hù)認(rèn)證與授權(quán)雜談(下)
向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI