SpringBoot+JWT完成token驗證

一、JWT是什么

JWT的全稱為json web token。是一種生成token的方式。一般我們訪問一個系統(tǒng)的流程就是:請求登錄接口,該接口會返回一個token,為了防止對象數(shù)據(jù)被篡改,生成JSON時會加上簽名,請求其他接口都要帶上token,token驗證通過才能訪問成功,而JWT就是生成token的一種機制。
廣義上講JWT是一個標準的名稱;狹義上講JWT指的就是用來傳遞的那個token字符串。

二、JWT的組成

JWT含有三個部分:

  1. 頭部(header)
  2. 載荷(payload)
  3. 簽證(signature)

2.1 頭部(header)

頭部一般有兩部分信息:類型、加密的算法(通常使用HMAC SHA256)
頭部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解密后:

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

2.2 載荷(payload)

該部分一般存放一些有效的信息。JWT的標準定義包含五個字段:

  1. iss:該JWT的簽發(fā)者
  2. sub:該JWT所面向的用戶
  3. aud:接收該JWT的一方
  4. exp(expires):什么時候過期,這里是一個Unit的時間戳
  5. iat(issued at):在什么時候簽發(fā)的

2.3簽證(signature)

JWT最后一個部分。該部分是使用了HS256加密后的數(shù)據(jù);包含三個部分:

  1. header(base64后的)
  2. payload(base64后的)
  3. secret 私鑰
    secret是保存在服務器端的,JWT的簽發(fā)生成也是在服務器端的,secret就是用來進行JWT的簽發(fā)和JWT的驗證,所以,它就是你服務端的秘鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret,那就意味著客戶端可以自我簽發(fā)JWT了。

2.4 解密調(diào)試器

官網(wǎng)的加密串解密調(diào)試器:https://jwt.io/#debugger-io

三、如何使用JWT?

3.1 工作原理

在身份鑒定的實現(xiàn)中,傳統(tǒng)的方法是在服務端存儲一個 session,但是如果是大并發(fā)系統(tǒng)服務器內(nèi)存占用將不可控,給客戶端返回一個 cookie。
而使用JWT之后,當用戶使用它的認證信息登錄系統(tǒng)之后,會返回給用戶一個JWT, 用戶只需要本地保存該 token(通常使用localStorage,也可以使用cookie)即可。

當用戶希望訪問一個受保護的路由或者資源的時候,通常應該在Authorization頭部使用Bearer模式添加JWT,其內(nèi)容格式:

Authorization: Bearer <token>

請求header形如:

fetch('api/users', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

Postman發(fā)起JWT請求Header的key為Authorization,Header的value為Bearer <token>,示意

image.png

因為用戶的狀態(tài)在服務端內(nèi)容中是不存儲的,所以這是一種無狀態(tài)的認證機制。服務端的保護路由將會檢查請求頭 Authorization 中的JWT信息,如果合法,則允許用戶的行為。由于JWT是 自包含的,因此,減少了需要查詢數(shù)據(jù)庫的需要。

JWT的這些特征使得我們可以完全依賴無狀態(tài)的特性提供數(shù)據(jù)API服務。因為JWT并不使用Cookie的,所以你可以在任何域名提供你的API服務而不需要擔心跨域資源共享問題(CORS)

3.2 流程介紹:

  1. 用戶使用賬號和密碼發(fā)出POST登錄請求;
  2. 服務器使用私鑰創(chuàng)建一個JWT;
  3. 服務器返回這個JWT給瀏覽器;
  4. 瀏覽器將該JWT串放在請求頭中向服務器發(fā)送請求;
  5. 服務器驗證該J

四、搭建SpringBoot + JWT工程

4.1 新建module

引入依賴,完整POM

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.erbadagang.springboot.jwt</groupId>
    <artifactId>jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwt</name>
    <description>JWT project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.0.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--  JWT token-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.2 application.yml

在工程application.yml配置文件中添加JWT的配置信息:

#####JWT配置#####
audience:
  # 代表這個JWT的接收對象,存入audience
  aud: 98f6bcd4621d37
  # 密鑰, 經(jīng)過Base64加密, 可自行替換。Base64加解密工具:http://tool.chinaz.com/Tools/Base64.aspx
  base64Secret: Z3VveGl1emhpRXJiYWRhZ2FuZ1dpbnNwYWNlVjMuMA==
  # JWT的簽發(fā)主體,存入issuer
  iss: issued by 郭秀志
  # 過期時間毫秒
  expiresSecond: 172800

Base64加解密工具:http://tool.chinaz.com/Tools/Base64.aspx

4.3 Audience

新建配置信息的實體類,以便獲取JWT配置:

package com.erbadagang.springboot.jwt.model;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * @description 配置信息DTO,通過讀取配置文件自動賦值。
 * @ClassName: Audience
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/16 20:07    
 * @Copyright:  
 */
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
    //代表這個JWT的接收對象,存入audience
    private String aud;
    private String base64Secret;
    //JWT的簽發(fā)主體,存入issuer
    private String iss;
    private int expiresSecond;

}

