1. 程式人生 > >JWT入門簡介

JWT入門簡介

工業 敏感信息 要去 obj pos 發生 only 字符 img

技術分享圖片

官網:https://jwt.io/
文檔:https://jwt.io/introduction/

目錄

  • 什麽是JWT
    • 頭部(Header)
    • 載荷(Payload)
    • 簽名(Signature)
  • JWT使用場景
  • 如何傳遞JWT
  • JWT應用實踐
  • 總結
    • JWT運行流程
    • 與傳統Session方式的比較
    • 使用JWT時註意事項

什麽是JWT

JWT是“JSON Web Token”的英文縮寫,是一種開放的工業標準方法(RFC 7519),用於在網絡應用環境中安全地傳遞聲明信息。雖然JWT的名稱中包含一個單詞“JSON”,但是JWT本身並不是JSON格式的(組成JWT的各個部分是JSON格式的)。實際上,JWT由三段信息構成,將這三段信息文本用.鏈接一起就構成了JWT字符串:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ.eNKsQ89xab7Za5P9uywqPvAiYZIHK1dwS0h8rRW9sVM

第一部分為頭部(Header),第二部分為載荷(Payload),第三部分為簽名(Signature)。

頭部(Header)

JWT的頭部承載兩部分信息:

  • 聲明類型,值為JWT
  • 聲明加密的算法,可以使用不同的簽名算法,如:HS256,HS384,HS512等等,不同的實現庫所能支持的算法也盡不相同

完整的頭部就像下面這樣的JSON:

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

然後將頭部進行Base64編碼就構成了第一部分:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

載荷(Payload)

載荷就是存放聲明信息的地方(通常可以將登錄的用戶信息存放在這裏),包含2個部分:

  1. 公共聲明
  2. 私有聲明

公共聲明中可以包含如下信息(建議但不強制使用):

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什麽時間之前,該jwt都是不可用的
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊

私有聲明中可以聲明一些與業務相關的信息,但是一般不建議存放敏感信息,因為Base64編碼值是可以解碼的,意味著該部分信息可以歸類為明文信息,存放敏感信息不安全。

一個Payload示例如下:

{
    "iss" "iss0"
    "sub": "1234567890",
    "name": "zhangsan",
    "admin": true
}

顯然,在上述定義的Payload中,name和admin都屬於自定義聲明信息。然後將其進行Base64編碼,得到JWT的第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ

簽名(Signature)

JWT的第三部分是一個簽名信息,這個部分需要Base64編碼後的Header和Base64編碼後的Payload使用.連接組成字符串,然後通過Header中聲明的加密方式進行加鹽secret組合加密並將加密結果進行Bas464編碼,就是構成了JWT的第三部分:eNKsQ89xab7Za5P9uywqPvAiYZIHK1dwS0h8rRW9sVM。如下為計算簽名值的完整示例:

public static void main(String[] args) throws InvalidKeyException {
    // 計算得到Base64編碼後的Header值
    String header = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
    // 計算得到Base64編碼後的Payload值
    String payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ";
    String secret = "secret";
    String encodeStr = header + "." + payload;
    // 對Base64編碼後的Header和Base64編碼後的payload進行HMAC256加鹽加密,得到JWT的第三部分簽名信息
    String signature = HMACSHA256(encodeStr.getBytes(), secret.getBytes());
    System.out.println(signature);
}

// 使用HMAC256加密
public static String HMACSHA256(byte[] data, byte[] key) throws InvalidKeyException {
    try {
        SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        return new BASE64Encoder().encode(mac.doFinal(data));
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } catch (InvalidKeyException e) {
        e.printStackTrace();
    }
    return null;
}

註意: secret是保存在服務器端的,JWT的簽發生成也是在服務器端的,secret用於進行JWT的簽發和驗證。所以,它是服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。

JWT使用場景

JWT主要解決的是在網絡中安全地傳遞用戶信息,因此可應用在如下場景:
1.在REST接口中保存用戶信息,實現API接口的訪問授權。
用戶登錄之後將信息(如:user_id)編碼到JWT字符串中傳遞給客戶端,這樣服務端就不用再保存登錄用戶信息了,便於服務端分布式擴容。另外,還可以直接使用JWT的公共聲明實現訪問控制(如通過exp聲明實現訪問失效,jti聲明實現一次性token等等)。
2.分布式站點的單點登錄(SSO)。
實現原理是將JWT字符串作為響應Cookie的一部分返回給瀏覽器客戶端,這樣JWT就可以在相同主域的多個站點之後傳遞,從而實現分布式站點的單點登錄。註意,在這裏必須使用HttpOnly屬性來防止Cookie被JavaScript讀取,從而避免XSS攻擊。

如何傳遞JWT

