JWT應用

以前我在開發App時,後端給我們的權限字符串是一個token,這個token很簡單,疑似一個固定字符串經過base64編碼,大約32個字符,並不長。每次我們向後端請求接口,都要帶着這個字符串。可能是由於那時候沒有做分佈式吧,這個token只是用來做權限甄別,並不攜帶其他信息。

最近兩年我做java服務端,開始也是用這種方式。這種token在單例部署服務中是可以的,後端生成token,和用戶綁定,收到前端的接口請求,把token對比一下就放行。簡單起到了權限檢查的作用,但是安全性很差。爲什麼?因爲沒有簽名算法,很容易造假。另外,在分佈式系統中,token簽發和校驗可能不在同一個服務中,這就造成了用戶信息丟失的問題。我們在分佈式系統中,mysql和redis也有可能是分開的,這就需要我們的前端在獲取token的時候,一併帶上自己的用戶信息,就像身份證一樣。於是jwt就起到了這個作用。

jwt的最終結果是生成個字符串,所以它只是一個算法工具。可以不依賴web程序進行測試。不過由於它也只有在web程序中才能體現自己的價值,所以我的demo還是以springboot爲基礎來搭建的。

看看程序的結構:

可以看出,要測試jwt,只需要很簡單幾個文件。除了web服務常見的controller、service和model之外,就只有TokenUtils是重點了,jwt的構建和解析都在其中。

一、添加pom依賴

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

        <!-- java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

二、配置yml,我習慣改個端口

server:
  port: 6005

三、model文件User.java

這其中放置用戶信息,假設是從數據庫中查詢得到的用戶信息

package com.chris.jwt.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author chrischan
 * create on 2019/6/24 9:50
 * use for:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private int id;
    private String username;
    private String password;
    private String[] permissions;

    public static User create(int id, String name, String password, String... permissions) {
        return new User(id, name, password, permissions);
    }
}

四、service層AccountService.java

package com.chris.jwt.service;

import com.chris.jwt.model.User;
import com.chris.jwt.utils.TokenUtils;
import org.springframework.stereotype.Service;

/**
 * @author chrischan
 * create on 2019/6/24 9:25
 * use for:
 */
@Service
public class AccountService {

    public Object login(String username, String password) {

        User user = User.create(1, username, password, "ROLE_ADMIN", "ROLE_USER");

        String sub = "jwt_test";

        //構建的時候把用戶信息中的用戶名
        return TokenUtils.build(user, sub, 600000, "username","permissions");
    }

    public String parseToken(String token, String field) {
        return String.valueOf(TokenUtils.parse(token).get(field));
    }

}

此次的目的是測試jwt,所以並沒有涉及用戶信息檢驗的邏輯,用戶信息要加在jwt中,調用的時候傳什麼都視爲合法,直接加進去。中間缺失的邏輯根據真實業務補充。

五、接口AccountController.java

package com.chris.jwt.api;

import com.chris.jwt.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author chrischan
 * create on 2019/6/24 9:22
 * use for:
 */
@RestController
@RequestMapping("/api")
public class AccountController {
    @Autowired
    AccountService accountService;

    /**
     * 登錄 登陸成功則返回一個token
     *
     * @param username
     * @param password
     * @return
     */
    @GetMapping("/login")
    public ResponseEntity<?> login(String username, String password) {
        return ResponseEntity.ok(accountService.login(username, password));
    }

    @PostMapping("/parseToken")
    public ResponseEntity<?> parseToken(String token, String field) {
        return ResponseEntity.ok(accountService.parseToken(token, field));
    }
}

很簡單的兩個接口,一個用來模擬登陸,獲得jwt,一個用來測試對token的解析。

六、最重要的部分,TokenUtils.java

jwt的構建和解析都在其中。

package com.chris.jwt.utils;

import com.chris.jwt.model.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author chrischan
 * create on 2019/6/24 9:48
 * use for:
 */
public class TokenUtils {
    /**
     * 指紋 最關鍵的東西 不可丟失
     */
    private static String secret = "ASFW56868UIIGNJ2356SKFH568DS856876FJK";

    /**
     * 工具初始化
     *
     * @param secret
     */
    public static void init(String secret) {
        //設置一個通用指紋 出入保持一致
        TokenUtils.secret = secret;
    }

    /**
     * 構建token 使用公用的指紋
     *
     * @param obj
     * @param subject
     * @param ttMillis
     * @param fieldNames
     * @param <T>
     * @return
     */
    public static <T> String build(T obj, String subject, long ttMillis, String... fieldNames) {
        return build(obj, TokenUtils.secret, subject, ttMillis, fieldNames);
    }

    /**
     * 構建token
     *
     * @param obj        需要添加到token中的用戶對象
     * @param secret     指紋
     * @param subject    主題
     * @param ttMillis   過期時間
     * @param fieldNames 需要添加到token中的字段
     * @param <T>
     * @return
     */
    public static <T> String build(T obj, String secret, String subject, long ttMillis, String... fieldNames) {
        //簽名算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成token的時間
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //私有生命 最有用的部分 可攜帶信息
        Class<?> objClass = obj.getClass();
        Map<String, Object> claims = new HashMap<>(16);
        for (String fieldName : fieldNames) {
            Field field = null;
            try {
                field = objClass.getDeclaredField(fieldName);
                field.setAccessible(true);
                Object value = field.get(obj);
                field.setAccessible(false);
                claims.put(fieldName, value);
            } catch (Exception e) {
                continue;
            }
        }

        //構建
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)//剛發現這部分自定義的數據要首先設置,否則此前的設置都會消失
                .setId(UUID.randomUUID().toString())
                .setIssuer("chris") //簽發人
                .setIssuedAt(now) //簽發時間
                .setSubject(subject) //主題
                .signWith(signatureAlgorithm, secret);
        //添加過期時間
        if (ttMillis > 0) {
            long expMillis = nowMillis + ttMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        return builder.compact();
    }

