SpringBoot使用JWT入門級示例

聲明這只是一個入門級的JWT示例教學,歡迎各位朋友踊躍發言,一起探討進步。

聲明本文不介紹JWT相關概念,也不比較JWT的各個類庫,對相關概念、JWT各個類庫感興趣的朋友可
           自行查閱相關資料;本文直接演示示例使用入門級JWT。


提示本人較懶,不想什麼基本的東西都自己寫,所以這裏使用了JWT衆多類庫中的nimbus-jose-jwt類庫。
           本文中涉及到的JwtUtil工具類,其實是本人對nimbus-jose-jwt提供的基本功能的一個簡單封裝。

軟硬件環境說明Windows10、IntelliJ IDEA、SpringBoot 2.1.6.RELEASE。

準備工作:在pom.xml中映入依賴

本人爲了及快速開發、爲了示例,還引入了其他的一些不是關鍵的依賴文件。給出完整pom.xml文字版:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.szlzcl</groupId>
    <artifactId>jwt-token</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwt-token</name>
    <description>測試使用json web token</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>7.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

JWT簡單使用示例

先給出本人的示例項目結構

各個文件的具體內容

  • JwtUtil

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;

import java.text.ParseException;

/**
 * 對nimbus-jose-jwt類庫提供的基本功能 再進行工具類封裝
 *
 * @author JustryDeng
 * @date 2019/7/21 13:46
 */
@SuppressWarnings("unused")
public class JwtUtil {

    /**
     * 生成token -- 採用【對稱加密】算法HS256驗證簽名
     *
     * 注:此方法生成的jwt的Header部分爲默認的
     *    {
     *      "alg": "HS256",
     *      "typ": "JWT"
     *    }
     *
     * @param payloadJsonString
     *            有效負載JSON字符串
     *
     * @param secret
     *            對稱加密/解密密鑰
     *            注意:secret.getBytes().length 必須 >=32
     *
     * @return token
     * @throws JOSEException
     *             以下情況會拋出此異常:
     *                 1、密鑰長度 secret.getBytes().length < 32時,會拋出此異常
     *                 2、JWS已簽名
     *                 3、JWS無法使用指定的簽名器
     * @date 2019/7/21 13:54
     */
    public static String generateToken(String payloadJsonString, String secret)
            throws JOSEException {

        // 創建Header(設置以JWSAlgorithm.HS256算法進行簽名認證)
        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);

        // 建立Payload
        Payload payload = new Payload(payloadJsonString);

        /// Signature相關
        // 根據Header以及Payload創建JSON Web Signature (JWS)對象
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        // 使用給的對稱加密密鑰,創建一個加密器
        JWSSigner jwsSigner = new MACSigner(secret);
        // 將該簽名器 與 JSON Web Signature (JWS)對象進行關聯
        // 即: 指定JWS使用該簽名器進行簽名
        jwsObject.sign(jwsSigner);

