官网:https://jwt.io/

文档:https://jwt.io/introduction/

目录

什么是JWT

JWT是“JSON Web Token”的英文缩写,是一种开放的工业标准方法(RFC 7519),用于在网络应用环境中安全地传递声明信息。虽然JWT的名称中包含一个单词“JSON”,但是JWT本身并不是JSON格式的(组成JWT的各个部分是JSON格式的)。实际上,JWT由三段信息构成,将这三段信息文本用.链接一起就构成了JWT字符串:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ.eNKsQ89xab7Za5P9uywqPvAiYZIHK1dwS0h8rRW9sVM

第一部分为头部(Header),第二部分为载荷(Payload),第三部分为签名(Signature)。

头部(Header)

JWT的头部承载两部分信息:

  • 声明类型,值为JWT
  • 声明加密的算法,可以使用不同的签名算法,如:HS256,HS384,HS512等等,不同的实现库所能支持的算法也尽不相同

完整的头部就像下面这样的JSON:

{
"typ": "JWT",
"alg": "HS256"
}

然后将头部进行Base64编码就构成了第一部分:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(Payload)

载荷就是存放声明信息的地方(通常可以将登录的用户信息存放在这里),包含2个部分:

  1. 公共声明
  2. 私有声明

公共声明中可以包含如下信息(建议但不强制使用):

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击

私有声明中可以声明一些与业务相关的信息,但是一般不建议存放敏感信息,因为Base64编码值是可以解码的,意味着该部分信息可以归类为明文信息,存放敏感信息不安全。

一个Payload示例如下:

{
"iss": "iss0"
"sub": "1234567890",
"name": "zhangsan",
"admin": true
}

显然,在上述定义的Payload中,name和admin都属于自定义声明信息。然后将其进行Base64编码,得到JWT的第二部分:eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ

签名(Signature)

JWT的第三部分是一个签名信息,这个部分需要Base64编码后的Header和Base64编码后的Payload使用.连接组成字符串,然后通过Header中声明的加密方式进行加盐secret组合加密并将加密结果进行Bas464编码,就是构成了JWT的第三部分:eNKsQ89xab7Za5P9uywqPvAiYZIHK1dwS0h8rRW9sVM。如下为计算签名值的完整示例:

