测试环境: jdk 11 maven 项目, 依赖包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--javax.xml.bind.DatatypeConverter
JAXB API是java EE 的API,因此在java SE 9.0 中不再包含这个 Jar 包。
java 9 中引入了模块的概念,默认情况下,Java SE中将不再包含java EE 的Jar包而在 java 6/7 / 8 时关于这个API 都是捆绑在一起的-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
Json Web Token
简称jwt,是一个开放的协议,用于通信过程中传递安全可靠的信息。是一个信息的载体,更是一个通信的安全凭证。
使用场景
在前后端分离的架构下,用户在客户端输入用户名密码登录,客户端将用户名密码发送给服务端校验,校验通过后,服务端生成该用户的登录凭证(Jwt字符串)并返回给前端,前端收到后保存该凭证,再次访问服务端时,客户端自动携带该凭证即可。
凭证必须是安全可靠的,凭证支持多种加密方式,JWT协议还支持凭证的有效期,在服务端生成时指定凭证的有效期,逾期后无效,需要用户重新使用用户名密码获取新的凭证。由于凭证是信息的载体,所以,服务端在生成凭证时可以往里面设置基本的用户信息,如用户名,权限等,当然,服务端可以解密Jwt字符串并获取到相关信息的。
组成结构
JWT整体上是一段字符串,通过 “.”连接的三个小段组成:xxxxx.yyyyy.zzzzz,分别对应Header,Payload,Signature ,参考示例:
eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjIyODM2ODAsInVzZXJuYW1lIjoidGltIiwiaWQiOiIxODgiLCJyb2xlIjoiYWRtaW4ifQ.9USUq4RTQTKTeHqsWSVSJuvlcRIhFwgUOlJrd7MUVag
第一部分,为Header(头部),按照协议,主要包括两个部分,其一为“typ”,值为JWT,表明这段字符串为JWT字符串,另一为jwt所使用的签名算法alg的名称,常用的签名算法包括摘要算法HMAC,SHA256,非对称加密算法的RSA。
//定义头部内容信息
{
"typ":"JWT",
"alg":"HS256"
}
//使用Base64Url 将内容进行编码:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
支持的加密算法如下:
+--------------+-------------------------------+--------------------+
| "alg" Param | Digital Signature or MAC | Implementation |
| Value | Algorithm | Requirements |
+--------------+-------------------------------+--------------------+
| HS256 | HMAC using SHA-256 | Required |
| HS384 | HMAC using SHA-384 | Optional |
| HS512 | HMAC using SHA-512 | Optional |
| RS256 | RSASSA-PKCS1-v1_5 using | Recommended |
| | SHA-256 | |
| RS384 | RSASSA-PKCS1-v1_5 using | Optional |
| | SHA-384 | |
| RS512 | RSASSA-PKCS1-v1_5 using | Optional |
| | SHA-512 | |
| ES256 | ECDSA using P-256 and SHA-256 | Recommended+ |
| ES384 | ECDSA using P-384 and SHA-384 | Optional |
| ES512 | ECDSA using P-521 and SHA-512 | Optional |
| PS256 | RSASSA-PSS using SHA-256 and | Optional |
| | MGF1 with SHA-256 | |
| PS384 | RSASSA-PSS using SHA-384 and | Optional |
| | MGF1 with SHA-384 | |
| PS512 | RSASSA-PSS using SHA-512 and | Optional |
| | MGF1 with SHA-512 | |
| none | No digital signature or MAC | Optional |
| | performed | |
+--------------+-------------------------------+--------------------+
第二部分为Payload,包含申明信息,比如用户的主体信息和其他附加信息,这里也可以划分为三个组成部分:
-
Registered claims:协议约定的申明字段,规范保留的一些关键字,如:iss (issuer签发人), exp (expiration time过期时间), sub (subject主题), aud (audience受众),nbf(Not Before生效时间),iat(Issued At签发时间),jti(JWT ID 编号),更多可以查看:https://www.iana.org/assignments/jwt/jwt.xhtml
-
Public claims:公开申明,这些我们就可以随意定义的信息,但是应该避免使用已注册的关键字,比如iss代表的是签发人,我们就不要用来定义其他含义的信息。
-
Private claims:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明,也不是公开声明。
这里payload的组成划分,完全是语义上的,是一种约定规范,相当于通用语。实际使用过程中,根据通信各方的需要设置具体的申明项。如果是简单场景的话,简约就好。
palyload 在JWT对象中表现为Claims对象,而Claims对象的本质为一个Map。
示例:
//定义palyload内容
{"iss":"tim","username":"tim","id":"188","role":"admin"}
//使用 Base64Url 编码:
eyJpc3MiOiJ0aW0iLCJ1c2VybmFtZSI6InRpbSIsImlkIjoiMTg4Iiwicm9sZSI6ImFkbWluIn0
第三部分为Signature,签名部分。这部分是通过前两部分内容生成出来的,使用第一部分Header中指定的加密算法和秘钥对前两部分的Base64Url编码内容进行加密得到。
//Header中指定的alg=HS256 (HMACSHA256)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
//得到签名
_95SdnCWoY9CLn2zYbuLYSnV8D0xeidwtnc124rA66U
示例组装好的token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aW0iLCJ1c2VybmFtZSI6InRpbSIsImlkIjoiMTg4Iiwicm9sZSI6ImFkbWluIn0._95SdnCWoY9CLn2zYbuLYSnV8D0xeidwtnc124rA66U
使用下述方法自己组装JWT:
/**
* 对Header 及 Payload 原始字符串进行Base64Url编码
* @param plainStr
* @return
*/
public String encode64Url(String plainStr) {
byte[] bytes = plainStr.getBytes(StandardCharsets.UTF_8);
String encodeStr = Base64UrlCodec.BASE64URL.encode(bytes);
return encodeStr;
}
/**
* 获得签名
* @param header Header内容的Base64Url编码字符串
* @param payload Payload 内容的Base64Url编码字符串
* @return
*/
public String getSignature(String header,String payload){
String s=header+"."+payload;
MacSigner macSigner = new MacSigner(SignatureAlgorithm.HS256,SECRET_KEY.getBytes(StandardCharsets.UTF_8));
byte[] bytes = macSigner.sign(s.getBytes(StandardCharsets.UTF_8));
return Base64UrlCodec.BASE64URL.encode(bytes);
}
/**
* 验证签名
* @param jwtToken JWTToken字符串
* @return
*/
public boolean verify(String jwtToken){
String[] splitStrs = jwtToken.split("\\.");
String signature = getSignature(splitStrs[0] , splitStrs[1]);
if(signature.equals(splitStrs[2])){
return true;
}else{
return false;
}
}
验证签名
签名只能证明这个JWTtoken是否被篡改或是伪造的,也可以近一步验证它是否过期。所以,在payload中,我们通常不应该放敏感信息,因为base64Url编码是可逆的!
验证签名只需要将获取到的Jwt token进行“.”分割,将header和payload再次进行签名,签名结果和分割得到的签名一致,就表示合法,否则,就存在被篡改或伪造的风险。如果token合法,再解码得到payload原始内容,并取出里面的exp申明,如果该时间小于当前时间则表示已过期。
自己组装的JWT token是否成功,可以通过官网提供的在线调试器进行验证,地址:https://jwt.io/#debugger-io
注意:在右下角填上自己的秘钥。
工具类
public class JWTDemo {
//加密的
private static final String SECRET_KEY = "123456789";
/**
* 生成JWT
* @param exp
* @return
*/
public String buildJwt(Date exp) {
HashMap<String, Object> header=new HashMap<>();
header.put("alg","HS256");
header.put("typ","JWT");
String jwtToken = Jwts.builder()
.setHeader(header)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)//指定HS256加密算法,本质是mac摘要算法,需要设置秘钥
.setExpiration(exp) //expTime是过期时间
.claim("iss","tim.dao") //根据需要设置需要申明的内容,未加密的情况下,请勿设置敏感信息
.claim("iat",new Date())
.claim("username","tim")
.claim("id","188")
.claim("role", "admin")
.compact();
return jwtToken;
}
/**
* 验证签名并解析出里面的内容
* @param jwt
* @return
*/
public boolean verifyToken(String jwt) {
try {
//解析JWT字符串中的数据,并进行最基础的验证
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY) //SECRET_KEY是加密算法对应的密钥,jjwt可以自动判断加密算法
.parseClaimsJws(jwt) //jwt是JWT字符串
.getBody();
System.out.println("解析得到申明内容:");
for (Map.Entry<String, Object> entry : claims.entrySet()) {
System.out.println(entry.getKey() + "-->" + entry.getValue());
}
return true;
}
//抛出SignatureException异常,说明该JWTtoken存在被篡改或伪造的风险
//"过期时间字段"已经早于当前时间,将会抛出ExpiredJwtException异常,说明token已失效
catch (SignatureException | ExpiredJwtException e) {
e.printStackTrace();
return false;
}
}
}