        // 使用JWS生成JWT(即:使用JWS生成token)
        return jwsObject.serialize();
    }

    /**
     * 校驗token是否被篡改,並返回有效負載JSON字符串 -- 採用【對稱加密】算法HS256驗證簽名
     *
     * @param secret
     *            對稱加密/解密密鑰
     *            注意:secret.getBytes().length 必須 >=32
     *
     * @return  有效負載JSON字符串
     * @throws JOSEException,ParseException,JwtSignatureVerifyException 異常信息
     * @date 2019/7/21 14:08
     */
    public static String verifySignature(String token, String secret)
            throws JOSEException, ParseException, JwtSignatureVerifyException {
        // 解析token,將token轉換爲JWSObject對象
        JWSObject jwsObject = JWSObject.parse(token);

        // 創建一個JSON Web Signature (JWS) verifier.用於校驗簽名(即:校驗token是否被篡改)
        JWSVerifier jwsVerifier = new MACVerifier(secret);
        // 如果校驗到token被篡改(即:簽名認證失敗),那麼拋出異常
        if(!jwsObject.verify(jwsVerifier)) {
            throw new JwtSignatureVerifyException("Signature verification result is fail!");
        }

        // 獲取有效負載
        Payload payload = jwsObject.getPayload();

        // 返回 有效負載JSON字符串
        return payload.toString();
    }


    /**
     * 生成token -- 採用【非對稱加密】算法RS256驗證簽名
     *
     * @param payloadJsonString
     *            有效負載JSON字符串
     *
     * @param rsaKey
     *            非對稱加密密鑰對
     *            提示:RSAKey示例,可以這麼獲得
     *            RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
     *            RSAKey rsaKey = rsaKeyGenerator.generate();
     *
     * @return token
     * @throws JOSEException 異常信息
     * @date 2019/7/21 13:54
     */
    public static String generateTokenByAsymmetric(String payloadJsonString, RSAKey rsaKey)
            throws JOSEException {

        // Header
        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
                                           .keyID(rsaKey.getKeyID())
                                           .build();
        // Payload
        Payload  payload= new Payload(payloadJsonString);

        /// Signature相關
        // 根據Header以及Payload創建JSON Web Signature (JWS)對象
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        // 使用給的對稱加密密鑰,創建一個加密器
        JWSSigner signer = new RSASSASigner(rsaKey);
        // 將該簽名器 與 JSON Web Signature (JWS)對象進行關聯
        // 即: 指定JWS使用該簽名器進行簽名
        jwsObject.sign(signer);

        // 使用JWS生成JWT(即:使用JWS生成token)
        return jwsObject.serialize();
    }

    /**
     * 校驗token是否被篡改,並返回有效負載JSON字符串 -- 採用【非對稱加密】算法RS256驗證簽名
     *
     * @param rsaKey
     *          非對稱加密密鑰對
     *
     * @return  有效負載JSON字符串
     * @throws JOSEException,ParseException,JwtSignatureVerifyException 異常信息
     * @date 2019/7/21 14:08
     */
    public static String verifySignatureByAsymmetric(String token, RSAKey rsaKey)
            throws JOSEException, ParseException, JwtSignatureVerifyException {
        // 根據token獲得JSON Web Signature (JWS)對象
        JWSObject jwsObject = JWSObject.parse(token);

        // 獲取到公鑰
        RSAKey publicRsaKey = rsaKey.toPublicJWK();
        // 根據公鑰 獲取 Signature驗證器
        JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey);

        // 如果校驗到token被篡改(即:簽名認證失敗),那麼拋出異常
        if(!jwsObject.verify(jwsVerifier)) {
            throw new JwtSignatureVerifyException("Signature verification result is fail!");
        }

        // 獲取有效負載
        Payload payload = jwsObject.getPayload();

        // 返回 有效負載JSON字符串
        return payload.toString();
    }

//    /**
//     *  main方法測試
//     *
//     *  --------------------------------------下面的爲測試代碼--------------------------------------
//     *
//     */
//    public static void main(String[] args)
//            throws JOSEException, ParseException, JwtSignatureVerifyException {
//        // 測試對稱加密算法 驗證簽名的JWT
//        testSymmetric();
//        // 測試非對稱加密算法 驗證簽名的JWT
//        testAsymmetric();
//    }
//
//    /**
//     * 測試對稱加密算法的token生成與 簽名檢驗
//     */
//    private static void testSymmetric()
//            throws JOSEException, ParseException, JwtSignatureVerifyException {
//        String secret = "adsgfiaughofashdofhjasodhfoasdafisd";
//        String token = JwtUtil.generateToken("{\"name\":\"張三\"}", secret);
//        System.out.println(token);
//        String payloadJsonString = JwtUtil.verifySignature(token, secret);
//        System.out.println(payloadJsonString);
//    }
//
//    /**
//     * 測試對稱加密算法的token生成與 簽名檢驗
//     */
//    private static void testAsymmetric()
//            throws JOSEException, ParseException, JwtSignatureVerifyException {
//        RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(1024 * 3);
//        RSAKey rsaKey = rsaKeyGenerator.generate();
//        String token =
//                JwtUtil.generateTokenByAsymmetric("{\"name\":\"張三\"}", rsaKey);
//        System.out.println(token);
//        String payloadJsonString = JwtUtil.verifySignatureByAsymmetric(token, rsaKey);
//        System.out.println(payloadJsonString);
//    }
}
  • JwtSignatureVerifyException:

/**
 * 簽名校驗失敗異常
 * 即:token被篡改異常
 *
 * @author JustryDeng
 * @date 2019/7/21 14:23
 */
