JWT 從入門到精通

什麼是JWT

Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。

簡單來說就是 JWT(Json Web Token)是實現token技術的一種解決方案

爲什麼使用JWT

token驗證和session認證的區別

傳統的session認證

http協議本身是一種無狀態的協議,而這就意味着如果用戶向我們的應用提供了用戶名和密碼來進行用戶認證,那麼下一次請求時,用戶還要再一次進行用戶認證纔行,因爲根據http協議,我們並不能知道是哪個用戶發出的請求,所以爲了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲一份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存爲cookie,以便下次請求時發送給我們的應用,這樣我們的應用就能識別請求來自哪個用戶了,這就是傳統的基於session認證。

session缺點

基於session的認證使應用本身很難得到擴展,隨着不同客戶端用戶的增加,獨立的服務器已無法承載更多的用戶

Session方式存儲用戶id的最大弊病在於要佔用大量服務器內存,對於較大型應用而言可能還要保存許多的狀態。

基於session認證暴露的問題

  • Session需要在服務器保存,暫用資源
  • 擴展性 session認證保存在內存中 ,無法擴展到其他機器中
  • CSRF 基於cookie來進行用戶識別的, cookie如果被截獲,用戶就會很容易受到跨站請求僞造的攻擊。

基於token的鑑權機制

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

JWT方式將用戶狀態分散到了客戶端中,可以明顯減輕服務端的內存壓力。除了用戶id之外,還可以存儲其他的和用戶相關的信息,例如用戶角色,用戶性別等。

請求流程

  1. 用戶使用用戶名密碼來請求服務器
  2. 服務器進行驗證用戶的信息
  3. 服務器通過驗證發送給用戶一個token
  4. 客戶端存儲token,並在每次請求時附送上這個token值
  5. 服務端驗證token值,並返回數據

這個token必須要在每次請求時傳遞給服務端,它應該保存在請求頭裏, 另外,服務端要支持 CORS(跨來源資源共享)策略,一般我們在服務端這麼做就可以了 Access-Control-Allow-Origin: *

JWT的結構

一個JWT是下面的結構

加密後jwt信息如下所示,是由.分割的三部分組成,分別爲Header、Payload、Signature

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JWT 的組成

  • Head -主要包含兩個部分,alg指加密類型,可選值爲HS256RSA等等,typ=JWT爲固定值,表示token的類型

    Header:
    {
     "alg": "HS256",
     "typ": "JWT"
    }
    
  • Payload - Payload又被稱爲Claims包含您想要簽署的任何信息

    Claims:
    {
     "sub": "1234567890",
     "name": "John Doe",
     "admin": true
    }
    

    JWT Payload的組成

    Payload通常由三個部分組成,分別是 Registered Claims ; Public Claims ; Private Claims ;每個聲明,都有各自的字段。

    Registered Claims

    iss 【issuer】發佈者的url地址

    sub 【subject】該JWT所面向的用戶,用於處理特定應用,不是常用的字段

    aud 【audience】接受者的url地址

    exp 【expiration】 該jwt銷燬的時間;unix時間戳

    nbf 【not before】 該jwt的使用時間不能早於該時間;unix時間戳

    iat 【issued at】 該jwt的發佈時間;unix 時間戳

    jti 【JWT ID】 該jwt的唯一ID編號

  • Signature 對 則爲對Header、Payload的簽名

    Signature:
    base64UrlEncode(Header) + "." + base64UrlEncode(Claims)
    

頭部、聲明、簽名用 . 號連在一起就得到了我們要的JWT 也就是夏明這種類型的字符串

eyJhbGciOiJIUzI1NiJ9.

eyJleHAiOjE1MTUyOTgxNDEsImtleSI6InZhdWxlIn0.

orewTmil7YmIXKILHwFnw3Bq1Ox4maXEzp0NC5LRaFQ

其實這些事一行的,我只是讓看的更直白點將其割開了。

JAVA 實現

JAVA中使用JWT

使用Maven引入和Gradle引入

Maven

 <dependency>     
 <groupId>io.jsonwebtoken</groupId>     
 <artifactId>jjwt</artifactId>     
 <version>0.9.0</version> </dependency>

Gradle

 dependencies { compile 'io.jsonwebtoken:jjwt:0.9.0' }

JWT依賴於Jackson,需要在程序中加入Jackson的jar包且版本大於2.x

簽發JWT

