Json Web Token的介绍和最佳实践

1. 什么是JSON Web Token

JSON Web Token是一个开放的、行业规范(RFC7519)的方法,用于通信双方进行可靠的数据传输。它规定了一种紧凑的数据格式,将要传输的数据以JSON对象方式的组织,然后进行编码和签名,转化成为Token。

JSON Web Token包含三部分信息数据,

Header.Payload.Signature

其中,

  • Header:数据头部,声明Token类型和加密算法(比如HMAC SHA256或者RSA)
  • Payload:数据的信息体,包含了要传输的信息体
  • Signature:数据的数字签名,用于对数据进行校验来源可靠性和不被篡改

在三部分中,header/payload都是公开信息,JSON对象数据格式,使用Base64方式进行编码,这意味着header/payload可以被任何人进行解码和阅读,所以不要在header/payload放入敏感信息。

一个Token样例为,
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

其中,

Header Base64编解码 {"alg":"HS256","typ":"JWT"}
Payload Base64编解码 {"sub":"1234567890","name":"John Doe","admin":true}
Signature HS256签名算法 HS256(eyJhb……RydWV9, secret)

注:签名算法中的eyJhb……RydWV9为Base64编码后的Header.Payload

2. 应用场景

JWT一般用在如下两个场景,

  • 认证:这是最常用JWT应用场景,用于用户登录后,后端服务器颁发一个带有时效性的JWT token给前端应用,然后前端应用在后续的请求中使用该Token进行访问后端资源。
  • 信息交换:由于JWT对数据进行了签名,信息不能被篡改,可以可靠地在通信双方进行数据信息交换。

下图简单描绘了JWT在认证场景的使用流程,

使用方法

JWT的使用方法包括:签发、解码、校验,

  • 签发:用于对信息进行签名,然后颁发给使用方,供后续进行认证和信息交换
  • 解码:对JWT的信息体解码,获取信息,一般用在不进行校验的场合,比如在浏览器端
  • 校验:对JWT的校验,确认信息的来源可靠性和不被篡改、时效不过期

目前已经有如下语言的支持,

  • Java
  • Node.js
  • JavaScript
  • Microsoft .NET、.NET RT(Windows 8.1 and Windows Phone 8.1)
  • Python
  • Perl
  • Ruby
  • Go
  • Lua
  • Scala
  • Object-C
  • Swift
  • PHP

等等。

下面主要介绍JavaScript/Node.Js/Java三种语言对Jwt的签发、校验、解码的代码实现,这三种语言分别代表着web客户端/web服务器端/后端服务。

1) JavaScript

下面使用jwt-decode类库在web客户端(浏览器端)对token进行解码操作。

点击这里下载jwt-token.js脚本。

在HTML页面中引入上面的JavaScript脚本,下面为简单的解码演示,

<script type="text/javascript" src="jwt-token.js"></script>
<script>

    let jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
    let payload = jwtToken.split('.')[1];
    let payloadInfo = JSON.parse(jwt.base64urldecode(payload));

    for (let key in payloadInfo) {
        console.log(key + ": " + payloadInfo[key]);
    }

</script>
2) Node.js

下面使用Node Auth0类库在nodejs的web服务器端进行token的签发和校验
安装命令,从npm中安装jsonwebtoken
npm install jsonwebtoken

签发

let jwt = require('jsonwebtoken');
let token = jwt.sign({name: 'John'}, 'secret', { algorithm: 'HS256', expiresIn: '1d', issuer: 'pphh'});

校验

let jwt = require('jsonwebtoken');
let info = jwt.verify(token, 'secret');
console.log(info.name) // John

解码

let info = jwt.decode(token, {complete: true});
console.log(info.header);
console.log(info.payload);
3) Java

下面使用Java Auth0类库来进行token的签发和校验。

在Maven项目中添加依赖项,

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>

签发

try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    String token = JWT.create()
                      .withIssuer("auth0")
                      .sign(algorithm);
} catch (UnsupportedEncodingException exception){
    //UTF-8 encoding not supported
} catch (JWTCreationException exception){
    //Invalid Signing configuration / Couldn't convert Claims.
}

校验

String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
    Algorithm algorithm = Algorithm.HMAC256("secret");
    JWTVerifier verifier = JWT.require(algorithm)
                              .withIssuer("auth0")
                              .build(); //Reusable verifier instance
    DecodedJWT jwt = verifier.verify(token);
} catch (UnsupportedEncodingException exception){
    //UTF-8 encoding not supported
} catch (JWTVerificationException exception){
    //Invalid signature/claims
}

解码

String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
    DecodedJWT jwt = JWT.decode(token);
} catch (JWTDecodeException exception){
    //Invalid token
}

更多语言和JWT类库使用,请参考官方网站和样例。

最佳实践

  1. 在JWT信息体Payload中不要包括敏感信息,若一定要,可以尝试使用JSON Web Encryption 。
  2. 一定要添加Token过期时间expiration time (exp claim) ,用于时效性检查。
  3. 不建议以无签名算法的方式(签名算法为none)来颁发JWT token,这个有安全风险,会导致攻击者绕过某些JWT类库的校验,具体可以查看该资料
  4. 建议使用HTTPS来传输JWT token信息,在最大程度保证信息在传输过程中的安全性,防止被中间人截取。
  5. 确保签名密钥只能让颁发者和使用者知道,尽可能使用非对称加密算法来颁发Token(比如RSA算法)。
  6. 如果担心重放攻击(replay attacks),可以尝试设置jti声明(JWT ID Claim),该声明将使得每个JWT token拥有唯一的ID,使其可以只使用一次,可以用来防止重放攻击。
  7. 在浏览器端,token可以存放到sessionStorage/localStorage,但注意XSS(跨站点脚本)攻击;还有一种方法是token放到了Cookie中,建议为其加上HttpOnly和Secure标记,HttpOnly保证JavaScript无法获取该Cookie,而Secure标记将保证该Cookie信息只能通过Https传输,但注意Cookie的方式可能会引起CSRF(跨站请求伪造)攻击。

演示项目

admin demo项目代码中有一一使用上述的JWT token颁发、校验、解码方法,

  1. 在前端项目admin-front中,通过jwt-decode解析JWT token来获取登录用户账号。
  2. 在前端项目admin-front中,在配置文件webpack.config.js中有提供/test/auth接口,通过jsonwebtoken颁发JWT token。
  3. 在后端项目admin-server中,在UserService中,通过com.auth0.jwt.JWT来颁发JWT token。
  4. 在后端项目admin-server中,在JwtFilter中,通过com.auth0.jwt.JWT来校验用户登录token。

具体可以获取项目代码查看。

admin demo代码仓库地址:https://gitee.com/pphh/simple-admin,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/simple-admin.git

参考资料