JWT官方文档:
- https://jwt.io/
- https://jwt.io/introduction/
图片来自官方文档,解释的很清楚了。
通俗地说,JWT的本质就是一个字符串,它是将用户信息保存到一个 Json字符串中,然后进行编码后得到一个JWT token,并且这个 JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为 Json对象传输。
2、JWT组成部分在其紧凑的形式中,JWT由以点 ( .) 分隔的三部分组成,它们是:
- 标头(Header)、
- 有效载荷(Payload)
- 签名(Signature)。
在输出时,会将 JWT的 各部分进行 Base64编码后用点 ( .) 进行连接形成最终传输的字符串。
JWTString=
Base64(Header).
Base64(Payload).
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
官方首页有查看 JWT Token字符串的解析内容。
2.1 HeaderJWT标头是一个描述 JWT元数据的 JSON对象。 通常由两部分组成:令牌的类型,即 JWT,以及正在使用的签名算法,例如 HMAC SHA256 或 RSA。
- alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
- typ属性表示令牌的类型,JWT令牌统一写为JWT。 最后,使用 Base64 URL算法将上述 JSON对象转换为字符串保存,即形成 JWT 的第一部分。
比如:
{
"alg": "HS256",
"typ": "JWT"
}
2.2 Payload
有效载荷部分,是 JWT的主体内容部分,也是一个 JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
这些预定义的字段并不要求强制使用。除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到 payload中。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
最后,对有效负载进行 Base64Url编码以形成 JSON Web 令牌的第二部分。
请注意:
对于已签名的令牌,此信息虽然受到保护以防篡改,但任何人都可以读取。除非已加密,否则请勿将机密信息放入 JWT 的有效负载或标头元素中。
签名部分是对上面两个部分数据进行哈希,需要使用 base64编码后的 header和 payload数据,通过指定的算法生成哈希签名,以确保数据不会被篡改。
- 首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。
- 然后,使用 header中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。
例如,如果您想使用 HMAC SHA256 算法,签名将通过以下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中没有被篡改,并且在使用私钥签名的令牌的情况下,它还可以验证 JWT 的发送者就是它所说的那个人。
JWT Token就是将 JWT的 3部分分别进行 Base64编码后用点 ( .) 进行连接形成最终传输的 Base64-URL 字符串。可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更紧凑。
二、Java使用 JWT开源库官网推荐了 6个 Java使用 JWT的开源库,其中比较推荐使用的是 java-jwt和 jjwt-root。
- java-jwt
- jose4j
- nimbus-jose-jwt
- jjwt-root
- fusionauth-jwt
- vertx-auth-jwt
通常根据使用的签名算法可以分为对称签名和非对称签名来生成 JWT Token,主要区别在于使用的算法。推荐使用非对称加密算法签名。
1、使用java-jwt首先引入依赖:
com.auth0
java-jwt
3.18.2
1.1 对称签名
这里使用 HMAC256对称算法。
1.1.1 生成JWT Tokenpublic class JavaJwtLearn1 {
private static final String SECRET_KEY = "secret_salt";
public static void main(String[] args) {
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
Date expiresDate = calendar.getTime();
String jwtToken = genetatetJwtToken(expiresDate);
System.out.println("生成 jwtToken=" + jwtToken);
}
/**
* 生成token
* @param expiresDate
* @return
*/
private static String genetatetJwtToken(Date expiresDate ) {
String jwtToken = JWT.create()
// Header
.withHeader(new HashMap())
// Payload
.withClaim("userId", 21)
.withClaim("userName", "admin")
// 过期时间
.withExpiresAt(expiresDate)
// 签名用的secret
.sign(Algorithm.HMAC256(SECRET_KEY));
return jwtToken;
}
}
注意:
- 每次生成的 JWT Token字符串内容是不一样的,尽管我们的 payload信息没有变动。因为 JWT中携带了超时时间,所以每次生成的 JWT Token就会不一样。
- Header和 Payload除了有默认值,我们也可以放置自定义的值。
public class JavaJwtLearn1 {
private static final String SECRET_KEY = "secret_salt";
public static void main(String[] args) {
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
Date expiresDate = calendar.getTime();
String jwtToken = genetatetJwtToken(expiresDate);
System.out.println("生成 jwtToken=" + jwtToken);
resolveJwtToken(jwtToken);
}
/**
* 解析token
* @param jwtToken
*/
private static void resolveJwtToken(String jwtToken) {
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(SECRET_KEY)).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
// 获取解析后的token中的信息
String header = decodedJWT.getHeader();
System.out.println("header:" + header);
Map payloadMap = decodedJWT.getClaims();
System.out.println("Payload:" + payloadMap);
Date expires = decodedJWT.getExpiresAt();
System.out.println("过期时间:" + expires);
String signature = decodedJWT.getSignature();
System.out.println("signature:" + signature);
}
}
注意:
- 如果在过期时间之内解析 JWT Token,则解析成功,否则就会报错。
- 如果不是JWT Token字符串,也会报错。
所以,我们可以根据报错来校验 JWT Token的合法性。
/**
* 判断token是否存在与有效
* @param jwtToken token字符串
* @return 如果token有效返回true,否则返回false
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
1.2 非对称签名
这里使用 RSA非对称加密算法。使用同上。
注意:
- 提前准备好秘钥对(根据需要密钥(盐)生成)。
私钥生成 JWT Token,公钥解析 JWT Token
。 - Header和 Payload除了有默认值,我们也可以放置自定义的值。
/**
* 使用 java-jwt开源库 非对称加密算法签名
*
*/
public class JavaJwtLearn2 {
private static final String RSA_PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAIR4W9EIR4mNBrXu6cyw7ninv4koTUTHSlB9bAcvUqVcbRfK2ZWyojgEMNjUcTSYXmWS4RwWSDcWr1/ZbaksY8UNcK2cNk81YuL3CHRl4tOoA5YfKBwDn8q6xbX/bjDPK65hr/eGBUCvdW8ZPbKVLJujpoHEeM28yzMiCv0Igik/AgMBAAECgYA6jZjIBIjaW+Ojdz8QowRFgKBA1/ePdyd5/HZLlrdJMFloMtmKObNKX0/YB88iGFdhPlMSPycccoKCM3EtXdmbEPNYPJDcOqOH+NyqdVP7mDw2KJ7ulSMGINuW/4MGqTecUdL01BAX3KlgwuJu6BzRiPMoWY/LEqYoUmHeedxBwQJBAOgKQ5J/A1ZRFHsyFGYwBY8LJcOGb/yEkawEEOn+yRVy16t+o5R3YFdbHwuWlFVFAys9rXxd4dBLQZI+dZQNZysCQQCSJhFh8DOpVzWqPtadFmlQipZZ48hFcsoq8zS3UTukhxaubBcmvzB5dixQFNOj1veCVic9f8SF57P6dsOWJ7w9AkEA0Aq70PIOBOsHKPmarpApu7mr7yVu7IHTtd2jaJjmo1NnKLyPX4K0nz30lMg6UEVi9PcEv7fQyZdfwAY+FzL5JwJADHj1OM+ACS6pJMtSE3vrJvV82VUILW0bdcjlsdNb7LGerOoKm8LrRyJfq8HrQetBmjzyAlyaD/dzM6fZD0J63QJBALR93M3+EItlV6QxeVGHi7sVyxHYlDlwsIIQPFZB12292blXzygWPCAC1b1BF4KbENN8yPmTL+0ixkRoxRojD8Q=";
private static final String RSA_PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCEeFvRCEeJjQa17unMsO54p7+JKE1Ex0pQfWwHL1KlXG0XytmVsqI4BDDY1HE0mF5lkuEcFkg3Fq9f2W2pLGPFDXCtnDZPNWLi9wh0ZeLTqAOWHygcA5/KusW1/24wzyuuYa/3hgVAr3VvGT2ylSybo6aBxHjNvMszIgr9CIIpPwIDAQAB";
public static void main(String[] args) throws Exception {
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
Date expiresDate = calendar.getTime();
Map payload = new HashMap();
payload.put("userId", "21");
payload.put("userName", "admin");
payload.put("userName2", "admin2");
payload.put("userName3", "admin3");
String jwtToken = genetatetJwtToken(expiresDate, payload);
System.out.println("生成 jwtToken=" + jwtToken);
// jwtToken =
// "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjQ0NDc0MzY5LCJ1c2VySWQiOjIxfQ.2Tgvta3obNfyqUSlFuZPNuGGBs-stB7NeD-GU2R6Sn8";
resolveJwtToken(jwtToken);
}
/**
* 生成token
*
* @param expiresDate
* @return
*/
private static String genetatetJwtToken(Date expiresDate, Map payload) throws Exception {
JWTCreator.Builder builder = JWT.create();
// Header
Map a = new HashMap(payload);
builder.withHeader(a);
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 过期时间
builder.withExpiresAt(expiresDate);
// 获取RSA私钥
RSAPrivateKey privateKey = (RSAPrivateKey) RsaUtils.getPrivateKey(RSA_PRIVATE_KEY);
// 签名
String jwtToken = builder.sign(Algorithm.RSA256(null, privateKey));
return jwtToken;
}
/**
* 解析token
*
* @param jwtToken
*/
private static void resolveJwtToken(String jwtToken) throws Exception {
// 获取RSA公钥
RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey(RSA_PUBLIC_KEY);
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA256(publicKey, null)).build();
// 解析指定的token
DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
// 获取解析后的token中的信息
String header = decodedJWT.getHeader();
System.out.println("header:" + header);
Map payloadMap = decodedJWT.getClaims();
System.out.println("Payload:" + payloadMap);
Date expires = decodedJWT.getExpiresAt();
System.out.println("过期时间:" + expires);
String signature = decodedJWT.getSignature();
System.out.println("signature:" + signature);
}
}
2、使用jjwt-root
这里使用 0.11.1版本,引入依赖如下:
io.jsonwebtoken
jjwt-api
0.11.2
io.jsonwebtoken
jjwt-impl
0.11.2
runtime
io.jsonwebtoken
jjwt-jackson
0.11.2
runtime
注意:
jjwt-root在 0.10版本以后发生了较大变化,pom依赖和部分使用方法都有所变化,并且,0.10版本后强制要求 secretKey满足规范中的长度要求,否则生成 jws时会抛出异常。异常如下:
标准规范中对各种加密算法的 secretKey的长度有如下要求: HS256:要求至少 256 bits (32 bytes) HS384:要求至少384 bits (48 bytes) HS512:要求至少512 bits (64 bytes) RS256 and PS256:至少2048 bits RS384 and PS384:至少3072 bits RS512 and PS512:至少4096 bits ES256:至少256 bits (32 bytes) ES384:至少384 bits (48 bytes) ES512:至少512 bits (64 bytes)
1.1 对称签名这里使用 HS256对称算法。
public class JjwtRootLearn1 {
private static final String SECRET_KEY = "secret_salt_aaaaaaaaaaaaaaa";
//private static final String SECRET_KEY = "secret_salt_MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCEeFvRCEeJjQa17unMsO54p7+JKE1Ex0pQfWwHL1KlXG0XytmVsqI4BDDY1HE0mF5lkuEcFkg3Fq9f2W2pLGPFDXCtnDZPNWLi9wh0ZeLTqAOWHygcA5/KusW1/24wzyuuYa/3hgVAr3VvGT2ylSybo6aBxHjNvMszIgr9CIIpPwIDAQAB";
public static void main(String[] args) {
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 30);
Date expiresDate = calendar.getTime();
String jwtToken = genetatetJwtToken(expiresDate);
System.out.println("生成 jwtToken=" + jwtToken);
resolveJwtToken(jwtToken);
System.out.println(checkToken(jwtToken));
jwtToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIyMSIsInVzZXJOYW1lIjoiYWRtaW4iLCJqdGkiOiJZbVk1TkdOa1pUUXRZVGt4TnkwMFlXTmlMVGsyWTJVdE1EVXdPRFUzTlRVMFlqRTQiLCJleHAiOjE2NDQ0Nzk5ODZ9.5lcaBjsDUxmeNCZOt-LnjCPhAajQPK3b2HF_bAzo2Hk";
System.out.println(checkToken(jwtToken));
}
/**
* 生成token
* @param expiresDate
* @return
*/
private static String genetatetJwtToken(Date expiresDate ) {
String jwtToken = Jwts.builder()
// Header
.setHeader(new HashMap())
// Payload
.claim("userId", "21")
.claim("userName", "admin")
//.setId(new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes())))
.setId("id----")
// 过期时间
.setExpiration(expiresDate)
// 签名指定key
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)
.compact();
return jwtToken;
}
/**
* 解析token
* @param jwtToken
*/
private static void resolveJwtToken(String jwtToken) {
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8))).build();
// 解析指定的token
Jws claimsJws = jwtParser.parseClaimsJws(jwtToken);
// 获取解析后的token中的信息
JwsHeader header = claimsJws.getHeader();
System.out.println("header:" + header);
Claims jwsBody = claimsJws.getBody();
System.out.println("Payload:" + jwsBody);
System.out.println("过期时间:" + jwsBody.getExpiration());
String signature = claimsJws.getSignature();
System.out.println("signature:" + signature);
}
/**
* 判断token是否存在与有效
* @param jwtToken token字符串
* @return 如果token有效返回true,否则返回false
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
Jwts.parserBuilder().setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8))).build().parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}
1.2 非对称签名
这里使用 RSA非对称加密算法。使用同上。
public class JjwtRootLearn2 {
private static final String RSA_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZZGWcPP8zrMNA6oNhFTGCO2bEeI7t9syazhbIu7ndOM068iI7D+a9KqdPZBVfLQadF/wGV424YDZSO3jh+6SIQrTFhyOaeDWTfMoEqaahqiX4SbcU/9bfkCHX4NaiW4E/8egngnVq+x/Qg6SraC5AOBWLx6kAsUhgKhSJpqNigVh/n4EPcq6IRa2yZWc+FUahmxB4jTJo3s7exRGAyseXmBi8zP4nT+48eL2jSdMoTm0IbCeymCU5u6EHCuOWlnTSv7akXszwIPtpg+IuYIgxpT9Q0CJZrEFWPdjct50/yqVyZ1FHR+rR3I5WskfdJVVNSI7m139BKrrD9GtrazDjAgMBAAECggEAScuEGtM5j3m5Ab0Q8Z7Jj7bWLQU29gK60mr9iRrPQz91dLtSfoma3zzq+wXSRlSaDu+f/skWVDJtT8hu0oFG2YsF/tWR6lmUpNzvL6kSkkoSNE36d27RyAJGVd5ERB2zo7jUkFVx+cLQvnbmvNPFFH4m13V5t+ySPjlgYgy6I8QHa9embSuTlYeFCZe69CP6dla04kpwJzelfs5nziQ7Biy3B1O6V2Y0VeanTdVPHEXZbCVEOvUeJWVXoMAkkXbpiRglt3ugzK/nct4hkViTxu5RDHF/q29q0AvgmXGXontKVW0ee42JphsTsNSGpOxxuXHErE5i/JrbCIgi8ce3KQKBgQDoCkOSfwNWURR7MhRmMAWPCyXDhm/8hJGsBBDp/skVcterfqOUd2BXWx8LlpRVRQMrPa18XeHQS0GSPnWUDWVrP7JWi2sO+++8zKA7KwEpyuhWDfvjKdQcq9lZ7j7T9KQlCJ8e+lIapy8XWjTM3juqkSoah1KrjWF2FncS7S0h7QKBgQCpOyhrZzIocIXvnJ93dGRh6ozzbZ4KBl9VlNHmNNBs45QXsgE5VaR8byZKCvuOMfr/hUTIxTrEqwYo4P4KqUrM9EDjF67DtUKtEo+Dtrs3NE6RNPkG+/OqlcNul6w28mmzPl8XFjGm/MnR3E+Pjw7EkFvixgLMe+7yG3V5WGiEDwKBgQCf30KDUuOXuzFjWDPZ3EhYMBQKzTunPien3v1QW21sS73wuMY36rAEQBH5x/vXbD8sscgwIfcNrmw1OLeGFFzGMhLLsi9HGaop6MqVOaIJi3XcpLHh59XvEzAj2BSNsMbPhUss6sda+clmS46JgKyXboEV2hrJfBWkaQINlkA8WQKBgH3JrBSRIxYt9VASQfHfgNHLLrOuEd9vtyL8uDv9m8KkMiqetAwy3U1krLgyi6K5AdE19NeqyjDu0mhGPG4eQawwDZ7+tndf3syYVDZZ97Rj29ZQ4p1PX2G3agllEavR6cFCphmZ9JQjp7umnzic5CQ1DSd1eRUXNZed02a70Qv/AoGBAMX6hTPKhIY7FUCsvVOI0+CTgOsWJj+0G8r91Mwg/DPrSsPuQy0x9CLh6XvMzAw3J/d39YUVk47x/i48KVudITcNsn+JAckumlyk3cT01JvMJ0WH/p7lJiLGey54dG4nT/+dvkcpCLQMd0R5IC+/iPo03sNKMqhVUE8xL+ChATJf";
private static final String RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmWRlnDz/M6zDQOqDYRUxgjtmxHiO7fbMms4WyLu53TjNOvIiOw/mvSqnT2QVXy0GnRf8BleNuGA2Ujt44fukiEK0xYcjmng1k3zKBKmmoaol+Em3FP/W35Ah1+DWoluBP/HoJ4J1avsf0IOkq2guQDgVi8epALFIYCoUiaajYoFYf5+BD3KuiEWtsmVnPhVGoZsQeI0yaN7O3sURgMrHl5gYvMz+J0/uPHi9o0nTKE5tCGwnspglObuhBwrjlpZ00r+2pF7M8CD7aYPiLmCIMaU/UNAiWaxBVj3Y3LedP8qlcmdRR0fq0dyOVrJH3SVVTUiO5td/QSq6w/Rra2sw4wIDAQAB";
public static void main(String[] args) throws Exception {
// 指定token过期时间为10秒
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, 10);
Date expiresDate = calendar.getTime();
String jwtToken = genetatetJwtToken(expiresDate);
System.out.println("生成 jwtToken=" + jwtToken);
//jwtToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwiZXhwIjoxNjQ0NDc0MzY5LCJ1c2VySWQiOjIxfQ.2Tgvta3obNfyqUSlFuZPNuGGBs-stB7NeD-GU2R6Sn8";
resolveJwtToken(jwtToken);
}
/**
* 生成token
* @param expiresDate
* @return
*/
private static String genetatetJwtToken(Date expiresDate ) throws Exception {
// 获取RSA私钥
RSAPrivateKey privateKey = (RSAPrivateKey) RsaUtils.getPrivateKey(RSA_PRIVATE_KEY);
// Header中自定义放点值
Map payload = new HashMap();
payload.put("userId", "21");
payload.put("userName", "admin");
String jwtToken = Jwts.builder()
// Header
.setHeader(payload)
// Payload
.claim("userId", "21")
.claim("userName", "admin")
.setId(new String(Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes())))
// 过期时间
.setExpiration(expiresDate)
// 签名指定私钥
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
return jwtToken;
}
/**
* 解析token
* @param jwtToken
*/
private static void resolveJwtToken(String jwtToken) throws Exception {
// 获取RSA公钥
RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey(RSA_PUBLIC_KEY);
// 创建解析对象,使用的算法和secret要与创建token时保持一致
JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(publicKey).build();
// 解析指定的token
Jws claimsJws = jwtParser.parseClaimsJws(jwtToken);
// 获取解析后的token中的信息
JwsHeader header = claimsJws.getHeader();
System.out.println("header:" + header);
Claims jwsBody = claimsJws.getBody();
System.out.println("Payload:" + jwsBody);
System.out.println("过期时间:" + jwsBody.getExpiration());
String signature = claimsJws.getSignature();
System.out.println("signature:" + signature);
}
}
总结:
- jjwt-root与 java-jwt对 JWT Token的生成和解析过程中的注意点都挺类似。
- 请勿将机密信息放入 JWT 的有效负载或标头元素中。
- 在实际开发中,对于 JWT Token中的 Payload信息,可以封装成对象来方便处理。也可以放入 key,把用户信息作为 value存入 Redis等等。
– 求知若饥,虚心若愚。