    /**
     * 解析token 使用通用指紋
     *
     * @param tokenJson
     * @return
     */
    public static Claims parse(String tokenJson) {
        return parse(tokenJson, secret);
    }

    /**
     * 解析token 使用自定義指紋
     *
     * @param tokenJson
     * @param secret
     * @return
     */
    public static Claims parse(String tokenJson, String secret) {
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(tokenJson).getBody();
        return claims;
    }

    /**
     * token是否有效檢查
     * 此處只對user做簡單密碼匹配校驗
     *
     * @param tokenJson
     * @param user
     * @return
     */
    public static boolean isEffective(String tokenJson, User user) {
        String password = user.getPassword();
        Claims claims = parse(tokenJson, secret);
        //todo 檢查過期
        Object password1 = claims.get("password");

        if (null != password1 && password.equals(password1)) {
            return true;
        }
        return false;
    }
}

jwt的構建,主要是操作JwtBuilder

        //構建
        JwtBuilder builder = Jwts.builder()
                .setClaims(claims)
                .setId(UUID.randomUUID().toString())
                .setIssuer("chris") //簽發人
                .setIssuedAt(now) //簽發時間
                .setSubject(subject) //主題
                .signWith(signatureAlgorithm, secret);

 

jwt的解析,主要是操作JwtParser 

Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(tokenJson).getBody();

七、運行測試

http://localhost:6005/api/login?username=zhangsan&password=123456

得到結果:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2NzkxNDIsImlhdCI6MTU3MDY3ODU0MiwianRpIjoiMjk1MzI2N2QtNTNiZS00N2Y2LWFjNTYtMjI4M2FjMDFkNWQ4IiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.LT9ngTFW8BARFnBmK-enzz37fjRc39sELFPKUXjDw2g

關於jwt的結構,此處不再贅述。jwt分三段,頭部指明瞭算法,中間部分是有效載荷,放置的是我們有用的信息。我們構建jwt的時候設置的大多數都是這部分,包括用戶名和權限列表,簽發人、主題和有效時間等等。最後一部分是吧前兩部分合起來通過指紋算法生成的,不可逆,可以用來做校驗。這也是最關鍵的,因爲使用的是摘要算法,無法解析,也就不可能在中途做假,我們攜帶的信息就無法被篡改。

三部分之間都是用英文句點來連接的。

1. 現在我們來看一來看頭部分,用base64解析一下。

左邊是解析之後的部分,起哦門可以看到其中包含算法方式信息。這個信息在解析的時候會很有用,系統會根據這個信息使用相同的算法來驗證簽名。

2. 我們來看看有效載荷部分解析之後的樣子

有效載荷就是第二部分,解析之後我們可以看到我們曾經添加的東西,主題、權限、用戶名、簽發人,有效時間都在裏面。我們可以解析出用戶名,在分佈式服務中直接使用,權限列表也可以直接在security中進行控制。

3. 我們調用一下解析接口,看看是否成功解析到我們想要的數據。我們把剛纔得到的token傳進去,獲取用戶名

http://localhost:6005/api/parseToken?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2NzkxNDIsImlhdCI6MTU3MDY3ODU0MiwianRpIjoiMjk1MzI2N2QtNTNiZS00N2Y2LWFjNTYtMjI4M2FjMDFkNWQ4IiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.LT9ngTFW8BARFnBmK-enzz37fjRc39sELFPKUXjDw2g&field=username

猜猜結果是什麼?

{
    "timestamp": "2019-10-10T04:04:42.682+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "JWT expired at 2019-10-10T11:45:42Z. Current time: 2019-10-10T12:04:42Z, a difference of 1140679 milliseconds.  Allowed clock skew: 0 milliseconds.",
    "path": "/api/parseToken"
}

提示這個token已經過期失效了。因爲我是邊 測試邊編輯,10分鐘的有效時間已經過了。這也剛好檢驗了一下我們的jwt對過期檢查的邏輯。我們重新請求一個token來檢測解析。

http://localhost:6005/api/parseToken?token=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3RfdGVzdCIsInBlcm1pc3Npb25zIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwiaXNzIjoiY2hyaXMiLCJleHAiOjE1NzA2ODExMDYsImlhdCI6MTU3MDY4MDUwNiwianRpIjoiMTNiNDAxZTktZTNkMC00YTZjLWEyZTItYTc3YWQzMzQyYmIwIiwidXNlcm5hbWUiOiJ6aGFuZ3NhbiJ9.XqDfNclsygF4nxp5pHToIEGOAx5rQhcl8oBXFvvVQc4&field=username

看看PostMan請求的結果:

 我們成功解析獲取到我們的用戶名: zhangsan。

八、說明

jwt自身攜帶用戶信息,在分佈式服務中可以直接驗證和提取用戶信息,省去了再到用戶賬號中心進行檢驗的麻煩。加上合理的設置有效時間,也能基本做到和用戶信息的同步。不過由於解析的時候要用到構建時的簽名,構建和解析時需要使用相同的指紋secret,而這個指紋也是jwt安全的關鍵所在,一旦丟失,jwt就可以在前端被僞造出來。所以一定要保密,或者要以非常安全的方式來管理secret。

本例中構建和解析都在一起。分佈式中構建則存在於簽發服務中,解析在業務服務中,是分開的。只需要把TokenUtils.java中關於解析和過期檢驗的邏輯搬過去即可,切記解析使用的類(如果有,如User)和指紋secret要一致。

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