JWT入门简介
官网: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个部分:
- 公共声明
- 私有声明
公共声明中可以包含如下信息(建议但不强制使用):
- 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协议的应用中可以有如下几种传递方式:
- 在HTTP消息头中传递,如:Authorization: 'Bearer ' + header.body.signature
- 在Cookie中传递,如:Set-Cookie: jwt=header.body.signature; HttpOnly;domain=.lenovo.com
- 在消息体中传递: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时注意事项
- 不应该在JWT的Payload部分存放敏感信息,因为Base64编码是很容易被解码的,这部分相当于明文数据。
- 保护好服务端用于加密的secret私钥,该私钥非常重要。
- 请使用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入门简介的更多相关文章
- 掌握 Ajax,第 1 部分: Ajax 入门简介
转:http://www.ibm.com/developerworks/cn/xml/wa-ajaxintro1.html 掌握 Ajax,第 1 部分: Ajax 入门简介 理解 Ajax 及其工作 ...
- MongoDB入门简介
MongoDB入门简介 http://blog.csdn.net/lolinzhang/article/details/4353699 有关于MongoDB的资料现在较少,且大多为英文网站,以上内容大 ...
- (转)Web Service入门简介(一个简单的WebService示例)
Web Service入门简介 一.Web Service简介 1.1.Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从I ...
- NodeJS入门简介
NodeJS入门简介 二.模块 在Node.js中,以模块为单位划分所有功能,并且提供了一个完整的模块加载机制,这时的我们可以将应用程序划分为各个不同的部分. const http = require ...
- ASP.NET Core学习之一 入门简介
一.入门简介 在学习之前,要先了解ASP.NET Core是什么?为什么?很多人学习新技术功利心很重,恨不得立马就学会了. 其实,那样做很不好,马马虎虎,联系过程中又花费非常多的时间去解决所遇到的“问 ...
- webservice入门简介
为了梦想,努力奋斗! 追求卓越,成功就会在不经意间追上你 webservice入门简介 1.什么是webservice? webservice是一种跨编程语言和跨操作系统平台的远程调用技术. 所谓的远 ...
- Web Service入门简介(一个简单的WebService示例)
Web Service入门简介 一.Web Service简介 1.1.Web Service基本概念 Web Service也叫XML Web Service WebService是一种可以接收从I ...
- Android精通教程-第一节Android入门简介
前言 大家好,给大家带来Android精通教程-第一节Android入门简介的概述,希望你们喜欢 每日一句 If life were predictable it would cease to be ...
- Nginx入门简介
Nginx入门简介 Nginx 介绍 Nginx (engine x) 是一个高性能的HTTP和反向代理服务,也是一个IMAP/POP3/SMTP服务.Nginx是由伊戈尔·赛索耶夫为俄罗斯访问量第二 ...
随机推荐
- layui中弹出层的两种表达方式
方式一: 定义js中定义html变量 方式二: 设置div :hidden:hidden 布局 数据表格自适应大小: 代码: <style> .btn-container { margin ...
- MySQL 数据表创建及管理
use stuinfo; -- 指定当前数据库 CREATE table if not exists student1( -- 创建数据表student1 sNo ) not NULL, sName ...
- 虚拟机配置Linux上网环境
概要:在虚拟机安装CentOS6.5的环境后,配置NAT模式,修改系统文件支持上网. (1)ip地址的配置,IP地址的子网掩码为255.255.255.0. (2)网关的指定,也就是默认路由,当我们需 ...
- Python基础之迭代器和生成器
阅读目录 楔子 python中的for循环 可迭代协议 迭代器协议 为什么要有for循环 初识生成器 生成器函数 列表推导式和生成器表达式 本章小结 生成器相关的面试题 返回顶部 楔子 假如我现在有一 ...
- 上传本地文件到GitHub上
问题解决 今天在windows上上传本地文件到github,出现用户名和仓库不匹配的情况,解决方式如下: 打开控制面板,选择用户账户 把该删除的账户删除一下就行了. 上传文件的步骤如下: 将上传的文件 ...
- 在windows下远程访问linux桌面
一.安装xrdp工具: # yum install xrdp # yum install tigervnc-server # service xrdp start 以上三个命令执行完毕安装完 ...
- KindEditor富文本编辑器使用
我的博客本来打算使用layui的富文本编辑器,但是出了一个问题,无法获取编辑器内容,我参考官方文档,获取内容也就那几个方法而已,但是引入进去后始终获取的值为空,百度和bing都试过了,但是始终还是获取 ...
- IDEA+循环语句 or 输出语句 快捷操作
IDEA+循环语句 or 输出语句 快捷操作:https://blog.csdn.net/shijiebei2009/article/details/44726433 for循环:仅输入fori然后回 ...
- LOJ3053 十二省联考2019 希望 容斥、树形DP、长链剖分
传送门 官方题解其实讲的挺清楚了,就是锅有点多-- 一些有启发性的部分分 L=N 一个经典(反正我是不会)的容斥:最后的答案=对于每个点能够以它作为集合点的方案数-对于每条边能够以其两个端点作为集合点 ...
- mm-wiki安装部署
参考连接:https://github.com/skyhack1212/mm-wiki 打开 https://github.com/phachon/mm-wiki/releases 找到对应平台的版本 ...