理論上,在基於HTTP協議的應用中可以有如下幾種傳遞方式:

  1. 在HTTP消息頭中傳遞,如:Authorization: ‘Bearer ‘ + header.body.signature
  2. 在Cookie中傳遞,如:Set-Cookie: jwt=header.body.signature; HttpOnly;domain=.lenovo.com
  3. 在消息體中傳遞:jwt=header.body.signature,但通常不應該這麽做

JWT應用實踐

  • 手動簽發JWT

以Java語言為例,我們完全可以按照JWT的定義格式自己簽發JWT。

// 手動實現JWT簽發
// 需要註意的是:使用JDK自帶的Base64工具類編碼的結果可能會以"=="結尾,需要去掉這個字符
public class JWTUtil {
    public static void main(String[] args) throws InvalidKeyException {
        // 構造頭部
        JSONObject headerJson = new JSONObject();
        headerJson.put("typ", "JWT");
        headerJson.put("alg", "HS256");
        String header = base64Encode(headerJson.toJSONString().getBytes());

        // 構造載荷
        JSONObject payloadJson = new JSONObject();
        payloadJson.put("iss", "iss0");
        payloadJson.put("sub", "1234567890");
        payloadJson.put("name", "zhangsan");
        payloadJson.put("admin", true);
        String payload = base64Encode(payloadJson.toJSONString().getBytes());

        // 加密
        String secret = "secret";
        String encodeStr = header + "." + payload;
        String signature = HMACSHA256(encodeStr.getBytes(), secret.getBytes());
        String jwt = new StringBuilder()
                .append(header)
                .append(".")
                .append(payload)
                .append(".")
                .append(signature)
                .toString();
        System.out.println(jwt);
    }

    // 使用HMAC256加密
    private static String HMACSHA256(byte[] data, byte[] key) throws InvalidKeyException {
        try {
            SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(signingKey);
            return base64Encode(mac.doFinal(data));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        }
        return null;
    }

    // base64編碼
    private static  String base64Encode(byte[] bytes) {
        String encode = Base64.getEncoder().encodeToString(bytes);
        int index = encode.indexOf("=");
        if(index > 0) {
            encode = encode.substring(0, index);
        }
        return encode;
    }
}
  • 使用類庫簽發JWT

從JWT的官網可以看到,目前已經有多種語言版本JWT的實現庫。
以Java庫為例,完全支持JWT公共聲明和常用加密算法的庫有3個,分別是:java-jwt,jose4j,jjwt,比較如下:

名稱 易用性 性能(ms) 熱度 地址
java-jwt 180 1812 https://github.com/auth0/java-jwt
jose4j 258 NaN https://bitbucket.org/b_c/jose4j/wiki/Home
jjwt 292 3187 https://github.com/jwtk/jjwt

附: 性能是指連續生成10次JWT所需要的平均耗時時間(單位:毫秒)。

鑒於易用性和性能方面的考慮,如下示例以使用java-jwt庫進行說明,更加詳細的使用請參考各個實現庫官方文檔。

  • 添加依賴
<!-- 集成JWT類庫 -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.3.0</version>
</dependency>
  • 服務端簽發和驗證JWT
@RestController
@RequestMapping("/jwt")
public class JwtController {
    private String secret = "secret";
    private String iss = "iss0";
    private String sub = "1234567890";
    private String key = null;

    // 模擬用戶登錄,並在登錄請求響應中返回JWT
    @PostMapping("/login")
    public Object login(HttpServletRequest req, HttpServletResponse resp,
                        @RequestBody JSONObject user) {
        // 用戶名和密碼
        String userName = user.getString("username");
        String passwrod = user.getString("passwrod");

        // 使用類庫簽發JWT
        try {
            Algorithm algorithm = Algorithm.HMAC256(this.secret);
            String jwt = JWT.create()
                    .withIssuer(iss)
                    .withSubject(sub)
                    //.withAudience(auArr)
                    //.withExpiresAt(exp)
                    //.withNotBefore(nbf)
                    //.withIssuedAt(iat)
                    //.withJWTId(jti)
                    .withClaim("name", userName)
                    .withClaim("admin", true)
                    .sign(algorithm);

            JSONObject data = new JSONObject();
            data.put("code", 200);
            data.put("message", "success");
            data.put("data", jwt);

            return data;
        } catch (UnsupportedEncodingException e){
            //UTF-8 encoding not supported
            e.printStackTrace();
        } catch (JWTCreationException e){
            //Invalid Signing configuration / Couldn't convert Claims.
            e.printStackTrace();
        }

        return null;
    }