public static void main(String[] args) throws InvalidKeyException {
// 计算得到Base64编码后的Header值
String header = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9";
// 计算得到Base64编码后的Payload值
String payload = "eyJzdWIiOiIxMjM0NTY3ODkwIiwiaXNzIjoiaXNzMCIsIm5hbWUiOiJ6aGFuZ3NhbiIsImFkbWluIjp0cnVlfQ";
String secret = "secret";
String encodeStr = header + "." + payload;
// 对Base64编码后的Header和Base64编码后的payload进行HMAC256加盐加密,得到JWT的第三部分签名信息
String signature = HMACSHA256(encodeStr.getBytes(), secret.getBytes());
System.out.println(signature);
} // 使用HMAC256加密
public static String HMACSHA256(byte[] data, byte[] key) throws InvalidKeyException {
try {
SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
return new BASE64Encoder().encode(mac.doFinal(data));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
}

注意: secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret用于进行JWT的签发和验证。所以,它是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

JWT使用场景

JWT主要解决的是在网络中安全地传递用户信息,因此可应用在如下场景:

1.在REST接口中保存用户信息,实现API接口的访问授权。

用户登录之后将信息(如:user_id)编码到JWT字符串中传递给客户端,这样服务端就不用再保存登录用户信息了,便于服务端分布式扩容。另外,还可以直接使用JWT的公共声明实现访问控制(如通过exp声明实现访问失效,jti声明实现一次性token等等)。

2.分布式站点的单点登录(SSO)。

实现原理是将JWT字符串作为响应Cookie的一部分返回给浏览器客户端,这样JWT就可以在相同主域的多个站点之后传递,从而实现分布式站点的单点登录。注意,在这里必须使用HttpOnly属性来防止Cookie被JavaScript读取,从而避免XSS攻击。

如何传递JWT

理论上,在基于HTTP协议的应用中可以有如下几种传递方式:

  1. 在HTTP消息头中传递,如:Authorization: 'Bearer ' + header.body.signature
  2. 在Cookie中传递,如:Set-Cookie: jwt=header.body.signature; HttpOnly;domain=.lenovo.com
  3. 在消息体中传递:jwt=header.body.signature,但通常不应该这么做

JWT应用实践

手动签发JWT

以Java语言为例,我们完全可以按照JWT的定义格式自己签发JWT。

// 手动实现JWT签发
// 需要注意的是:使用JDK自带的Base64工具类编码的结果可能会以"=="结尾,需要去掉这个字符
public class JWTUtil {
public static void main(String[] args) throws InvalidKeyException {
// 构造头部
JSONObject headerJson = new JSONObject();
headerJson.put("typ", "JWT");
headerJson.put("alg", "HS256");
String header = base64Encode(headerJson.toJSONString().getBytes()); // 构造载荷
JSONObject payloadJson = new JSONObject();
payloadJson.put("iss", "iss0");
payloadJson.put("sub", "1234567890");
payloadJson.put("name", "zhangsan");
payloadJson.put("admin", true);
String payload = base64Encode(payloadJson.toJSONString().getBytes()); // 加密
String secret = "secret";
String encodeStr = header + "." + payload;
String signature = HMACSHA256(encodeStr.getBytes(), secret.getBytes());
String jwt = new StringBuilder()
.append(header)
.append(".")
.append(payload)
.append(".")
.append(signature)
.toString();
System.out.println(jwt);
} // 使用HMAC256加密
private static String HMACSHA256(byte[] data, byte[] key) throws InvalidKeyException {
try {
SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
return base64Encode(mac.doFinal(data));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
} // base64编码
private static String base64Encode(byte[] bytes) {
String encode = Base64.getEncoder().encodeToString(bytes);
int index = encode.indexOf("=");
if(index > 0) {
encode = encode.substring(0, index);
}
return encode;
}
}

使用类库签发JWT

从JWT的官网可以看到,目前已经有多种语言版本JWT的实现库。

以Java库为例,完全支持JWT公共声明和常用加密算法的库有3个,分别是:java-jwt,jose4j,jjwt,比较如下:

名称 易用性 性能(ms) 热度 地址
java-jwt 180 1812 https://github.com/auth0/java-jwt
jose4j 258 NaN https://bitbucket.org/b_c/jose4j/wiki/Home
jjwt 292 3187 https://github.com/jwtk/jjwt

附: 性能是指连续生成10次JWT所需要的平均耗时时间(单位:毫秒)。

鉴于易用性和性能方面的考虑,如下示例以使用java-jwt库进行说明,更加详细的使用请参考各个实现库官方文档。

  • 添加依赖
<!-- 集成JWT类库 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.3.0</version>
</dependency>
  • 服务端签发和验证JWT
@RestController
@RequestMapping("/jwt")
public class JwtController {
private String secret = "secret";
private String iss = "iss0";
private String sub = "1234567890";
private String key = null; // 模拟用户登录,并在登录请求响应中返回JWT
@PostMapping("/login")
public Object login(HttpServletRequest req, HttpServletResponse resp,
@RequestBody JSONObject user) {
// 用户名和密码
String userName = user.getString("username");
String passwrod = user.getString("passwrod"); // 使用类库签发JWT
try {
Algorithm algorithm = Algorithm.HMAC256(this.secret);
String jwt = JWT.create()
.withIssuer(iss)
.withSubject(sub)
//.withAudience(auArr)
//.withExpiresAt(exp)
//.withNotBefore(nbf)
//.withIssuedAt(iat)
//.withJWTId(jti)
.withClaim("name", userName)
.withClaim("admin", true)
.sign(algorithm); JSONObject data = new JSONObject();
data.put("code", 200);
data.put("message", "success");
data.put("data", jwt); return data;
} catch (UnsupportedEncodingException e){
//UTF-8 encoding not supported
e.printStackTrace();
} catch (JWTCreationException e){
//Invalid Signing configuration / Couldn't convert Claims.
e.printStackTrace();
} return null;
} // 模拟在用户登录之后将JWT通过HTTP消息头返回给服务端进行验证
@GetMapping("/list")
public Object list(HttpServletRequest req, HttpServletResponse resp) {
String auth = req.getHeader("Authorization");
if(auth != null) {
String jwt = auth.split(" ")[1];
try {
Algorithm algorithm = Algorithm.HMAC256(this.secret);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(this.iss)
.build(); //Reusable verifier instance
DecodedJWT jwtDecode = verifier.verify(jwt); System.out.println("=========================");
System.out.println(jwtDecode.getClaim("name").asString());
System.out.println(jwtDecode.getClaim("admin").asBoolean());
System.out.println("=========================");
} catch (UnsupportedEncodingException e){
//UTF-8 encoding not supported
e.printStackTrace();
} catch (JWTVerificationException e){
//Invalid signature/claims
e.printStackTrace();
}
} List<String> list = new ArrayList<String>();
list.add("张三");
list.add("李四"); JSONObject data = new JSONObject();
data.put("code", 200);
data.put("message", "success");
data.put("data", list); return data;
}
}
  • 客户端读取并返回JWT
var jwt = null;

// 模拟用户登录获取JWT
function doLogin() {
var url = "http://localhost:8080/jwt/login";
var params = {"username": "zhangsan", "password": "111111"};
$.ajax({
type: "POST",
url: url,
dataType: "json",
contentType: "application/json",
data: JSON.stringify(params),
success: function (data) {
console.log(data.data);
jwt = data.data;
}
});
} // 模拟用户登录之后执行操作,将JWT返回给服务端
function doList() {
var url = "http://localhost:8080/jwt/list";
$.ajax({
type: "GET",
url: url,
headers: {
// 客户端需要在HTTP请求消息头中将JWT返回给服务端
'Authorization': 'Bearer ' + jwt,
},
success: function(data){
console.log(data);
}
});
}

总结

JWT运行流程

与传统Session方式的比较

本质上来讲,JWT就是一种在网络应用中保存用户信息的方式。因此,不得不与传统的Session保存用户信息的方式进行比较。

  • 基于Session方式保存用户信息

HTTP协议本身是无状态的,为了在Web应用中记住登录用户的信息,传统方式通过Session在服务端保存登录用户信息。具体实现为:用户访问网站时会在服务端随机生成一个Session ID,服务端使用该Session ID在内存中保存一个与之相关联的对象,再以Cookie的形式将该Session ID返回给浏览器客户端,以后每次浏览器客户端访问服务器时都以Cookie的形式将该Sesion ID再返回给服务器端,这是前提。在用户登录成功后,将相关信息保存在与该Session ID相关的对象中(通常是保存在内存),通过这种方式就实现了在服务器端保存用户信息。这种通过Cookie方式实现Session并在服务端保存用户信息的方式存在一些弊端:

(1)服务端内存压力大:Session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

(2)服务端扩展性不好:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,也意味着限制了应用的扩展能力。

(3)CSRF:因为是基于Cookie来实现Session的, 如果实现不当Cookie被截获,用户就会很容易受到跨站请求伪造的攻击。

  • 基于Token方式保存用户信息

将用户信息基于Token方式在每次请求中进行传递,这样就不需要在服务端保存,大大降低了服务端的存储压力。另外,服务端可以实现任意的分布式扩容缩容。当然,基于Token方式保存用户信息的方式完全可以自定义实现(参考:细说REST API安全之访问授权),此时需要考虑如何保证Token安全传递等方方面面的因素。而基于JWT这样的标准结构,大大降低了实现的难度。

(1)由于JSON的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,PHP等很多语言都可以使用。

(2)因为有了Payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。

(3)便于传输,JWT的构成非常简单,字节占用很小,所以它是非常便于传输的。

使用JWT时注意事项

  1. 不应该在JWT的Payload部分存放敏感信息,因为Base64编码是很容易被解码的,这部分相当于明文数据。
  2. 保护好服务端用于加密的secret私钥,该私钥非常重要。
  3. 请使用https协议保证传输的安全性。

【参考】

[1]. https://www.jianshu.com/p/576dbf44b2ae 什么是 JWT -- JSON WEB TOKEN

[2]. https://blog.leapoahead.com/2015/09/06/understanding-jwt/ JSON Web Token - 在Web应用间安全地传递信息

[3]. http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/ 八幅漫画理解使用JSON Web Token设计单点登录系统

JWT入门简介的更多相关文章

  1. 掌握 Ajax,第 1 部分: Ajax 入门简介

    转:http://www.ibm.com/developerworks/cn/xml/wa-ajaxintro1.html 掌握 Ajax,第 1 部分: Ajax 入门简介 理解 Ajax 及其工作 ...

  2. MongoDB入门简介

    MongoDB入门简介 http://blog.csdn.net/lolinzhang/article/details/4353699 有关于MongoDB的资料现在较少,且大多为英文网站,以上内容大 ...

  3. (转)Web Service入门简介(一个简单的WebService示例)

    Web Service入门简介 一.Web Service简介 1.1.Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从I ...

  4. NodeJS入门简介

    NodeJS入门简介 二.模块 在Node.js中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,这时的我们可以将应用程序划分为各个不同的部分. const http = require ...

  5. ASP.NET Core学习之一 入门简介

    一.入门简介 在学习之前,要先了解ASP.NET Core是什么?为什么?很多人学习新技术功利心很重,恨不得立马就学会了. 其实,那样做很不好,马马虎虎,联系过程中又花费非常多的时间去解决所遇到的“问 ...

  6. webservice入门简介

    为了梦想,努力奋斗! 追求卓越,成功就会在不经意间追上你 webservice入门简介 1.什么是webservice? webservice是一种跨编程语言和跨操作系统平台的远程调用技术. 所谓的远 ...

  7. Web Service入门简介(一个简单的WebService示例)

    Web Service入门简介 一.Web Service简介 1.1.Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从I ...

  8. Android精通教程-第一节Android入门简介

    前言 大家好,给大家带来Android精通教程-第一节Android入门简介的概述,希望你们喜欢 每日一句 If life were predictable it would cease to be ...

  9. Nginx入门简介

    Nginx入门简介 Nginx 介绍 Nginx (engine x) 是一个高性能的HTTP和反向代理服务,也是一个IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二 ...

随机推荐

  1. 我的第一个python web开发框架(32)——定制ORM(八)

    写到这里,基本的ORM功能就完成了,不知大家有没有发现,这个ORM每个方法都是在with中执行的,也就是说每个方法都是一个完整的事务,当它执行完成以后也会将事务提交,那么如果我们想要进行一个复杂的事务 ...

  2. VMware虚拟机上网络连接解决方案

    VMware虚拟机上网络连接解决方案 作者:凯鲁嘎吉 - 博客园http://www.cnblogs.com/kailugaji/ 从虚拟机上连接外部网络,需要设置以下几个地方. 1.服务 (1)打开 ...

  3. 用人类社会工程学对C语言中的一些基本概念的剖析与理解

    最近在学C语言程序设计时总是遇到一些概念理解上的不清晰与混乱的地方,在一次偶然间想到了以前看过的一部电影<我是谁,没有一个系统是安全的>,里面的主角用社会工程学的想法结合黑客知识化险为夷, ...

  4. .NET Core跨平台部署

    目录 .NET Core跨平台部署 1. Windows-IIS 1.1 安装.NET Core Windows Server Hosting 1.2 配置应用程序池 1.3 使用发布文件 2 Lin ...

  5. 比较两个slice、struct或者map是否相等

    我们可以直接使用reflect.DeepEqual来比较两个slice.struct或者map是否相等 package main import ( "fmt" "refl ...

  6. RabbitMQ广播:direct模式

    一. 消息的广播需要exchange:exchange是一个转发器,其实把消息发给RabbitMQ里的exchange fanout: 所有bind到此exchange的queue都可以接收消息,广播 ...

  7. Spring-扫描注解原理,注解自动扫描原理分析

    注解自动扫描原理分析 在spring的配置文件中加入如下代码,spring便开启了自动扫描,那么它的底层到底是如何实现的呢? <context:component-scan base-packa ...

  8. 做自己的docker镜像(基于ubuntu:16.04)

    基于ubuntu:16.04 apt-get update -y apt-get install sudo -y 换源 sudo apt-get install vim sudo vim /etc/a ...

  9. mysql 有没有参数都报错“mysql: unknown option”

    报错: [root@XXXX tmp]# mysql -uroot -pmysql: unknown option '--You have new mail in /var/spool/mail/ro ...

  10. Oracle Metric sequence load elapsed time

    Oracle Metric sequence load elapsed time The sequence load elapsed time Oracle metric is the amount ...