天天看點

jwt入門

JWT(JSON Web Token)是一個非常輕巧的規範,這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊,通常使用在HTTP通信過程中進行身份認證。

我們知道,HTTP通信是無狀态的,用戶端的請求到了伺服器處理完之後是無法傳回給原來的用戶端的,是以需要對通路的用戶端進行識别,常用的做法是通過Session機制:用戶端在服務端登入成功後,服務端會生成一個sessionId傳回給用戶端,然後用戶端會把這個sessionId存到本地浏覽器的Cookie中,當用戶端再向服務端發起請求就會從Cookie中取出這個sessionId并附加到請求頭中。這樣服務端就能夠知道是哪個使用者發起的請求。

Session存在的問題

1.Session是儲存在服務端的,當客戶通路量增加的時候,服務端就需要存儲大量的Session會話,對響應性能造成影響。

2.當伺服器為叢集的時候,使用者登入其中的一台伺服器,會将Session儲存到該伺服器的記憶體中,但是當使用者通路到其他伺服器的時候,因為在該伺服器的記憶體中找不到該Session,就會重新生成一個Session傳回給用戶端,造成分布式Session共享問題。通常會采用緩存一緻性的技術來解決分布式Session的共享問題,或者是使用第三方緩存(比如Redis)來儲存Session,其實也可以使用JWT來解決這個問題,也就是使用JWT替代Session。

JWT的産生與使用步驟

1.用戶端通過使用者名和密碼登入伺服器。

2.服務端對用戶端進行身份驗證。

3.服務端對該使用者生成Token,傳回給用戶端。

4.用戶端發起請求,需要攜帶該Token。

5.服務端收到請求後,首先驗證Token,然後再傳回資料。

6.用戶端将Token儲存到本地浏覽器,一般也是儲存到Cookie中。

這樣,服務端就不需要儲存Token,隻需要對Token中攜帶的資訊進行驗證即可。

無論用戶端通路背景的哪台伺服器,隻要可以通過使用者資訊的驗證即可。

JWT的原理

JWT的原理是,伺服器認證之後,生成一個JSON對象,傳回給使用者,就像下面這樣。

{
    "姓名": "張飛",
    "角色": "管理者",
    "到期時間": "2019年05月20日0時0分"
}      

以後使用者和伺服器進行通信的時候都要發送這個JSON對象。伺服器完全隻靠這個對象認證使用者資訊。為了防止使用者篡改資料,伺服器在生成這個對象的時候,都會加上簽名。這樣,伺服器就不需要儲存任何Session會話資訊了,也就是說,伺服器變成無狀态的了,進而比較容易實作擴充。

JWT的資料結構

實際的JWT大概就是像下面這個樣子:

jwt入門

這是一個很長的字元串,中間用點分隔符【.】分隔成三個部分(要注意,JWT内部是沒有換行的)。

JWT由三個部分組成:頭部(Header)、負載(Payload)和簽名(Signature)。

jwt入門

Header

Heade部分是一個JSON對象,是描述JWT的中繼資料。通常是下面這種格式的:

{
    "alg": "HS256",
    "typ": "JWT"
}      

在上面的代碼中,alg屬性表示簽名的算法(Algorithm),預設是HMAC SHA256(寫成HS256);typ屬性表示這個令牌(token)的類型(type),JWT令牌固定寫為JWT。最後,将上面的JSON對象使用Base64URL算法轉成字元串。

Payload

Payload部分也是一個JSON對象,用來存放實際需要傳遞的資料。JWT規定了7個官方字段供選用。

iss(issuer) 簽發人
exp(expiration time) 過期時間
sub(subject) 主題
aud(audience) 閱聽人
nbf(not before) 生效時間
iat(issued at) 簽發時間
jti(jwt id) 編号

除了這些官方字段,也可以在這個部分定義私有字段:

{
    "sub": "buybuybuy",
    "name": "yanggb",
    "admin": true
}      

注意這個JWT部分預設是不加密的,任何人都可以讀到,是以不要把秘密資訊放在這個部分。

這個JSON對象也要使用Base64URL算法轉成字元串。

Signature

Signature部分是對前兩部分的簽名,防止資料篡改。

首先,需要指定一個密鑰(secret)。這個密鑰隻有伺服器才知道,不能洩露給使用者。

