什么是JWT?
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
jwt的組成
-
Header: 標題包含了令牌的元數據,並且在最小包含簽名和/或加密算法的類型
-
Claims: Claims包含您想要簽署的任何信息
-
JSON Web Signature (JWS): 在header中指定的使用該算法的數字簽名和聲明
示例
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Claims:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature:
base64UrlEncode(Header) + "." + base64UrlEncode(Claims)
加密生成的token:
JJWT
JJWT是一個提供端到端的JWT創建和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築為中心的流暢界面,隱藏了它的大部分復雜性。
-
JJWT的目標是最容易使用和理解用於在JVM上創建和驗證JSON Web令牌(JWTs)的庫。
-
JJWT是基於JWT、JWS、JWE、JWK和JWA RFC規范的Java實現。
-
JJWT還添加了一些不屬於規范的便利擴展,比如JWT壓縮和索賠強制。
JJWT 規范兼容
-
創建和解析明文壓縮JWTs
-
創建、解析和驗證所有標准JWS算法的數字簽名壓縮JWTs(又稱JWSs):
-
HS256:使用SHA-256的HMAC
-
HS384:使用SHA-384的HMAC
-
HS512:使用SHA-512的HMAC
-
RS256:使用SHA-256的RSASSA-PKCS-v1_5
-
RS384:使用SHA-384的RSASSA-PKCS-v1_5
-
RS512:使用SHA-512的RSASSA-PKCS-v1_5
-
PS256:使用SHA-256的RSASSA-PSS和使用SHA-256的MGF1
-
PS384:使用SHA-384的RSASSA-PSS和使用SHA-384的MGF1
-
PS512:使用SHA-512的RSASSA-PSS和使用SHA-512的MGF1
-
ES256:使用P-256和SHA-256的ECDSA
-
ES384:使用P-384和SHA-384的ECDSA
-
ES512:使用P-521和SHA-512的ECDSA
下面我們根據 https://github.com/jwtk/jjwt 上的demo,來介紹下 JJWT 的用法。
jjwt 安裝
jjwt 提供了 Maven 和 Gradle 兩種構建方式,Maven 配置如下即可使用 JJWT
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
Gradle 使用方式如下:
dependencies { compile 'io.jsonwebtoken:jjwt:0.9.0' }
注意:JJWT依賴於Jackson 2.x. 如果您已經在您的應用程序中使用了舊版本的 Jackson 請升級相關配置。
示例代碼
簽發 JWT
public static String createJWT() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) // JWT_ID
.setAudience("") // 接受者
.setClaims(null) // 自定義屬性
.setSubject("") // 主題
.setIssuer("") // 簽發者
.setIssuedAt(new Date()) // 簽發時間
.setNotBefore(new Date()) // 失效時間
.setExpiration(long) // 過期時間
.signWith(signatureAlgorithm, secretKey); // 簽名算法以及密匙
return builder.compact();
}
驗證 JWT
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
完整示例
package com.tingfeng.demo;
import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
/**
* 由字符串生成加密key
*
* @return
*/
public SecretKey generalKey() {
String stringKey = Constant.JWT_SECRET;
// 本地的密碼解碼
byte[] encodedKey = Base64.decodeBase64(stringKey);
// 根據給定的字節數組使用AES加密算法構造一個密鑰
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 創建jwt
* @param id
* @param issuer
* @param subject
* @param ttlMillis
* @return
* @throws Exception
*/
public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {
// 指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的時間
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
Map<String, Object> claims = new HashMap<>();
claims.put("uid", "123456");
claims.put("user_name", "admin");
claims.put("nick_name", "X-rapido");
// 生成簽名的時候使用的秘鑰secret,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。
// 一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
SecretKey key = generalKey();
// 下面就是在為payload添加各種標准聲明和私有聲明了
JwtBuilder builder = Jwts.builder() // 這里其實就是new一個JwtBuilder,設置jwt的body
.setClaims(claims) // 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標准的聲明賦值之后,就是覆蓋了那些標准的聲明的
.setId(id) // 設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。
.setIssuedAt(now) // iat: jwt的簽發時間
.setIssuer(issuer) // issuer:jwt簽發人
.setSubject(subject) // sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。
.signWith(signatureAlgorithm, key); // 設置簽名使用的簽名算法和簽名使用的秘鑰
// 設置過期時間
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 解密jwt
*
* @param jwt
* @return
* @throws Exception
*/
public Claims parseJWT(String jwt) throws Exception {
SecretKey key = generalKey(); //簽名秘鑰,和生成的簽名的秘鑰一模一樣
Claims claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //設置簽名的秘鑰
.parseClaimsJws(jwt).getBody(); //設置需要解析的jwt
return claims;
}
public static void main(String[] args) {
User user = new User("tingfeng", "bulingbuling", "1056856191");
String subject = new Gson().toJson(user);
try {
JwtUtil util = new JwtUtil();
String jwt = util.createJWT(Constant.JWT_ID, "Anson", subject, Constant.JWT_TTL);
System.out.println("JWT:" + jwt);
System.out.println("\n解密\n");
Claims c = util.parseJWT(jwt);
System.out.println(c.getId());
System.out.println(c.getIssuedAt());
System.out.println(c.getSubject());
System.out.println(c.getIssuer());
System.out.println(c.get("uid", String.class));
} catch (Exception e) {
e.printStackTrace();
}
}
}
Constant.java
package com.tingfeng.demo;
import java.util.UUID;
public class Constant {
public static final String JWT_ID = UUID.randomUUID().toString();
/**
* 加密密文
*/
public static final String JWT_SECRET = "woyebuzhidaoxiediansha";
public static final int JWT_TTL = 60*60*1000; //millisecond
}
輸出示例
JWT:eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiIxMjM0NTYiLCJzdWIiOiJ7XCJuaWNrbmFtZVwiOlwidGluZ2ZlbmdcIixcIndlY2hhdFwiOlwiYnVsaW5nYnVsaW5nXCIsXCJxcVwiOlwiMTA1Njg1NjE5MVwifSIsInVzZXJfbmFtZSI6ImFkbWluIiwibmlja19uYW1lIjoiWC1yYXBpZG8iLCJpc3MiOiJBbnNvbiIsImV4cCI6MTUyMjMxNDEyNCwiaWF0IjoxNTIyMzEwNTI0LCJqdGkiOiJhNGQ5MjA0Zi1kYjM3LTRhZGYtODE0NS1iZGNmMDAzMzFmZjYifQ.B5wdY3_W4MZLj9uBHSYalG6vmYwdpdTXg0otdwTmU4U
解密
a4d9204f-db37-4adf-8145-bdcf00331ff6
Thu Mar 29 16:02:04 CST 2018
{"nickname":"tingfeng","wechat":"bulingbuling","qq":"1056856191"}
Anson
123456
一般我們把驗證操作作為中間件或者攔截器就行了,請求后端API接口時候,通過過濾器獲取header中的token,驗證是否正確、是否過期等。
