JWT官網(wǎng): https://jwt.io/
JWT(Java版)的github地址:https://github.com/jwtk/jjwt
什么是JWT
Json Web Token(JWT):JSON網(wǎng)絡(luò)令牌臭胜,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而制定的一種基于JSON的開放標(biāo)準(zhǔn)((RFC 7519)哆档。JWT是一個輕便的安全跨平臺傳輸格式,定義了一個緊湊的自包含的方式用于通信雙方之間以 JSON 對象行使安全的傳遞信息庐冯。因為數(shù)字簽名的存在侣监,這些信息是可信的辜伟。
廣義上講JWT是一個標(biāo)準(zhǔn)的名稱弛随;狹義上講JWT指的就是用來傳遞的那個token字符串腮郊。
JWT的組成
JWT含有三個部分:
- 頭部(header)
- 載荷(payload)
- 簽證(signature)
頭部(header)
頭部一般有兩部分信息:類型
抄谐、加密的算法
(通常使用HMAC SHA256)
頭部一般使用base64加密:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
解密后:
{
"typ":"JWT",
"alg":"HS256"
}
載荷(payload)
該部分一般存放一些有效的信息渺鹦。JWT的標(biāo)準(zhǔn)定義包含五個字段:
- iss:該JWT的簽發(fā)者
- sub:該JWT所面向的用戶
- aud:接收該JWT的一方
- exp(expires):什么時候過期,這里是一個Unit的時間戳
- iat(issued at):在什么時候簽發(fā)的
簽證(signature)
JWT最后一個部分蛹含。該部分是使用了HS256加密后的數(shù)據(jù)毅厚;包含三個部分:
- header(base64后的)
- payload(base64后的)
- secret 私鑰
secret
是保存在服務(wù)器端
的,JWT的簽發(fā)生成也是在服務(wù)器端的浦箱,secret
就是用來進(jìn)行JWT的簽發(fā)
和JWT的驗證
吸耿,所以祠锣,它就是你服務(wù)端的秘鑰
,在任何場景都不應(yīng)該流露出去咽安。一旦客戶端得知這個secret伴网,那就意味著客戶端可以自我簽發(fā)JWT了。
JWT特點
- 緊湊:意味著這個字符串很小妆棒,甚至可以放在URL參數(shù)澡腾,POST Parameter中以Http Header的方式傳輸。
- 自包含:傳輸?shù)淖址芏嘈畔⒏馍海瑒e人拿到以后就不需要多次訪問數(shù)據(jù)庫獲取信息动分,而且通過其中的信息就可以知道加密類型和方式(當(dāng)然解密需要公鑰和密鑰)。
如何使用JWT红选?
在身份鑒定的實現(xiàn)中澜公,傳統(tǒng)的方法是在服務(wù)端存儲一個 session
,給客戶端返回一個 cookie
喇肋,而使用JWT之后坟乾,當(dāng)用戶使用它的認(rèn)證信息登錄系統(tǒng)之后,會返回給用戶一個JWT
苟蹈, 用戶只需要本地保存該 token
(通常使用localStorage糊渊,也可以使用cookie)即可。
當(dāng)用戶希望訪問一個受保護(hù)的路由或者資源的時候慧脱,通常應(yīng)該在 Authorization
頭部使用 Bearer
模式添加JWT渺绒,其內(nèi)容格式:
Authorization: Bearer <token>
因為用戶的狀態(tài)在服務(wù)端內(nèi)容中是不存儲
的,所以這是一種無狀態(tài)
的認(rèn)證機(jī)制菱鸥。服務(wù)端的保護(hù)路由將會檢查請求頭 Authorization
中的JWT信息宗兼,如果合法,則允許用戶的行為氮采。由于JWT是 自包含
的殷绍,因此,減少了需要查詢數(shù)據(jù)庫的需要鹊漠。
JWT的這些特征使得我們可以完全依賴無狀態(tài)的特性提供數(shù)據(jù)API服務(wù)主到。因為JWT并不使用Cookie的,所以你可以在任何域名提供你的API服務(wù)而不需要擔(dān)心跨域資源共享問題(CORS)
下面的序列圖展示了該過程:
中文流程介紹:
- 用戶使用賬號和密碼發(fā)出POST登錄請求躯概;
- 服務(wù)器使用私鑰創(chuàng)建一個JWT登钥;
- 服務(wù)器返回這個JWT給瀏覽器;
- 瀏覽器將該JWT串放在請求頭中向服務(wù)器發(fā)送請求娶靡;
- 服務(wù)器驗證該JWT牧牢;
- 返回響應(yīng)的資源給瀏覽器。
說了這么多JWT到底如何應(yīng)用到我們的項目中,下面我們就使用SpringBoot 結(jié)合 JWT完成用戶的登錄驗證塔鳍。
應(yīng)用流程
-
初次登錄生成JWT流程圖
-
用戶訪問資源流程圖
搭建SpringBoot + JWT工程
下面通過代碼來實現(xiàn)用戶認(rèn)證的功能伯铣,博主這里主要采用Spring Boot與JWT整合的方式實現(xiàn)。關(guān)于Spring Boot項目如何搭建與使用本章不做詳細(xì)介紹轮纫。
- 首先引入JWT依賴:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- 在工程 application.yml 配置文件中添加JWT的配置信息:
##jwt配置
audience:
# 代表這個JWT的接收對象,存入audience
clientId: 098f6bcd4621d373cade4e832627b4f6
# 密鑰, 經(jīng)過Base64加密, 可自行替換
base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
# JWT的簽發(fā)主體腔寡,存入issuer
name: restapiuser
# 過期時間,時間戳
expiresSecond: 172800
- 新建配置信息的實體類蜡感,以便獲取JWT配置:
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
private String clientId;
private String base64Secret;
private String name;
private int expiresSecond;
}
JWT驗證主要是通過過濾器驗證蹬蚁,所以我們需要添加一個攔截器來演請求頭中是否包含有后臺頒發(fā)的 token
,這里請求頭的格式:
Authorization: Bearer <token>
- 創(chuàng)建JWT工具類:
package com.thtf.util;
import com.thtf.common.exception.CustomException;
import com.thtf.common.response.ResultCode;
import com.thtf.model.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;
/**
* ========================
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/17 17:24
* Version: v1.0
* ========================
*/
public class JwtTokenUtil {
private static Logger log = LoggerFactory.getLogger(JwtTokenUtil.class);
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);
}
}
/**
* 構(gòu)建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());
//userId是重要信息郑兴,進(jìn)行加密下
String encryId = Base64Util.encode(userId);
//添加構(gòu)成JWT的參數(shù)
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
// 可以將基本不重要的對象信息放到claims
.claim("role", role)
.claim("userId", userId)
.setSubject(username) // 代表這個JWT的主體犀斋,即它的所有人
.setIssuer(audience.getClientId()) // 代表這個JWT的簽發(fā)主體;
.setIssuedAt(new Date()) // 是一個時間戳情连,代表這個JWT的簽發(fā)時間叽粹;
.setAudience(audience.getName()) // 代表這個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());
}
}
- 創(chuàng)建JWT驗證攔截器:
package com.thtf.interceptor;
import com.thtf.annotation.JwtIgnore;
import com.thtf.common.exception.CustomException;
import com.thtf.common.response.ResultCode;
import com.thtf.model.Audience;
import com.thtf.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驗證攔截器
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/18 9:46
* Version: v1.0
* ========================
*/
@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)證校驗
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是否有效--無效已做異常拋出,由全局異常處理后返回對應(yīng)信息
JwtTokenUtil.parseJWT(token, audience.getBase64Secret());
return true;
}
}
- 配置攔截器:
package com.thtf.config;
import com.thtf.interceptor.JwtInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
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;
/**
* ========================
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/18 10:37
* Version: v1.0
* ========================
*/
@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可能會有跨域問題啡氢,配置跨域支持。
- 編寫測試Controller接口:
package com.thtf.controller;
import com.alibaba.fastjson.JSONObject;
import com.thtf.annotation.JwtIgnore;
import com.thtf.common.response.Result;
import com.thtf.model.Audience;
import com.thtf.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;
/**
* ========================
* Created with IntelliJ IDEA.
* User:pyy
* Date:2019/7/18 10:41
* Version: v1.0
* ========================
*/
@Slf4j
@RestController
public class AdminUserController {
@Autowired
private Audience audience;
@PostMapping("/login")
@JwtIgnore
public Result adminLogin(HttpServletResponse response, String username,String password) {
// 這里模擬測試, 默認(rèn)登錄成功术裸,返回用戶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放在響應(yīng)頭
response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
// 將token響應(yīng)給客戶端
JSONObject result = new JSONObject();
result.put("token", token);
return Result.SUCCESS(result);
}
@GetMapping("/users")
public Result userList() {
log.info("### 查詢所有用戶列表 ###");
return Result.SUCCESS();
}
}
- 接下來我們使用PostMan工具進(jìn)行測試:
沒有登錄時候直接訪問:http://localhost:8080/users 接口:
執(zhí)行登錄:
攜帶生成token再次訪問:http://localhost:8080/users 接口
注意
:這里選擇 Bearer Token
類型倘是,就把不要在 Token
中手動Bearer
,postman會自動拼接袭艺。
源碼下載地址:https://github.com/pyygithub/JWT-DEMO.git
作者:一行代碼一首詩
鏈接:http://www.reibang.com/p/430cd44a2796
來源:簡書
簡書著作權(quán)歸作者所有搀崭,任何形式的轉(zhuǎn)載都請聯(lián)系作者獲得授權(quán)并注明出處。