一、JWT是什么
JWT的全稱為json web token。是一種生成token的方式。一般我們訪問一個系統(tǒng)的流程就是:請求登錄接口,該接口會返回一個token,為了防止對象數(shù)據(jù)被篡改,生成JSON時會加上簽名,請求其他接口都要帶上token,token驗證通過才能訪問成功,而JWT就是生成token的一種機制。
廣義上講JWT是一個標準的名稱;狹義上講JWT指的就是用來傳遞的那個token字符串。
二、JWT的組成
JWT含有三個部分:
- 頭部(header)
- 載荷(payload)
- 簽證(signature)
2.1 頭部(header)
頭部一般有兩部分信息:類型、加密的算法(通常使用HMAC SHA256)
頭部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解密后:
{
"typ":"JWT",
"alg":"HS256"
}
2.2 載荷(payload)
該部分一般存放一些有效的信息。JWT的標準定義包含五個字段:
-
iss:該JWT的簽發(fā)者 -
sub:該JWT所面向的用戶 -
aud:接收該JWT的一方 -
exp(expires):什么時候過期,這里是一個Unit的時間戳 -
iat(issued at):在什么時候簽發(fā)的
2.3簽證(signature)
JWT最后一個部分。該部分是使用了HS256加密后的數(shù)據(jù);包含三個部分:
- header(base64后的)
- payload(base64后的)
- 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>,示意

因為用戶的狀態(tài)在服務端內(nèi)容中是不存儲的,所以這是一種無狀態(tài)的認證機制。服務端的保護路由將會檢查請求頭 Authorization 中的JWT信息,如果合法,則允許用戶的行為。由于JWT是 自包含的,因此,減少了需要查詢數(shù)據(jù)庫的需要。
JWT的這些特征使得我們可以完全依賴無狀態(tài)的特性提供數(shù)據(jù)API服務。因為JWT并不使用Cookie的,所以你可以在任何域名提供你的API服務而不需要擔心跨域資源共享問題(CORS)
3.2 流程介紹:
- 用戶使用賬號和密碼發(fā)出POST登錄請求;
- 服務器使用私鑰創(chuàng)建一個JWT;
- 服務器返回這個JWT給瀏覽器;
- 瀏覽器將該JWT串放在請求頭中向服務器發(fā)送請求;
- 服務器驗證該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ù):

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

注意:這里選擇
Bearer Token類型,就把不要在 Token中手動Bearer,postman會自動拼接。
底線
本文源代碼使用 Apache License 2.0開源許可協(xié)議,可從Gitee代碼地址通過git clone命令下載到本地或者通過瀏覽器方式查看源代碼。