JWT使用詳解
技術標籤:JWT
目錄
1、什麼是JWT
(1)、JWT簡介
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with theHMAC
Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。
(2)、JWT與傳統的seesion的區別是什麼
在網際網路應用中,http協議本身是一種無狀態的協議(無狀態協議就是每次請求服務端都是一個新的請求,伺服器端無法知曉請求是否來自一個客戶端甚至某個使用者),例如登入系統,使用者使用使用者名稱和密碼進行登入認證,由於http請求為無狀態,那麼使用者下一次請求服務端還得讓使用者登入,為什麼識別是否是同一使用者,伺服器端需要儲存使用者的資訊,然後把登入資訊返回給瀏覽器(傳統做法就是生成seesionId,然後往session裡面存入使用者資訊---應用伺服器快取),這樣下一次客戶端請求帶上sessionid,這樣伺服器就根據seesionid去快取中查詢此id對應的使用者資訊,用來識別是否為同一使用者。我們的認證使應用本身很難得到擴充套件,隨著不同客戶端使用者的增加,獨立的伺服器已無法承載更多的使用者,而這時候基於session認證應用的問題就會暴露出來.
(3)、基於session認證所顯露的問題
Session: 每個使用者經過我們的應用認證之後,我們的應用都要在服務端做一次記錄,以方便使用者下次請求的鑑別,通常而言session都是儲存在記憶體中,而隨著認證使用者的增多,服務端的開銷會明顯增大。
擴充套件性: 使用者認證之後,服務端做認證記錄,如果認證的記錄被儲存在記憶體中的話,這意味著使用者下次請求還必須要請求在這臺伺服器上,這樣才能拿到授權的資源,這樣在分散式的應用上,相應的限制了負載均衡器的能力。這也意味著限制了應用的擴充套件能力。分散式系統下,還需要做session同步。
CSRF: 因為是基於cookie來進行使用者識別的, cookie如果被截獲,使用者就會很容易受到跨站請求偽造的攻擊。
2、JWT能幫們做什麼,為什麼需要JWT
基於token的鑑權機制類似於http協議也是無狀態的,它不需要在服務端去保留使用者的認證資訊或者會話資訊。這就意味著基於token認證機制的應用不需要去考慮使用者在哪一臺伺服器登入了,這就為應用的擴充套件提供了便利。
流程上是這樣的:
- 使用者使用使用者名稱密碼來請求伺服器
- 伺服器進行驗證使用者的資訊
- 伺服器通過驗證傳送給使用者一個token
- 客戶端儲存token,並在每次請求時附送上這個token值(一般請求在請求頭加入token內容)
- 服務端驗證token值,並返回資料
這個token必須要在每次請求時傳遞給服務端,它應該儲存在請求頭裡, 另外,服務端要支援CORS(跨來源資源共享)
策略,一般我們在服務端這麼做就可以了Access-Control-Allow-Origin: *(實現跨域訪問)
。
3、JWT的結構
(1)JWT的結構
JWT是由三段資訊構成的,將這三段資訊文字用.
連結一起就構成了Jwt字串。就像這樣
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MTMxMTA1LCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0.NCOJUZDNQRdB6ApGfXRkEGM8rQA1kMOMj1U12aZ7vhg
這三段中間通過(英文點符號隔開)
第一段:頭部(header)
第二段:載荷(payload)
第三段:簽證(signature)
(2)JWT的三段結構詳解
第一段:頭部header
jwt的頭部承載兩部分資訊:
- 宣告型別,這裡是jwt
- 宣告加密的演算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
JWT原始碼
JWT-Builder類的方法可以看到,如果預設不設定header時,由JWT底層預設
{
"typ":"JWT",
"alg":"HS256"//這個由具體的簽名型別提供
}
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if(algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
this.headerClaims.put("typ", "JWT");
String signingKeyId = algorithm.getSigningKeyId();
if(signingKeyId != null) {
this.withKeyId(signingKeyId);
}
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}
第二部分:載荷(payload)
載荷就是存放有效資訊的地方。這個名字像是特指飛機上承載的貨品,這些有效資訊包含三個部分
- 標準中註冊的宣告
- 公共的宣告
- 私有的宣告
標準中註冊的宣告 (建議但不強制使用) :
- iss: jwt簽發者
- sub: jwt所面向的使用者
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什麼時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共的宣告 :
公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊.但不建議新增敏感資訊,因為該部分在客戶端可解密.
私有的宣告 :
私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,意味著該部分資訊可以歸類為明文資訊。
定義一個payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然後將載荷進行base64加密(該加密是可以對稱解密的),構成了第二部分.
第三段:簽證(signature)
jwt的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:
- header (base64後的)
- payload (base64後的)
- secret
這個部分需要base64加密後的header和base64加密後的payload使用.
連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret
組合加密,然後就構成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.
連線成一個完整的字串,構成了最終的jwt:
簽名部分的生成:
private String sign() throws SignatureGenerationException {
String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));
String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));
//待簽名的內容;header簽名+載荷簽名
String content = String.format("%s.%s", new Object[]{header, payload});
byte[] signatureBytes = this.algorithm.sign(content.getBytes(StandardCharsets.UTF_8));
String signature = Base64.encodeBase64URLSafeString(signatureBytes);
return String.format("%s.%s", new Object[]{content, signature});
}
注意:secret是儲存在伺服器端的,jwt的簽發生成也是在伺服器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味著客戶端是可以自我簽發jwt了。
4、如何使用JWT及工具類封裝
(1)基於maven的專案使用
需要在pom.xml引用JWT的依賴
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
(2)JWT工具類
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.codec.binary.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Created by user on 2020/12/27.
*/
public class JwtUtil {
/**
* 過期時間5分鐘
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* jwt 金鑰
*/
private static final String SECRET = "jwt_secret";
/**
* 生成簽名,五分鐘後過期
*
* @return
*/
public static String sign() {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map headerMap = new HashMap();
headerMap.put("alg", "HS256");
headerMap.put("typ", "JWT");
return JWT.create()
.withHeader(headerMap)
// 將 user id 儲存到 token 裡面
//只能保留一個
.withAudience("zhengwei").withAudience("地址")
//.withAudience()
.withClaim("userId", "zhengwei") // payload
.withClaim("age", "30") // payload
.withClaim("phone", "1810824293X") // payload
// 五分鐘後token過期
.withExpiresAt(date)
//.withNotBefore() 在這個時間之前,不能用
// token 的金鑰
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
/**
* 根據token獲取userId
*
* @param token
* @return
*/
public static String getUserId(String token) {
try {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 校驗token
*
* @param token
* @return
*/
public static boolean checkSign(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
// .withClaim("username", username)
.build();
DecodedJWT jwt = verifier.verify(token);
System.out.println("header:" + jwt.getHeader());
System.out.println(JSON.toJSONString(jwt.getClaims()));
System.out.println(jwt.getClaims().get("userId").asString());
System.out.println(jwt.getClaims().get("age").asString());
System.out.println(jwt.getClaims().get("phone").asString());
System.out.println(jwt.getClaims().get("exp").asDate());
System.out.println(jwt.getAudience().get(0));
return true;
} catch (JWTVerificationException exception) {
System.out.println(exception.getMessage());
throw new RuntimeException("token 無效,請重新獲取");
}
}
public static void main(String[] args) {
String token = JwtUtil.sign();
System.out.println("token:" + token);
boolean checkSign = JwtUtil.checkSign(token);
System.out.println("checkSign:" + checkSign);
System.out.println(new String(Base64.decodeBase64("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9")));
System.out.println(new String(Base64.decodeBase64("eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MDc0MjYyLCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0")));
System.out.println(new String(Base64.decodeBase64("DJLbf61nDoayJP5h3SpRm_VnIofGZ5lGflUQjR-vhR4")));
//JwtUtil.checkSign("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiLlnLDlnYAiLCJwaG9uZSI6IjE4MTA4MjQyOTNYIiwiZXhwIjoxNjA5MDc0MzIzLCJ1c2VySWQiOiJ6aGVuZ3dlaSIsImFnZSI6IjMwIn0.e0wqMwC6PBsPKqjTsRtU5BJivMn8p1sVyPpiTLnBnC8");
}
}
6、JWT專案整合
在spring-boot專案中整合JWT
(1)在登陸成功後,生成JWT的token返給前端
在controller的登入方法中,呼叫getSign生成token,具體載荷裡面要放那些資訊,根據實際需要
.withClaim("iss", "Service") .withClaim("aud", "APP")// payload 這樣就表示在載荷中存放了iss和aud
通過JWT原始碼分析,有些key只能是唯一的(key不可重複)
KEY:
kid keyId
iss 簽發人issuer
sub jwt所面向的使用者,一般我們可以設定為APP、PC、WAP、XCX等,用於標識使用者渠道來源
aud 接收jwt的一方
exp jwt的過期時間,這個過期時間必須要大於簽發時間
nbf 定義在什麼時間之前,該jwt都是不可用的.
iat jwt的簽發時間
jti jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊
public JWTCreator.Builder withKeyId(String keyId) {
this.headerClaims.put("kid", keyId);
return this;
}
public JWTCreator.Builder withIssuer(String issuer) {
this.addClaim("iss", issuer);
return this;
}
public JWTCreator.Builder withSubject(String subject) {
this.addClaim("sub", subject);
return this;
}
public JWTCreator.Builder withAudience(String... audience) {
this.addClaim("aud", audience);
return this;
}
public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}
public JWTCreator.Builder withNotBefore(Date notBefore) {
this.addClaim("nbf", notBefore);
return this;
}
public JWTCreator.Builder withIssuedAt(Date issuedAt) {
this.addClaim("iat", issuedAt);
return this;
}
public JWTCreator.Builder withJWTId(String jwtId) {
this.addClaim("jti", jwtId);
return this;
}
示例:
public static String sign(String userId) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(SECRET);
return JWT.create()
// 將 user id 儲存到 token 裡面
.withAudience(userId)
// 五分鐘後token過期
.withExpiresAt(date)
// token 的金鑰
.sign(algorithm);
} catch (Exception e) {
return null;
}
}
(2)在請求攔截器中去攔截請求,做token驗證判斷
定義攔截器InterceptorConfig
//config作為一個配置類
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
//新增過濾器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
//要攔截的請求
.addPathPatterns("/**"); // 攔截所有請求
}
//宣告一個過濾器
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
過濾器類編寫
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 從 http 請求頭中取出 token
// 如果不是對映到方法直接通過
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//檢查是否有passtoken註釋,有則跳過認證
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//檢查有沒有需要使用者許可權的註解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 執行認證
if (token == null) {
throw new RuntimeException("無token,請重新登入");
}
// 獲取 token 中的 user id
//這裡呼叫JWTutil方法進行驗籤
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
// User user = userService.findUserById(userId);
// if (user == null) {
// throw new RuntimeException("使用者不存在,請重新登入");
// }
// 驗證 token user.getPassword()
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("")).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
}
JWT驗籤會返回常用四種異常,根據具體的異常型別做出不同的提示
1、簽名不匹配
throw new AlgorithmMismatchException
2、簽名異常
throw new SignatureVerificationException
3、載荷中載體引數型別格式化異常
throw new InvalidClaimException
4、token過期
throw new TokenExpiredException
全部異常
7、總結
優點
- 因為json的通用性,所以JWT是可以進行跨語言支援的,像JAVA,JavaScript,NodeJS,PHP等很多語言都可以使用。
- 因為有了payload部分,所以JWT可以在自身儲存一些其他業務邏輯所必要的非敏感資訊。
- 便於傳輸,jwt的構成非常簡單,位元組佔用很小,所以它是非常便於傳輸的。
- 它不需要在服務端儲存會話資訊, 所以它易於應用的擴充套件
安全相關
- 不應該在jwt的payload部分存放敏感資訊,因為該部分是客戶端可解密的部分。(因為客戶端可以通過Base64.decodeBase64(jwt.getSignature())那到加密後的明文資訊)
- 保護好secret私鑰,該私鑰非常重要。(私鑰在宣告Algorithm algorithm = Algorithm.HMAC256(SECRET)簽名和驗籤時需要,一般在伺服器端完成)
- 如果可以,請使用https協議
8、JWT原始碼分析
JWT抽象類
public abstract class JWT {
public JWT() {
}
public static DecodedJWT decode(String token) throws JWTDecodeException {
return new JWTDecoder(token);
}
public static Verification require(Algorithm algorithm) {
return JWTVerifier.init(algorithm);
}
//對於create來講,這裡使用的工廠方法,在建立一個Builder
public static Builder create() {
return JWTCreator.init();
}
}
JWTCreator類
public final class JWTCreator {
//初始化JWT,其實就是建立了一個Builder,這個builder儲存JWT相關所有的東西
static JWTCreator.Builder init() {
return new JWTCreator.Builder();
}
}
Builder為JWTCreator的內部類
public static class Builder {
//儲存載荷內容的map屬性
private final Map<String, Object> payloadClaims = new HashMap();
//儲存heander內容的map屬性
private Map<String, Object> headerClaims = new HashMap();
Builder() {
}
public JWTCreator.Builder withHeader(Map<String, Object> headerClaims) {
this.headerClaims = new HashMap(headerClaims);
return this;
}
public JWTCreator.Builder withKeyId(String keyId) {
this.headerClaims.put("kid", keyId);
return this;
}
public JWTCreator.Builder withIssuer(String issuer) {
this.addClaim("iss", issuer);
return this;
}
public JWTCreator.Builder withSubject(String subject) {
this.addClaim("sub", subject);
return this;
}
public JWTCreator.Builder withAudience(String... audience) {
this.addClaim("aud", audience);
return this;
}
public JWTCreator.Builder withExpiresAt(Date expiresAt) {
this.addClaim("exp", expiresAt);
return this;
}
public JWTCreator.Builder withNotBefore(Date notBefore) {
this.addClaim("nbf", notBefore);
return this;
}
public JWTCreator.Builder withIssuedAt(Date issuedAt) {
this.addClaim("iat", issuedAt);
return this;
}
public JWTCreator.Builder withJWTId(String jwtId) {
this.addClaim("jti", jwtId);
return this;
}
public JWTCreator.Builder withClaim(String name, Boolean value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Integer value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Long value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Double value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withClaim(String name, Date value) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, value);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, String[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, Integer[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public JWTCreator.Builder withArrayClaim(String name, Long[] items) throws IllegalArgumentException {
this.assertNonNull(name);
this.addClaim(name, items);
return this;
}
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {
if(algorithm == null) {
throw new IllegalArgumentException("The Algorithm cannot be null.");
} else {
this.headerClaims.put("alg", algorithm.getName());
this.headerClaims.put("typ", "JWT");
String signingKeyId = algorithm.getSigningKeyId();
if(signingKeyId != null) {
this.withKeyId(signingKeyId);
}
return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();
}
}
private void assertNonNull(String name) {
if(name == null) {
throw new IllegalArgumentException("The Custom Claim\'s name can\'t be null.");
}
}
private void addClaim(String name, Object value) {
if(value == null) {
this.payloadClaims.remove(name);
} else {
this.payloadClaims.put(name, value);
}
}
}