前一篇中介绍了Token的机制以及一个简单实现了一个Token管理类。
这篇中介绍的是现有的工业化框架—— JWT (JSON Web Tokens)。
- 前一篇地址:[JAVA]Token机制及管理类代码实例
- 官方地址:JWT.IO: JSON Web Tokens
概括来说,JWT是一种符合RFC 7519工业体系设计的、基于JSON对象的、利用RSA等算法签发的Token框架/标准。
- JWT在通常情况下,并不对Token数据进行加密(和上一篇中简单利用数字摘要技术产生的Token不同,JWT签发的Token是可以进行逆向解码的),只关注于Token令牌本身的签发时效性、完整性、合法性。
- JWT同样可以利用加密手段对Token进行签发。但通常来说并不推荐这么做,相比于利用HTTP传递一个经过JWT加密的Token,更好的做法是利用HTTPS传递一个Token。同时也不建议在JWT签发的Token中传递敏感信息——但合理的利用加密Token传递数据可以有效减轻服务器负载。
对于JWT,它的通用环境主要有两种:
注:官方对此有更详细的说明:
- Authorization: This is the most common scenario for using JWT. Once the user is logged in, each subsequent request will include the JWT, allowing the user to access routes, services, and resources that are permitted with that token. Single Sign On is a feature that widely uses JWT nowadays, because of its small overhead and its ability to be easily used across different domains.
- Information Exchange: JSON Web Tokens are a good way of securely transmitting information between parties. Because JWTs can be signed—for example, using public/private key pairs—you can be sure the senders are who they say they are. Additionally, as the signature is calculated using the header and the payload, you can also verify that the content hasn’t been tampered with.
JWT是一套框架和标准,其实现含有Java
、Python
、C#
、Node.js
,每种语言也有不同的实现库。因此,基于JWT可以实现跨语言的Token签发、认证、解码。
Ref: JWT.IO-Libraries for Token Signing/Verification
JWT的主体结构为:
习惯上分别将之称作头部、载荷、签名。
大部分时候,我们在代码中只关心头部中的加密算法和载荷中的信息。
头部主要包含两类信息,加密算法(alg)和令牌签发类型(typ),一个示例如下:
{
"alg": "HS256",
"typ": "JWT"
}
- 注意:出于简洁和数据传输时的开销考虑,每个JSON字符串的索引都是三个字符的缩写,如
alg
(algorithm)。
载荷包含三种声明域(Claims):
Ref:签发域标准——RFC 7519:Registered Claim Names
签发域是我们需要着重关注和了解的第一个域。其中重中之重主要有四个:iss
(issuer),exp
(expiration time),sub
(subject),aud
(audience)。但此处将会把每一个都罗列:
缩写 | 子域名 | 说明 |
---|---|---|
iss | Issuer | * (必需项) 签发此Token的发行者,如 CSDN |
sub | Subject | 标识主体,用于识别用户的唯一身份标识,如 张三 |
aud | Audience | 接受此JWT签发Token的受众,如 Chrome用户 |
exp | Expiration Time | Token的过期时间,详见下 |
nbf | Not Before | Token的启用时间,详见下 |
iat | Issued At | Token的签发时间,详见下 |
jti | JWT ID | 为此JWT受以一个身身份标识符,主要用于多JWT签发时的Token唯一性,如 001 |
注意:上表中带
*
的是必需项,其余是可选项。
关于三个Token的时间exp
,nbf
, iat
,记收到Token的时间为TODAY
,有以下产生Token认证失败的原因:
iat < TODAY
exp < TODAY
nbf < TODAY
优先接获异常的顺序时自上到下。
Tips:如果这三项未指定则不会触发相应的校验规则。
Ref:公共域标准——RFC 7519:Public Claim Names
公共域信息是非加密的信息,服务器可以在这里捎带一些非敏感的数据信息,例如用户头像的URL等。
使用样例可见文末的java-jwt
使用样例。
Ref:私有域标准——RFC 7519:Private Claim Names
私有域的信息用于创建共享信息。在编程中常与共有域一同视为一个超集——自定义域。
注意:虽然名字为私有域,但其并不进行加密。
签名是对头部、载荷利用先前指定的加密算法进行的签发,用以证明这个Token确实是服务器发出的。签发时,将会利用服务器的私钥secret
进行签发,因此签名部分的形成算法如下(假定使用HMACSHA256
算法进行加密):
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
其中,
base64UrlEncode
是对base64
算法的转义,主要是替换掉=
、-
等在URL中具有特殊意义的字符。
JWT是一套框架和标准,其实现含有Java
、Python
、C#
、Node.js
,每种语言也有不同的实现库。
这里主要使用 java-jwt@Github,这也是官方的实现库,目前实现版本为3.7.0,可以使用Maven或Gradle等进行配置。
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.7.0version>
dependency>
implementation 'com.auth0:java-jwt:3.7.0'
或者到我备份的CSDN地址下载jar包:https://download.csdn.net/download/shenpibaipao/11015001
public static void main(String [] args){
// 设置一个私钥,也可以使用KeyProvider产生,参见:
// @link https://github.com/auth0/java-jwt#using-a-keyprovider
String key = "Shenpibaipao";
// 给定一个算法,如HmacSHA-256
Algorithm alg = Algorithm.HMAC256(key);
// 1 签发Token
Date currentTime = new Date();
String token = JWT.create()
.withIssuer("CSDN Blog") // 发行者
.withSubject("userid") // 用户身份标识
.withAudience("CSDN User") // 用户单位
.withIssuedAt(currentTime) // 签发时间
.withExpiresAt(new Date(currentTime.getTime() + 24*3600*1000L)) // 一天有效期
.withJWTId("001") // 分配JWT的ID
.withClaim("PublicClaimExample", "You should not pass!") // 定义公共域信息
.sign(alg);
System.out.println("生成的Token是:"+token);
// 2 验证Token
JWTVerifier verifier = JWT.require(alg)
.withIssuer("CSDN Blog")
.withAudience("CSDN User")
.build();
try{
verifier.verify(token);
System.out.println("验证通过!");
} catch (JWTVerificationException e) {
e.printStackTrace();
System.out.println("验证失败!");
}
// 3 尝试解码
try{
DecodedJWT originToken = JWT.decode(token);
System.out.println("解码得到发行者是:"+originToken.getIssuer());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("解码得到签发时间是:"+sdf.format(originToken.getIssuedAt()));
System.out.println("解码得到公共域信息是:"+originToken.getClaim("PublicClaimExample").asString());
} catch (JWTDecodeException e){
e.printStackTrace();
}
}