4.4 創(chuàng)建JWT工具類

package com.erbadagang.springboot.jwt.util;

import com.erbadagang.springboot.jwt.common.exception.CustomException;
import com.erbadagang.springboot.jwt.common.response.ResultCode;
import com.erbadagang.springboot.jwt.model.Audience;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;

/**
 * @description JWT工具類,生成及解析token
 * @ClassName: JwtTokenUtil
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/16 16:55
 * @Copyright:
 */
@Slf4j
public class JwtTokenUtil {

    public static final String AUTH_HEADER_KEY = "Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";

    /**
     * 解析jwt
     *
     * @param jsonWebToken
     * @param base64Security
     * @return
     */
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (ExpiredJwtException eje) {
            log.error("===== Token過期 =====", eje);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED);
        } catch (Exception e) {
            log.error("===== token解析異常 =====", e);
            throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
        }
    }

    /**
     * 構建jwt
     *
     * @param userId
     * @param username
     * @param role
     * @param audience
     * @return
     */
    public static String createJWT(String userId, String username, String role, Audience audience) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //生成簽名密鑰
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //添加構成JWT的參數(shù)
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以將基本不重要的對象信息放到claims
                    .claim("role", role)
                    .claim("userId", userId)
                    .setSubject(username)           // 代表這個JWT的主體,即它的所有人
                    .setIssuer(audience.getIss())              // 代表這個JWT的簽發(fā)主體;
                    .setIssuedAt(new Date())        // 是一個時間戳,代表這個JWT的簽發(fā)時間;
                    .setAudience(audience.getAud())          // 代表這個JWT的接收對象;
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token過期時間
            int TTLMillis = audience.getExpiresSecond();
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // 是一個時間戳,代表這個JWT的過期時間;
                        .setNotBefore(now); // 是一個時間戳,代表這個JWT生效的開始時間,意味著在這個時間之前驗證JWT是會失敗的
            }

            //生成JWT
            return builder.compact();
        } catch (Exception e) {
            log.error("簽名失敗", e);
            throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
        }
    }

    /**
     * 從token中獲取用戶名
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUsername(String token, String base64Security) {
        return parseJWT(token, base64Security).getSubject();
    }

    /**
     * 從token中獲取用戶ID
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static String getUserId(String token, String base64Security) {
        String userId = parseJWT(token, base64Security).get("userId", String.class);
        return Base64Util.decode(userId);
    }

    /**
     * 是否已過期
     *
     * @param token
     * @param base64Security
     * @return
     */
    public static boolean isExpiration(String token, String base64Security) {
        return parseJWT(token, base64Security).getExpiration().before(new Date());
    }
}

4.5 攔截器

JWT驗證主要是通過過濾器驗證,所以我們需要添加一個攔截器來演請求頭中是否包含有后臺頒發(fā)的 token,這里請求頭的格式:

Authorization: Bearer <token>
package com.erbadagang.springboot.jwt.interceptor;