然後,使用Header裡面指定的簽名算法(預設是HMAC SHA256),按照下面的公式産生簽名:

HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)      

最後,在算出簽名之後,把Header、Payload、Signature三個部分拼成一個字元串,每個部分之間用點分隔符【.】分割,就可以傳回給使用者了。

Base64URL算法

前面提到,Header和Payload串型化的算法是Base64URL。這個算法跟Base64算法基本類似,但有一些小的不同。

JWT作為一個令牌(Token),有些場合可能會放到URL(比如api.yanggb.com/?token=xxx)。Base64有三個字元【+】、【/】和【=】,在URL裡面是有特殊含義的,是以需要被替換掉:【=】号被省略、【+】号被替換成【-】、【/】被替換成【_】。這就是Base64URL算法和Base64算法的不同。

JWT的使用方式

用戶端收到服務端傳回的JWT,可以存儲在Cookie裡面,也可以存儲在localStorage裡面。此後,用戶端每次與伺服器通信,都要帶上這個JWT。你可以把它放在Cookie裡面自動發送,但是這樣就不能跨域,是以更好的做法是放在HTTP請求的頭資訊Authorization字段裡面。

Authorization: Bearer <token>      

另一種做法是在跨域的時候将JWT放在POST請求的資料體裡面。

JWT的幾個特點

1.JWT預設是不加密,但是也是可以加密的。生成原始的Token以後,可以用密鑰再加密一次。JWT不加密的情況下,不能将秘密資料寫入JWT。

2.JWT不僅可以用于認證,也可以用于交換資訊。有效地使用JWT,可以降低伺服器查詢資料庫的次數。

3.JWT的最大缺點是由于伺服器不儲存Session會話狀态,是以無法在使用過程中廢止某個Token或者更改Token的權限。意思就是說,一旦JWT簽發了,在到期之前就會始終有效,除非伺服器有部署額外的處理邏輯。

4.JWT本身包含了認證資訊,一旦洩露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT的有效期應該設定得比較短。對于一些比較重要的權限,使用時應該再次對使用者進行認證。

5.為了減少盜用,JWT不應該使用HTTP協定明碼傳輸,要使用HTTPS協定傳輸。

JWT的使用執行個體(Demo)

在Maven項目中添加JWT的依賴、相關依賴和插件。

<!-- JWT -->
<dependencies>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>RELEASE</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5</version>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>      

建立一個單元測試類。

public class JWTDemo {
    // 加密的KEY
    private static final String SECRET_KEY = "123456";

    @Test
    public void jwtTest() throws InterruptedException {
        // 建立JWT
        long time = System.currentTimeMillis() + 30*60*1000;
        String jwt = this.buildJwt(new Date(time));
        System.out.println("JWT字元串:" + jwt);

        // 驗證token是否可用
        boolean vaild = this.isJwtValid(jwt);
        System.out.println("Token可用:" + vaild);
    }

    /**
     * 建立JWT
     * @param exp 過期時間
     * @return JWT String
     */
    private String buildJwt(Date exp) {
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY) // SECRET_KEY是加密算法對應的密鑰,這裡使用額是HS256加密算法
                .claim("name", "yanggb")
                .setExpiration(exp) // expTime是過期時間
                .compact();
    }

    private boolean isJwtValid(String jwt) {
        try {
            // 解析JWT字元串中的資料,并進行最基礎的驗證
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY) // SECRET_KEY是加密算法對應的密鑰,jjwt可以自動判斷機密算法
                    .parseClaimsJws(jwt) // jwt是JWT字元串
                    .getBody();
            System.out.println(claims);
        }
        // 在解析JWT字元串時,如果密鑰不正确,将會解析失敗,抛出SignatureException異常,說明該JWT字元串是僞造的
        // 在解析JWT字元串時,如果過期時間字段已經早于目前時間,将會抛出ExpiredJwtException異常,說明本次請求已經失效
        catch (SignatureException | ExpiredJwtException e) {
            return false;
        }
        return true;
    }
}      

運作之後就可以在控制台看到列印出來的資訊,然後可以使用JWT的解析工具解析出内容(https://jwt.io/):

jwt入門

這些就是JWT的入門知識。

"我以為回憶終究會慢慢擱淺,化作泡沫,消失在時光の海的邊陲。可是記憶的種子卻時而開花,時而凋零,告訴我們:今後無論去哪,都要勇敢。"