    // 模擬在用戶登錄之後將JWT通過HTTP消息頭返回給服務端進行驗證
    @GetMapping("/list")
    public Object list(HttpServletRequest req, HttpServletResponse resp) {
        String auth = req.getHeader("Authorization");
        if(auth != null) {
            String jwt = auth.split(" ")[1];
            try {
                Algorithm algorithm = Algorithm.HMAC256(this.secret);
                JWTVerifier verifier = JWT.require(algorithm)
                        .withIssuer(this.iss)
                        .build(); //Reusable verifier instance
                DecodedJWT jwtDecode = verifier.verify(jwt);

                System.out.println("=========================");
                System.out.println(jwtDecode.getClaim("name").asString());
                System.out.println(jwtDecode.getClaim("admin").asBoolean());
                System.out.println("=========================");
            } catch (UnsupportedEncodingException e){
                //UTF-8 encoding not supported
                e.printStackTrace();
            } catch (JWTVerificationException e){
                //Invalid signature/claims
                e.printStackTrace();
            }
        }

        List<String> list = new ArrayList<String>();
        list.add("張三");
        list.add("李四");

        JSONObject data = new JSONObject();
        data.put("code", 200);
        data.put("message", "success");
        data.put("data", list);

        return data;
    }
}
  • 客戶端讀取並返回JWT
var jwt = null;

// 模擬用戶登錄獲取JWT
function doLogin() {
    var url = "http://localhost:8080/jwt/login";
    var params = {"username": "zhangsan", "password": "111111"};
    $.ajax({
        type: "POST",
        url: url,
        dataType: "json",
        contentType: "application/json",
        data: JSON.stringify(params),
        success: function (data) {
            console.log(data.data);
            jwt = data.data;
        }
    });
}

// 模擬用戶登錄之後執行操作,將JWT返回給服務端
function doList() {
    var url = "http://localhost:8080/jwt/list";
    $.ajax({
        type: "GET",
        url: url,
        headers: {
            // 客戶端需要在HTTP請求消息頭中將JWT返回給服務端
            'Authorization': 'Bearer ' + jwt,
        },
        success: function(data){
            console.log(data);
        }
    });
}

總結

JWT運行流程

技術分享圖片

與傳統Session方式的比較

本質上來講,JWT就是一種在網絡應用中保存用戶信息的方式。因此,不得不與傳統的Session保存用戶信息的方式進行比較。

  • 基於Session方式保存用戶信息

HTTP協議本身是無狀態的,為了在Web應用中記住登錄用戶的信息,傳統方式通過Session在服務端保存登錄用戶信息。具體實現為:用戶訪問網站時會在服務端隨機生成一個Session ID,服務端使用該Session ID在內存中保存一個與之相關聯的對象,再以Cookie的形式將該Session ID返回給瀏覽器客戶端,以後每次瀏覽器客戶端訪問服務器時都以Cookie的形式將該Sesion ID再返回給服務器端,這是前提。在用戶登錄成功後,將相關信息保存在與該Session ID相關的對象中(通常是保存在內存),通過這種方式就實現了在服務器端保存用戶信息。這種通過Cookie方式實現Session並在服務端保存用戶信息的方式存在一些弊端:
(1)服務端內存壓力大:Session都是保存在內存中,而隨著認證用戶的增多,服務端的開銷會明顯增大。
(2)服務端擴展性不好:用戶認證之後,服務端做認證記錄,如果認證的記錄被保存在內存中的話,這意味著用戶下次請求還必須要請求在這臺服務器上,這樣才能拿到授權的資源,這樣在分布式的應用上,相應的限制了負載均衡器的能力,也意味著限制了應用的擴展能力。
(3)CSRF:因為是基於Cookie來實現Session的, 如果實現不當Cookie被截獲,用戶就會很容易受到跨站請求偽造的攻擊。

  • 基於Token方式保存用戶信息

將用戶信息基於Token方式在每次請求中進行傳遞,這樣就不需要在服務端保存,大大降低了服務端的存儲壓力。另外,服務端可以實現任意的分布式擴容縮容。當然,基於Token方式保存用戶信息的方式完全可以自定義實現(參考:細說REST API安全之訪問授權),此時需要考慮如何保證Token安全傳遞等方方面面的因素。而基於JWT這樣的標準結構,大大降低了實現的難度。
(1)由於JSON的通用性,所以JWT是可以進行跨語言支持的,像JAVA,JavaScript,PHP等很多語言都可以使用。
(2)因為有了Payload部分,所以JWT可以在自身存儲一些其他業務邏輯所必要的非敏感信息。
(3)便於傳輸,JWT的構成非常簡單,字節占用很小,所以它是非常便於傳輸的。

使用JWT時註意事項

  1. 不應該在JWT的Payload部分存放敏感信息,因為Base64編碼是很容易被解碼的,這部分相當於明文數據。
  2. 保護好服務端用於加密的secret私鑰,該私鑰非常重要。
  3. 請使用https協議保證傳輸的安全性。

【參考】
[1]. https://www.jianshu.com/p/576dbf44b2ae 什麽是 JWT -- JSON WEB TOKEN
[2]. https://blog.leapoahead.com/2015/09/06/understanding-jwt/ JSON Web Token - 在Web應用間安全地傳遞信息
[3]. http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/ 八幅漫畫理解使用JSON Web Token設計單點登錄系統

JWT入門簡介