JWT入门简介

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

目录

  • 什么是JWT

    • 头部(Header)
    • 载荷(Payload)
    • 签名(Signature)
  • JWT使用场景
  • 如何传递JWT
  • JWT应用实践
  • 总结
    • JWT运行流程
    • 与传统Session方式的比较
    • 使用JWT时注意事项

什么是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设计单点登录系统

原文地址:https://www.cnblogs.com/nuccch/p/9083663.html

时间: 2024-10-09 23:00:57

JWT入门简介的相关文章

Junit(3)JUnit和单元测试入门简介

1.几个相关的概念 白盒测试--把测试对象看作一个打开的盒子,程序内部的逻辑结构和其他信息对测试人员是公开的. 回归测试--软件或环境的修复或更正后的"再测试",自动测试工具对这类测试尤其有用. 单元测试--是最小粒度的测试,以测试某个功能或代码块.一般由程序员来做,因为它需要知道内部程序设计和编码的细节. JUnit --是一个开发源代码的Java测试框架,用于编写和运行可重复的测试.他是用于单元测试框架体系xUnit的一个实例(用于java语言).主要用于白盒测试,回归测试. 2.

Dubbo入门简介(一)

Dubbo入门简介(一) 一.什么是Dubbo Dubbo是阿里巴巴开源出来的一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案.通过扩展spring schema 和 注解标签,可以和spring无缝集成(http://blog.csdn.net/achilles12345/article/details/41789527) 核心组件: Remoting:提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交

zabbix专题:第一章 zabbix入门简介(更新中)

zabbix入门简介 备注:本章节图片来自互联网 对Linux有兴趣的朋友加入QQ群:476794643 在线交流 本节目录大纲 zabbix专题:第一章 zabbix简介 我们为什么需要监控? 常用的开源监控系统有哪些? Zabbix是什么? Zabbix的功能和特性 Zabbix的架构 Zabbix的工作流程 zabbix的进程 zabbix的逻辑关系图 zabbix监控环境中相关术语 zabbix的流程图,其串联了各术语之间的关系 zabbix的监控架构 zabbix专题:第一章 zabb

Unity 3D入门简介

最近在刚开始学习Unity 3D,在这里记录一下学习心得和学习笔记,边学边写,可能会比较零散.好了,废话不多说,今天从Unity 3D入门写起,主要简要介绍一下Unity 3D的和一些学习资料.以下如果不作特别说明,均是指Windows平台的Unity 3D,版本是4.5.1f3. Unity 3D是一款游戏开发引擎,目前支持2D和3D游戏的开发,其最大的优势就是跨平台性非常好,可以很容易的将PC端制作的游戏移植到Android.IOS等移动平台,当然也可以创建网页游戏.刚开始进行Unity 3

JUnit入门简介(转)

1.学习摘要 看<重构-改善既有代码的设计>这本书的时候,里面提到测试环境对于重构的重要性,想到之前在编写代码的时候都是通过System.out和 alert来做测试,非常麻烦,而且不够正规,对于即将步入工作的人来说,一个正规的写代码习惯和测试习惯是非常重要的,因此我觉得好好学学如何使用 JUnit. 在JUnit和单元测试入门简介一文中提到“JUnit框架是一个典型的Composite模式:TestSuite可以容纳任何派生自Test的对象:当调用TestSuite对象的run()方法是,会

nodejs的入门简介

1.概念 nodejs不是一种独立的语言,也和php.java..net即是开发语言也是平台不同,其也不是javascript的框架,不能和Extjs相提并论.其是将javascript运行在服务端的开发平台[开发平台就是一个供程序员开发软件的软件.其实也就是一个工具(如:微软VS,JAVA一般是Eclipse……)而已,可以编辑和执行程序员写的开发代码语言,从而变成人们平时使用的软件(如QQ.MSN.游戏……)] 2.作用 javascript是由客户端产生,Node.js是为网络而生. 具有

Linux 内核入门简介

作者 : 韩曙亮 转载请出名出处 : http://blog.csdn.net/shulianghan/article/details/38636827 一. Linux 内核简介 1. 内核功能简介 (1) 操作系统 和 内核 简介 操作系统 : -- 功能 : 完成基本功能 和 系统管理; -- 组成 : 内核(kernel), 设备驱动程序(driver), 启动引导程序(bootloader), 命令行(shell), 用户界面(UI), 文件系统(filesystem), 管理工具;

构建基于JAVASCRIPT的移动WEB CMS入门——简介(转载)

构建基于JAVASCRIPT的移动WEB CMS入门——简介 发布时间:2014-07-17 14:16:18.035763 看到项目上的移动框架,网上寻找了一下,发现原来这些一开始都有.于是,找了个示例开始构建一个移动平台的CMS——墨颀 CMS,方便项目深入理解的同时,也可以自己维护一个CMS系统. 构建框架 尝试过用AngularJS和EmberJS,发现对于使用AngluarJS以及EmberJS来说,主要的问题是要使用自己熟悉的东西没那么容易引入.而且考虑到谷歌向来对自己的项目的支持不

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

一.入门简介 在学习之前,要先了解ASP.NET Core是什么?为什么?很多人学习新技术功利心很重,恨不得立马就学会了. 其实,那样做很不好,马马虎虎,联系过程中又花费非常多的时间去解决所遇到的“问题”,是简单的问题,对,就是简单,就是因为觉得简单被忽略的东西,恰恰这才是最重要的. 1.学习资料 首先,介绍下哪里可以获得学习资料 英文官网,最好的文档,英语得过硬 https://docs.microsoft.com/en-us/aspnet/core/ 可惜当年英语就是马马虎虎过来的,所以找了