public static String createJWT() {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)                                      // JWT_ID
                .setAudience("")                                // 接受者
                .setClaims(null)                                // 自定義屬性
                .setSubject("")                                 // 主題
                .setIssuer("")                                  // 簽發者
                .setIssuedAt(new Date())                        // 簽發時間
                .setNotBefore(new Date())                       // 失效時間
                .setExpiration(long)                                // 過期時間
                .signWith(signatureAlgorithm, secretKey);           // 簽名算法以及密匙
        return builder.compact();
}

驗證JWT

public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(jwt)
        .getBody();
}

完整示例

package com.tingfeng.demo;

import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public SecretKey generalKey() {
        String stringKey = Constant.JWT_SECRET;

        // 本地的密碼解碼
        byte[] encodedKey = Base64.decodeBase64(stringKey);

        // 根據給定的字節數組使用AES加密算法構造一個密鑰
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    /**
     * 創建jwt
     * @param id
     * @param issuer
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

        // 指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的時間
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("uid", "123456");
        claims.put("user_name", "admin");
        claims.put("nick_name", "X-rapido");

        // 生成簽名的時候使用的祕鑰secret,切記這個祕鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。
        // 一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
        SecretKey key = generalKey();

        // 下面就是在爲payload添加各種標準聲明和私有聲明瞭
        JwtBuilder builder = Jwts.builder() // 這裏其實就是new一個JwtBuilder,設置jwt的body
                .setClaims(claims)          // 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
                .setId(id)                  // 設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
                .setIssuedAt(now)           // iat: jwt的簽發時間
                .setIssuer(issuer)          // issuer:jwt簽發人
                .setSubject(subject)        // sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什麼userid,roldid之類的,作爲什麼用戶的唯一標誌。
                .signWith(signatureAlgorithm, key); // 設置簽名使用的簽名算法和簽名使用的祕鑰

        // 設置過期時間
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public Claims parseJWT(String jwt) throws Exception {
        SecretKey key = generalKey();  //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //設置簽名的祕鑰
                .parseClaimsJws(jwt).getBody();     //設置需要解析的jwt
        return claims;
    }

    public static void main(String[] args) {

        User user = new User("tingfeng", "bulingbuling", "1056856191");
        String subject = new Gson().toJson(user);

        try {
            JwtUtil util = new JwtUtil();
            String jwt = util.createJWT(Constant.JWT_ID, "Anson", subject, Constant.JWT_TTL);
            System.out.println("JWT:" + jwt);

            System.out.println("\n解密\n");

            Claims c = util.parseJWT(jwt);
            System.out.println(c.getId());
            System.out.println(c.getIssuedAt());
            System.out.println(c.getSubject());
            System.out.println(c.getIssuer());
            System.out.println(c.get("uid", String.class));

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

Constant.java

package com.tingfeng.demo;

import java.util.UUID;

public class Constant {
    public static final String JWT_ID = UUID.randomUUID().toString();

    /**
     * 加密密文
     */
    public static final String JWT_SECRET = "woyebuzhidaoxiediansha";
    public static final int JWT_TTL = 60*60*1000;  //millisecond
}

輸出示例

JWT:eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiIxMjM0NTYiLCJzdWIiOiJ7XCJuaWNrbmFtZVwiOlwidGluZ2ZlbmdcIixcIndlY2hhdFwiOlwiYnVsaW5nYnVsaW5nXCIsXCJxcVwiOlwiMTA1Njg1NjE5MVwifSIsInVzZXJfbmFtZSI6ImFkbWluIiwibmlja19uYW1lIjoiWC1yYXBpZG8iLCJpc3MiOiJBbnNvbiIsImV4cCI6MTUyMjMxNDEyNCwiaWF0IjoxNTIyMzEwNTI0LCJqdGkiOiJhNGQ5MjA0Zi1kYjM3LTRhZGYtODE0NS1iZGNmMDAzMzFmZjYifQ.B5wdY3_W4MZLj9uBHSYalG6vmYwdpdTXg0otdwTmU4U

解密

a4d9204f-db37-4adf-8145-bdcf00331ff6
Thu Mar 29 16:02:04 CST 2018
{"nickname":"tingfeng","wechat":"bulingbuling","qq":"1056856191"}
Anson
123456

總結

優點

  • 因爲json的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
  • 因爲有了payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
  • 便於傳輸,jwt的構成非常簡單,字節佔用很小,所以它是非常便於傳輸的。
  • 它不需要在服務端保存會話信息, 所以它易於應用的擴展

安全相關

  • 不應該在jwt的payload部分存放敏感信息,因爲該部分是客戶端可解密的部分。
  • 保護好secret私鑰,該私鑰非常重要。
  • 如果可以,請使用https協議
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章