import com.erbadagang.springboot.jwt.annotation.JwtIgnore;
import com.erbadagang.springboot.jwt.common.exception.CustomException;
import com.erbadagang.springboot.jwt.common.response.ResultCode;
import com.erbadagang.springboot.jwt.model.Audience;
import com.erbadagang.springboot.jwt.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * token驗證攔截器
 */
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private Audience audience;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 忽略帶JwtIgnore注解的請求, 不做后續(xù)token認證校驗
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 獲取請求頭信息authorization信息
        final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        log.info("## authHeader= {}", authHeader);

        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
            log.info("### 用戶未登錄,請先登錄 ###");
            throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
        }

        // 獲取token
        final String token = authHeader.substring(7);

        if (audience == null) {
            BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
            audience = (Audience) factory.getBean("audience");
        }

        // 驗證token是否有效--無效已做異常拋出,由全局異常處理后返回對應信息
        JwtTokenUtil.parseJWT(token, audience.getBase64Secret());

        return true;
    }

}

4.6 配置攔截器

package com.erbadagang.springboot.jwt.config;

import com.erbadagang.springboot.jwt.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * 添加攔截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //攔截路徑可自行配置多個 可用 ,分隔開
        registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
    }

    /**
     * 跨域支持
     *
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }

}

這里JWT可能會有跨域問題,配置跨域支持。

4.7 編寫測試Controller接口

package com.erbadagang.springboot.jwt.controller;

import com.alibaba.fastjson.JSONObject;
import com.erbadagang.springboot.jwt.annotation.JwtIgnore;
import com.erbadagang.springboot.jwt.common.response.Result;
import com.erbadagang.springboot.jwt.model.Audience;
import com.erbadagang.springboot.jwt.util.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

@Slf4j
@RestController
public class AdminUserController {

    @Autowired
    private Audience audience;

    @PostMapping("/login")
    @JwtIgnore
    public Result adminLogin(HttpServletResponse response, String username, String password) {
        // 這里模擬測試, 默認登錄成功,返回用戶ID和角色信息
        String userId = UUID.randomUUID().toString();
        String role = "admin";

        // 創(chuàng)建token
        String token = JwtTokenUtil.createJWT(userId, username, role, audience);
        log.info("### 登錄成功, token={} ###", token);

        // 將token放在響應頭
        response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
        // 將token響應給客戶端
        JSONObject result = new JSONObject();
        result.put("token", token);
        return Result.SUCCESS(result);
    }

    @GetMapping("/users")
    public Result userList() {
        log.info("### 查詢所有用戶列表 ###");
        return Result.SUCCESS();
    }
}

4.8 測試

接下來我們使用PostMan工具進行測試。

4.8.1 未登錄

沒有登錄時候直接訪問:http://localhost:8080/users 接口:

未登錄提示

4.8.2 登錄

訪問登錄接口,模擬用戶guoxiuzhi登錄:
http://localhost:8080/login?username=guoxiuzhi&password=12345678

成功登錄信息

其中的token信息:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiYWRtaW4iLCJ1c2VySWQiOiJkZWIxODhmMS1mYTZiLTQ0MDktYTVlNC1hMTY5ZmJkNGVhOWIiLCJzdWIiOiJndW94aXV6aGkiLCJpc3MiOiJpc3N1ZWQgYnkg6YOt56eA5b-XIiwiaWF0IjoxNTk0OTAzMTgxLCJhdWQiOiI5OGY2YmNkNDYyMWQzNyIsImV4cCI6MTU5NDkwMzQ4MSwibmJmIjoxNTk0OTAzMTgxfQ.ry1h3Lpq_GqkIjGUARYNSBhDctb0wgk4b0N6PIGizUM

該token可以在JWT官網(wǎng)的加密串解密調(diào)試器:https://jwt.io/#debugger-io進行解密查看數(shù)據(jù):

解密token數(shù)據(jù)

4.8.3 帶token訪問

攜帶生成token再次訪問:http://localhost:8080/users 接口

成功返回

注意:這里選擇Bearer Token類型,就把不要在 Token中手動Bearer,postman會自動拼接。

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議,可從Gitee代碼地址通過git clone命令下載到本地或者通過瀏覽器方式查看源代碼。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容