@SuppressWarnings("unused")
public class JwtSignatureVerifyException extends Exception {

    private static final long serialVersionUID = -861994790728930634L;

    /**
     * Creates a new JwtSignatureVerifyException with the specified message.
     *
     * @param message The exception message.
     */
    public JwtSignatureVerifyException(String message) {
        super(message);
    }

    /**
     * Creates a new JwtSignatureVerifyException with the specified message and cause.
     *
     * @param message The exception message.
     * @param cause   The exception cause.
     */
    public JwtSignatureVerifyException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • PayloadDTO

import lombok.*;

import java.io.Serializable;

/**
 * JWT中間部分 有效負載 數據模型
 *
 * @author JustryDeng
 * @date 2019/7/21 15:31
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class PayloadDTO implements Serializable {

    private static final long serialVersionUID = 5988619597830511341L;

    /**
     * ------------------------------------這部分爲 註冊聲明------------------------------------
     * 這個jwt的身份id
     */
    private String jti;

    /** 簽發人 */
    private String iss;

    /** 過期時長(單位ms) */
    private Long exp;

    /** 主題 */
    private String sub;

    /** 受衆 */
    private String aud;

    /** 生效時間(1970年1月1日到現在的偏移量) */
    private Long nbf;

    /** 簽發時間(1970年1月1日到現在的偏移量) */
    private Long iat;

    /**
     * ------------------------------------這部分爲 公開聲明------------------------------------
     * 姓名
     */
    private String name;

    /**
     * 性別
     */
    private String gender;

    /**
     * 出生日期
     */
    private String birthday;

    /**
     * ------------------------------------這部分爲 私有聲明------------------------------------
     * 是否是管理員
     */
    private Boolean isAdmin;
}
  • application.properties

# 驗證簽名的(對稱加密)算法的密鑰
my.jwt.signature.algorithm.secret=abcdefghijklmnopqrstuvwxyz123456789
# 單位ms
my.jwt.expiration.time=60000
  • DemoController

import com.alibaba.fastjson.JSON;
import com.nimbusds.jose.JOSEException;
import com.szlzcl.jwttoken.model.PayloadDTO;
import com.szlzcl.jwttoken.util.JwtSignatureVerifyException;
import com.szlzcl.jwttoken.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.text.ParseException;

/**
 * controller
 *
 * @author JustryDeng
 * @date 2019/7/21 11:03
 */
@Slf4j
@RestController
public class DemoController {

    @Value("${my.jwt.signature.algorithm.secret}")
    private String signatureAlgorithmSecret;

    @Value("${my.jwt.expiration.time}")
    private Long tokenExpirationTime;

    /**
     * 用戶登錄, 並返回token信息
     *
     * 注:真正使用時,往往除了token,還會返回一些其他的相關信息。
     * 注:真正使用時,token往往放在響應頭裏面進行返回。
     * 注:真正使用時,用戶登錄不應用get方法,而是用post方法。
     *
     * @param name 用戶名
     * @param password mima
     *
     * @return  返回jwt
     * @date 2019/7/21 11:05
     */
    @GetMapping("/login")
    @SuppressWarnings("all")
    public String login(@RequestParam("name") String name,
                        @RequestParam("password")  String password){
        // 模擬 用戶名密碼 均正確
        if("鄧沙利文".equals(name) && "123xyz".equals(password)) {
            /*
             * 模擬獲得有效負載信息
             * 注:根據自己業務的不同,往往這一步會有非常大的不同
             */
            PayloadDTO payloadDTO  = getPayload(name);
            // 生成token
            String payloadJsonString = JSON.toJSONString(payloadDTO);
            String token;
            try {
                token = JwtUtil.generateToken(payloadJsonString, signatureAlgorithmSecret);
            } catch (JOSEException e) {
                log.error("生成token失敗!", e);
                return "生成token失敗";
            }
            return token;
        }
        return "用戶名密碼有誤!";
    }

