引言:什么是jwt
Json Web Token(JWT):JSON網(wǎng)絡(luò)令牌,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而制定的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519)宫仗。JWT是一個(gè)輕便的安全跨平臺傳輸格式贮庞,定義了一個(gè)緊湊的自包含的方式用于通信雙方之間以 JSON 對象行使安全的傳遞信息示惊。因?yàn)閿?shù)字簽名的存在急迂,這些信息是可信的匿值。
JWT的組成
jwt含有三個(gè)部分
- 頭部(header)
- 載荷(payload)
- 簽證(signature)
頭部(header)
頭部一般有兩部分信息:類型垄琐、加秘的算法(通常使用HMAC SHA256)
載荷(payload)
該部分一般存放一些有效的信息边酒。jwt的標(biāo)準(zhǔn)定義包含五個(gè)字段:
- iss: jwt的簽發(fā)者
- sub:jwt所面向的用戶
- aud:接收該jwt的一方
- exp(expires):什么時(shí)候過期,這里是一個(gè)Unit的時(shí)間戳
- iat(issued at):在什么時(shí)候簽發(fā)的
簽證(signature)
jwt最后一個(gè)部分狸窘。該部分是使用了HS256加密后的數(shù)據(jù)墩朦;包含了三個(gè)部分:
- header(base64后的)
- payload(base64后的)
- secret 私鑰
secret
是保存在服務(wù)器端(server)
的,jwt的簽發(fā)生成也是在服務(wù)器端的翻擒,secret
就是用來進(jìn)行jwt的簽發(fā)
和jwt的驗(yàn)證
氓涣。所以,它就是服務(wù)端的密鑰
陋气,z在任何場景都不應(yīng)該流露出去劳吠。一旦客戶端得知這個(gè)secret
,那就有客戶端自我簽發(fā)jwt的安全危險(xiǎn)了恩伺。
jwt特點(diǎn)
- 緊湊: 意味著這個(gè)字符串很小赴背,甚至可以放在URL參數(shù),POST Parameter中以Http Header的方式傳輸。
- 自包含: 傳輸?shù)淖址芏嘈畔⒒思裕瑒e人拿到以后就不需要多次訪問數(shù)據(jù)庫獲取信息燃观,并且通過其中的信息就可以知道加密類型和方式(當(dāng)然解密需要公鑰和密鑰)。
如何使用jwt
在身份鑒定的實(shí)現(xiàn)中便瑟,傳統(tǒng)的方法是在服務(wù)端存儲一個(gè) session
缆毁,給客戶端返回一個(gè) cookie
,而使用JWT之后到涂,當(dāng)用戶使用它的認(rèn)證信息登錄系統(tǒng)之后脊框,會(huì)返回給用戶一個(gè)JWT(token)
, 用戶只需要本地保存該 token(通常使用localStorage践啄,也可以使用cookie)
即可浇雹。
當(dāng)用戶希望訪問一個(gè)受保護(hù)的路由或者資源的時(shí)候,通常應(yīng)該在 Authorization
頭部使用 Bearer
模式添加JWT屿讽,其內(nèi)容格式:
Authorization: Bearer <token>
因?yàn)橛脩舻臓顟B(tài)在服務(wù)端內(nèi)容中是不存儲的
昭灵,所以這是一種無狀態(tài)
的認(rèn)證機(jī)制。服務(wù)端的保護(hù)路由將會(huì)檢查請求頭 Authorization
中的JWT信息伐谈,如果合法烂完,則允許用戶的行為。由于JWT是自包含
的诵棵,因此抠蚣,減少了需要查詢數(shù)據(jù)庫的需要。
JWT的這些特征使得我們可以完全依賴無狀態(tài)的特性提供數(shù)據(jù)API服務(wù)履澳。因?yàn)镴WT并不使用Cookie的嘶窄,所以你可以在任何域名提供你的API服務(wù)而不需要擔(dān)心跨域資源共享問題(CORS)
下面的序列圖展示了該過程:
中文流程介紹:
- 用戶使用賬號和密碼發(fā)出POST登錄請求;
- 服務(wù)器使用私鑰創(chuàng)建一個(gè)JWT距贷;
- 服務(wù)器返回這個(gè)JWT給瀏覽器护侮;
- 瀏覽器將該JWT串放在請求頭中向服
務(wù)器發(fā)送請求; - 服務(wù)器驗(yàn)證該JWT储耐;
- 返回響應(yīng)的資源給瀏覽器。
說了這么多JWT到底如何應(yīng)用到我們的項(xiàng)目中滨溉,下面我們就使用SpringBoot 結(jié)合 JWT完成用戶的登錄驗(yàn)證什湘。
應(yīng)用
-
初次登錄生成jwt流程圖
登錄生成jwt流程圖 -
用戶訪問資源流程圖
用戶訪問資源流程圖
集成
壞境:
- spring boot 2.2.4.RELEASE
-
jjwt 0.9.1
其它工具版本可以查看我gitee上pom.xml的依賴。
下面通過代碼來實(shí)現(xiàn)用戶認(rèn)證的功能晦攒,博主這里主要采用Spring Boot與JWT整合的方式實(shí)現(xiàn)
1闽撤、我的pom.xml相關(guān)依賴版本代碼
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jinzheyi</groupId>
<artifactId>jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>jwt</name>
<description>Spring Boot集成jwt</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--引入jwt依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<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-tomcat</artifactId>
<scope>provided</scope>
</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>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、在工程 application.yml 配置文件中添加JWT的配置信息:
##jwt配置
audience:
#代表這個(gè)jwt的接收對象脯颜,存入audience
clientId: 098f6bcd4621d373cade4e832627b4f6
#密鑰哟旗,經(jīng)過Base64加密,可自行替換
base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
# JWT的簽發(fā)主體,存入issuer
name: restapiuser
# 過期時(shí)間闸餐,時(shí)間戳
expiresSecond: 172800
server:
port: 8081
3饱亮、新建配置信息的實(shí)體類,以便獲取JWT配置:
package com.jinzheyi.jwt.entity;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
*@Description 創(chuàng)建的關(guān)于yml文件中jwt配置屬性的實(shí)體類
*@Author jinzheyi
*@Date 2020/2/15 22:49
*@version 0.1
*/
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
//客戶端id
private String clientId;
//經(jīng)過base64加密后密鑰
private String base64Secret;
//簽發(fā)主題
private String name;
//過期時(shí)間
private int expiresSecond;
}
JWT驗(yàn)證主要是通過過濾器驗(yàn)證舍沙,所以我們需要添加一個(gè)攔截器來演請求頭中是否包含有后臺頒發(fā)的 token
4近上、創(chuàng)建JWT驗(yàn)證攔截器(進(jìn)行了一層封裝):
package com.jinzheyi.jwt.config.interceptor;
import com.jinzheyi.jwt.common.Constant;
import com.jinzheyi.jwt.common.annotation.JwtIgnore;
import com.jinzheyi.jwt.common.exception.CustomException;
import com.jinzheyi.jwt.common.response.ResultCode;
import com.jinzheyi.jwt.entity.Audience;
import com.jinzheyi.jwt.utils.JwtUtil;
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;
/**
*@Description 封裝好的jwt攔截器處理類
*@Author jinzheyi
*@Date 2020/2/16 14:00
*@version 0.1
*/
@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認(rèn)證校驗(yàn)
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(Constant.AUTH_HEADER_KEY);
log.info("##authHeader = {}",authHeader);
if (StringUtils.isBlank(authHeader)||!authHeader.startsWith(Constant.TOKEN_PREFIX)) {
log.info("###用戶未登錄拂铡,請先登錄###");
throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
}
//獲取token
final String token = authHeader.substring(7);
if (audience == null) {
BeanFactory beanFactory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
audience = (Audience) beanFactory.getBean("audience");
}
JwtUtil.parseJwt(token, audience.getBase64Secret());
return true;
}
}
package com.jinzheyi.jwt.config;
import com.jinzheyi.jwt.config.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;
/**
*@Description 全局配置公共類壹无,含攔截器、允許跨域請求處理
*@Author jinzheyi
*@Date 2020/2/16 14:01
*@version 0.1
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 添加攔截器
* @param interceptorRegistry
*/
@Override
public void addInterceptors(InterceptorRegistry interceptorRegistry){
//攔截路徑可自行配置多個(gè) 可用感帅,分隔開
interceptorRegistry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
}
/**
* 跨域支持
* @param corsRegistry
*/
@Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
}
5斗锭、然后我們創(chuàng)建JWT工具類:
package com.jinzheyi.jwt.utils;
import com.jinzheyi.jwt.common.exception.CustomException;
import com.jinzheyi.jwt.common.response.ResultCode;
import com.jinzheyi.jwt.entity.Audience;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
/**
*@Description jwt工具類
*@Author jinzheyi
*@Date 2020/2/16 14:03
*@version 0.1
*/
public class JwtUtil {
//創(chuàng)建日志對象
private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);
/**
* 解析 jwt
* @param jsonWebToken token
* @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 e){
logger.error("===token過期===");
throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED_EXCEPTION);
} catch (MalformedJwtException e){
logger.error("====json web token格式錯(cuò)誤====");
throw new CustomException(ResultCode.PERMISSION_TOKEN_MALFORMEDJWT_EXCEPTION);
}catch (Exception e){
logger.error("=====token解析異常=====");
throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
}
}
/**
* 構(gòu)建jwt
* @param userId 用戶id
* @param username jwt的所有者
* @param role 權(quán)限
* @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());
//userId是重要的信息,進(jìn)行簡要加密下
String encryId = Base64Util.encode(userId);
//添加構(gòu)成JWT的參數(shù)
JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("type", "JWT")
//可以將基本不重要的對象信息放到claims
.claim("role", role)
.claim("userId", encryId)
.setSubject(username) //代表這個(gè)jwt的主體失球,即它的所有人
.setIssuer(audience.getClientId()) //代表這個(gè)jwt的簽發(fā)主體
.setIssuedAt(new Date()) //一個(gè)時(shí)間戳岖是,代表簽發(fā)時(shí)間
.setAudience(audience.getName()) //代表jwt的接收對象
.signWith(signatureAlgorithm, signingKey);
//添加token過期時(shí)間
int expiresMillis = audience.getExpiresSecond();
if (expiresMillis >= 0) {
long expMillis = nowMillis + expiresMillis;
Date expireTime = new Date(expMillis);
jwtBuilder.setExpiration(expireTime) //是一個(gè)時(shí)間戳,代表jwt的過期時(shí)間
.setNotBefore(now); //是一個(gè)時(shí)間戳她倘,代表這個(gè)jwt生效的開始時(shí)間璧微,意味在這個(gè)時(shí)間之前驗(yàn)證jwt都是無效的
}
return jwtBuilder.compact();
} catch (Exception e){
logger.error("生成簽名失敗");
throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
}
}
/**
* 獲取用戶
* @param token
* @param base64Security
* @return
*/
public static String getUsername(String token, String base64Security){
return parseJwt(token, base64Security).getSubject();
}
/**
* 獲取用戶id
* @param token
* @param base64Security
* @return
*/
public static String getUserId(String token, String base64Security){
return Base64Util.decode(parseJwt(token, base64Security).get("userId",String.class));
}
/**
* token是否過期
* @param token
* @param base64Security
* @return
*/
public static boolean isExpiration(String token, String base64Security){
return parseJwt(token, base64Security).getExpiration().before(new Date());
}
}
其中創(chuàng)建jwt工具類的時(shí)候,內(nèi)部使用到的一些封裝方法硬梁,可以到我的gitee分享的代碼里面進(jìn)行查看前硫。
6、添加全局異常處理
package com.jinzheyi.jwt.common.exception;
import com.jinzheyi.jwt.common.response.ResultCode;
import com.jinzheyi.jwt.common.response.ResultData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
/**
*@Description 定義全局異常處理類
*@Author jinzheyi
*@Date 2020/2/16 13:58
*@version 0.1
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
public static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 處理自定義異常
* @param e
* @return
*/
@ExceptionHandler(CustomException.class)
public ResultData handleException(CustomException e){
// 打印異常信息
logger.error("### 異常信息:{} ###", e.getMessage());
return new ResultData(e.getResultCode());
}
/**
* 參數(shù)錯(cuò)誤異常
* @param e
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public ResultData handleException(Exception e) {
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
BindingResult result = validException.getBindingResult();
StringBuffer errorMsg = new StringBuffer();
if (result.hasErrors()) {
List<ObjectError> errors = result.getAllErrors();
errors.forEach(p ->{
FieldError fieldError = (FieldError) p;
errorMsg.append(fieldError.getDefaultMessage()).append(",");
logger.error("### 請求參數(shù)錯(cuò)誤:{"+fieldError.getObjectName()+"},field{"+fieldError.getField()+ "},errorMessage{"+fieldError.getDefaultMessage()+"}");
});
}
} else if (e instanceof BindException) {
BindException bindException = (BindException)e;
if (bindException.hasErrors()) {
logger.error("### 請求參數(shù)錯(cuò)誤: {}", bindException.getAllErrors());
}
}
return new ResultData(ResultCode.PARAM_IS_INVALID);
}
/**
* 處理所有不可知的異常
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResultData handleOtherException(Exception e){
//打印異常堆棧信息
e.printStackTrace();
// 打印異常信息
logger.error("### 不可知的異常:{} ###", e.getMessage());
return new ResultData(ResultCode.SYSTEM_INNER_ERROR);
}
}
7荧止、最后添加用戶Controller進(jìn)行測試
package com.jinzheyi.jwt.web.controller;
import com.alibaba.fastjson.JSONObject;
import com.jinzheyi.jwt.common.Constant;
import com.jinzheyi.jwt.common.annotation.JwtIgnore;
import com.jinzheyi.jwt.common.response.ResultData;
import com.jinzheyi.jwt.entity.Audience;
import com.jinzheyi.jwt.utils.JwtUtil;
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;
/**
*@Description 用戶業(yè)務(wù)控制層
*@Author jinzheyi
*@Date 2020/2/16 14:03
*@version 0.1
*/
@Slf4j
@RestController
public class UserController {
@Autowired
private Audience audience;
@PostMapping("/login")
@JwtIgnore
public ResultData login(HttpServletResponse response, String username, String password){
//這里模擬測試屹电,默認(rèn)登錄成功,返回用戶id和角色信息
String userId = UUID.randomUUID().toString();
String role = "admin";
//創(chuàng)建token
String token = JwtUtil.createJwt(userId, username, role, audience);
log.info("### 登錄成功, token={} ###", token);
response.setHeader(Constant.AUTH_HEADER_KEY, Constant.TOKEN_PREFIX + token);
//將token響應(yīng)給客戶端
JSONObject result = new JSONObject();
result.put("token", token);
return ResultData.successResultData(result);
}
@GetMapping("/users")
public ResultData userList() {
log.info("### 查詢所有用戶列表 ###");
return ResultData.successResult();
}
}
8跃巡、接下來我們使用PostMan工具進(jìn)行測試:
沒有登錄時(shí)候直接訪問:http://localhost:8081/users 接口:
然后我們?nèi)?zhí)行登錄接口:http://localhost:8081/login?username=zhangsan接口
此時(shí)我們獲取到了后臺服務(wù)器生成的token危号。將token帶入到獲取用戶的接口中,如圖
注意
:這里選擇 Bearer Token
類型素邪,就把不要在 Token中手動(dòng)Bearer外莲,postman會(huì)自動(dòng)拼接。補(bǔ)充
:如果帶入錯(cuò)誤的token會(huì)提示token已失效兔朦,或者超時(shí)之后帶入token偷线,會(huì)提示已超時(shí)。攔截器里面已經(jīng)做了判斷沽甥。登錄接口不用驗(yàn)證token是因?yàn)槲覀冊赾ontroller層login方法加了@JwtIgnore
注解
作者:金哲一(jinzheyi)【筆名】
本文代碼地址:https://gitee.com/jinzheyi/springboot/tree/master/springboot2.x
本文鏈接:http://www.reibang.com/p/92e96634746d