官网: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. layui中弹出层的两种表达方式

    方式一: 定义js中定义html变量 方式二: 设置div :hidden:hidden 布局 数据表格自适应大小: 代码: <style> .btn-container { margin ...

  2. MySQL 数据表创建及管理

    use stuinfo; -- 指定当前数据库 CREATE table if not exists student1( -- 创建数据表student1 sNo ) not NULL, sName ...

  3. 虚拟机配置Linux上网环境

    概要:在虚拟机安装CentOS6.5的环境后,配置NAT模式,修改系统文件支持上网. (1)ip地址的配置,IP地址的子网掩码为255.255.255.0. (2)网关的指定,也就是默认路由,当我们需 ...

  4. Python基础之迭代器和生成器

    阅读目录 楔子 python中的for循环 可迭代协议 迭代器协议 为什么要有for循环 初识生成器 生成器函数 列表推导式和生成器表达式 本章小结 生成器相关的面试题 返回顶部 楔子 假如我现在有一 ...

  5. 上传本地文件到GitHub上

    问题解决 今天在windows上上传本地文件到github,出现用户名和仓库不匹配的情况,解决方式如下: 打开控制面板,选择用户账户 把该删除的账户删除一下就行了. 上传文件的步骤如下: 将上传的文件 ...

  6. 在windows下远程访问linux桌面

    一.安装xrdp工具: #  yum install xrdp #   yum install tigervnc-server #   service xrdp start 以上三个命令执行完毕安装完 ...

  7. KindEditor富文本编辑器使用

    我的博客本来打算使用layui的富文本编辑器,但是出了一个问题,无法获取编辑器内容,我参考官方文档,获取内容也就那几个方法而已,但是引入进去后始终获取的值为空,百度和bing都试过了,但是始终还是获取 ...

  8. IDEA+循环语句 or 输出语句 快捷操作

    IDEA+循环语句 or 输出语句 快捷操作:https://blog.csdn.net/shijiebei2009/article/details/44726433 for循环:仅输入fori然后回 ...

  9. LOJ3053 十二省联考2019 希望 容斥、树形DP、长链剖分

    传送门 官方题解其实讲的挺清楚了,就是锅有点多-- 一些有启发性的部分分 L=N 一个经典(反正我是不会)的容斥:最后的答案=对于每个点能够以它作为集合点的方案数-对于每条边能够以其两个端点作为集合点 ...

  10. mm-wiki安装部署

    参考连接:https://github.com/skyhack1212/mm-wiki 打开 https://github.com/phachon/mm-wiki/releases 找到对应平台的版本 ...