    /**
     * 驗證請求中的token是否被篡改、是否過期(是否有效)。
     * 如果有效那麼返回響應的業務信息。
     *
     * 注:真正使用時,token的驗證往往在Filter或AOP中進行。
     * 注:真正使用時,一般將token放在請求頭中,以Authorization作爲key,以Bearer <token>作爲值。
     *
     * @param token json web token(JWT)
     *
     * @return  返回相應信息
     * @date 2019/7/21 11:05
     */
    @GetMapping("/test")
    public String logis(@RequestHeader("Authorization") String token){
        String payloadJsonString;
        // 驗證token是否被篡改
        try {
            payloadJsonString = JwtUtil.verifySignature(token, signatureAlgorithmSecret);
        } catch (JwtSignatureVerifyException e) {
            log.error("token簽名驗證失敗, token已經被篡改!", e);
            return "token簽名驗證失敗, token已經被篡改!";
        } catch (JOSEException | ParseException e) {
            log.error("驗證token簽名時,系統異常!", e);
            return "驗證token簽名時,系統異常!";
        }

        /// 驗證 有效負載中的其他信息 (如:過期時間、權限信息 等等)
        PayloadDTO payloadDTO =JSON.parseObject(payloadJsonString, PayloadDTO.class);
        log.info("token中存放的用戶信息是 -> {}", payloadDTO);
        // token生效時間
        long nbf = payloadDTO.getNbf();
        log.info("token的生效時間是 -> {}", nbf);
        // token有效時長
        long exp = payloadDTO.getExp();
        log.info("token的生效時長是 -> {}", exp);
        // 當前時間
        long nowTime = System.currentTimeMillis();
        log.info("當前時間是 -> {}", nowTime);

        boolean isAuthorized = (nowTime - nbf) >= 0 && exp >= (nowTime - nbf);
        log.info("token -> 【{}】是否有效? {}", token, isAuthorized);

        if (isAuthorized) {
            return "token認證通過!";
        }
        return "token已過期,請重新登錄";

    }

    /**
     * 模擬生成 有效負載信息
     *
     * 注:根據自己業務情況的不同,可能會往有效負載中放入不同的信息;
     *    此步驟的邏輯也可能會非常複雜。
     *
     * @date 2019/7/21 16:00
     */
    private PayloadDTO getPayload(String name) {
        return PayloadDTO.builder()
                // 放置過期時長
                .exp(tokenExpirationTime)
                // 放置生效時間
                .nbf(System.currentTimeMillis())
                // 放置用戶信息
                .name(name)
                .birthday("1994-02-05")
                .isAdmin(true)
                .build();
    }
}

測試一下

第一步:啓動項目,並訪問localhost:8080/login?name=鄧沙利文&password=123xyz

 

第二步:以Authorization爲key,以將第一步拿到的token爲value,放入請求Header裏面,並訪問localhost:8080/test

 

此時,我們不妨觀察一下IDEA的控制檯輸出:

token中存放的用戶信息是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=鄧沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效時間是 -> 1563700723248
token的生效時長是 -> 60000
當前時間是 -> 1563700766988
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? true

 

第三步:等一分鐘,讓時間超過我們設置的有效時長(本人設置的是60000毫秒),再按照第二步的方式進行訪問:

此時,我們不妨再觀察一下IDEA的控制檯輸出:

token中存放的用戶信息是 -> PayloadDTO(jti=null, iss=null, exp=60000, sub=null, aud=null, nbf=1563700723248, iat=null, name=鄧沙利文, gender=null, birthday=1994-02-05, isAdmin=true)
token的生效時間是 -> 1563700723248
token的生效時長是 -> 60000
當前時間是 -> 1563701012872
token -> 【eyJhbGciOiJIUzI1NiJ9.eyJiaXJ0aGRheSI6IjE5OTQtMDItMDUiLCJleHAiOjYwMDAwLCJpc0FkbWluIjp0cnVlLCJuYW1lIjoi6YKT5rKZ5Yip5paHIiwibmJmIjoxNTYzNzAwNzIzMjQ4fQ.0nCrMAC8QO6r-dVnvQkzsSDlIWu3dxNwxOLnVs74saU】是否有效? false

 

由此可見,入門級JWT使用並示例成功!

 

^_^ 如有不當之處,歡迎指正

^_^ 參考鏈接
     
         http://andaily.com/blog/?p=956
               https://www.jianshu.com/p/75208a68c3b9
               https://www.sohu.com/a/250972011_575744

^_^ 測試代碼託管鏈接
           
   https://github.com/JustryDeng...er/Abc_JwtToken_Demo

^_^ 本文已經被收錄進《程序員成長筆記(五)》,筆者JustryDeng

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