JWT介绍

官方介绍

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

什么时候你应该用JSON Web Token ?

下列场景中使用JSON Web Token是很有用的:

  • Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。
  • Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWT可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
    在这里插入图片描述

博主讲解

JWT具体用来干什么的?

JSON Web Token (JWT) 就是用来保证数据不可篡改,鉴定消息无篡改;绝不是保证消息保密性的东西;这一点要特别清楚,首先清楚JWT具体用来干什么的,从而能够理解其设计原理;经过jwt加密的消息是公开可见的,是不可篡改的,因此jwt的消息体中payload的数据不要放敏感数据,如果需要放敏感数据,请加密。例如payload中可以存放用户名,密码的话需要加密后在存放;

明白了上述用途,再去了解原理才会融会贯通,下面讲解一下非对称加密;

非对称加密

非对称加密(如RSA)是通过公钥和私钥进行加密,解密的过程;私钥只能被一方所拥有,一般是服务器;公钥可以公开,任何人可以获取公钥;通过公钥加密的消息只能通过私钥解密,通过私钥加密的消息也只能通过公钥解密;但是这个过程不是线性的,使用公钥加密和私钥加密的密文是不一样的;

  • 公钥加密,私钥解密

    这种情况用于消息的加密,保证消息的安全性,即保证消息的不可见性,保密性,敏感性;一般是客户端向服务器发送消息时为了避免消息被泄露,使用服务器公开的公钥进行消息加密形成密文发给服务器,只有服务器使用私钥才能解密密文获得消息。

  • 私钥加密,公钥解密

    使用私钥加密,可以作为数字签名防止伪造,但是无法保证数据的不可见性;因为私钥加密的密文可以被其他用户使用公钥解密,其他用户也就可以获取消息,如果要避免敏感消息被盗取,需要将敏感消息加密后放在jwt的payload中。

小常识

遗憾的是,公钥算法速度非常慢,大约比对称算法慢 1000 倍。 使用它们来加密大量数据是不切实际的。 实际上,公钥算法用于加密 会话密钥。 对称算法 用于加密/解密大多数数据。由于对消息进行签名实际上会加密消息,因此使用公钥签名算法对大型消息进行签名是不切实际的。

JWT的数据结构

JWT其实就是一个很长的字符串,字符之间通过"."分隔符分为三个子串,各字串之间没有换行符。每一个子串表示了一个功能块,总共有三个部分:JWT头(header)有效载荷(payload)签名(signature),如下图所示:

在这里插入图片描述

JWT头

JWT头是一个描述JWT元数据的JSON对象,通常如下所示:

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

alg:表示签名使用的算法,默认为HMAC SHA256(写为HS256)

typ:表示令牌的类型,JWT令牌统一写为JWT

最后,使用Base64 URL算法将上述JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。

有效载荷

有效载荷,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。

有效载荷部分规定有如下七个默认字段供选择:

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

除以上默认字段外,还可以自定义私有字段。

userid:100001
username:nick
email:123@qq.com

最后,同样使用Base64 URL算法将有效载荷部分JSON对象转换为字符串,注意Base64 URL只是编码算法,不是加密算法,可以被解码得到数据。

签名

签名实际上是一个使用私钥加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改。

首先需要指定一个密码(secret,可以服务器随机设置,相当于种子),该密码仅仅保存在服务器中,并且不能向用户公开。然后使用JWT头中指定的签名算法(默认情况下为HMAC SHA256),根据以下公式生成签名哈希:为了得到签名部分,你必须有编码过的header、编码过的payload、一个密码,签名算法是header中指定的那个,然对它们签名即可。

HMACSHA256(  base64UrlEncode(header) + "." + base64UrlEncode(payload),  secret  )

在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串 ,每个部分用"."分隔,就构成整个JWT对象。

xxxxx.yyyyy.zzzzz,xxxxx是header经过Base64 URL编码后的字符串,yyyyy是payload经过Base64 URL编码后的字符串,zzzzz是对xxxxx.yyyyy经过私钥加密后得到的字符串;

JWT签名算法

JWT签名算法中,一般有两个选择:HS256和RS256。

HS256 (带有 SHA-256 的 HMAC )是一种对称加密算法, 双方之间仅共享一个密钥,没有公钥私钥之分。由于使用相同的密钥生成签名和验证签名, 因此必须注意确保密钥不被泄密。

RS256 (采用SHA-256 的 RSA 签名) 是一种非对称加密算法, 它使用公共/私钥对: JWT的提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。

token认证

在讲token认证之前,我们先回顾一下以前熟悉的seesion认证,Cookie-session 认证机制是通过浏览器带上来Cookie对象来与服务器端的session对象匹配来实现状态管理。第一次请求认证在服务端创建一个Session对象,同时在用户的浏览器端创建了一个Cookie对象;当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效。session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。最大的问题是session将用户信息存储在了服务器上,这样会导致服务器存储压力。而token相反,把认证签名存放在客户端,客户端访问只需要带上token令牌签名即可访问自己的账户;

JSON Web Tokens是如何工作的

在认证的时候,当用户用他们的凭证成功登录以后,一个JSON Web Token将会被返回。此后,token就是用户凭证了,你必须非常小心以防止出现安全问题。一般而言,你保存令牌的时候不应该超过你所需要它的时间。HTTP协议是无状态的,也就是说,如果我们已经认证了一个用户,那么他下一次请求的时候,服务器不知道我是谁,我们必须再次认证。传统的做法是将已经认证过的用户信息存储在服务器上,比如Session。用户下次请求的时候带着Session ID,然后服务器以此检查用户是否认证过。Sessions : 每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大。 JWT与Session的差异 相同点是,它们都是存储用户信息;然而,Session是在服务器端的,而JWT是在客户端的。

  • Session方式存储用户信息的最大问题在于要占用大量服务器内存,增加服务器的开销。

  • 而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。

  • Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。

基于Token的身份认证是如何工作的 基于Token的身份认证是无状态的,服务器或者Session中不会存储任何用户信息。没有会话信息意味着应用程序可以根据需要扩展和添加更多的机器,而不必担心用户登录的位置。

虽然这一实现可能会有所不同,但其主要流程如下:

  1. 用户携带用户名和密码请求访问 ,第一次访问必须使用账号密码;

  2. 服务器校验用户凭据,验证账号密码;

  3. 应用提供一个token给客户端,通过后生成jwt数据xxxxx.yyyyy.zzzzz作为token;

  4. 客户端存储token,并且在随后的每一次请求中都带着它,token用于验证用户身份;

  5. 服务器校验token并返回数据,如何验证呢?只需要使用私钥对xxxxx.yyyyy进行加密,然后得到的结果与zzzzz对比即可,通过后对yyyyy进行base64解码获得用户id,即证明客户端是上一次访问的用户,且数据未被篡改;如果被篡改了,那么xxxxx.yyyyy加密不能得到zzzzz。

    每一次请求都需要token,Token应该放在请求header中 ,我们还需要将服务器设置为接受来自所有域的请求。

其他

用Token的好处 - 无状态和可扩展性:Tokens存储在客户端。完全无状态,可扩展。我们的负载均衡器可以将用户传递到任意服务器,因为在任何地方都没有状态或会话信息。 - 安全:Token不是Cookie。(The token, not a cookie.)每次请求的时候Token都会被发送。而且,由于没有Cookie被发送,还有助于防止CSRF攻击。即使在你的实现中将token存储到客户端的Cookie中,这个Cookie也只是一种存储机制,而非身份认证机制。没有基于会话的信息可以操作,因为我们没有会话!

还有一点,token在一段时间以后会过期,这个时候用户需要重新登录。这有助于我们保持安全。还有一个概念叫token撤销,它允许我们根据相同的授权许可使特定的token甚至一组token无效。JWT与OAuth的区别 -OAuth2是一种授权框架 ,JWT是一种认证协议 -无论使用哪种方式切记用HTTPS来保证数据的安全性 -OAuth2用在使用第三方账号登录的情况(比如使用weibo, qq, github登录某个app),而JWT是用在前后端分离, 需要简单的对后台API进行保护时使用。

实战jwt

jjwt是一个提供JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。

导入依赖
    <dependencies>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
实战案例
package com.example.test;

import cn.hutool.core.io.FileUtil;
import io.jsonwebtoken.*;
import io.jsonwebtoken.SignatureException;
import org.junit.Test;

import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;

public class JwtTest {

    /**
     * 对称加密算法
     */

    @Test
    public void test_HS256(){
        String key = "8888"; //秘钥要大于3个字符,至少四个字符
        // 封装jwt的head部信息
        HashMap<String, Object> header = new HashMap<>();
        // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
        header.put("arg",SignatureAlgorithm.HS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
        header.put("typ","JWT"); //指定令牌类型

        // 封装jwt的body部分
        HashMap<String, Object> body = new HashMap<>();
        body.put("account","15637283927");
        body.put("phone","123456");
        body.put("role","user");

        // 生成JWT令牌
        String token = Jwts.builder()
                .setHeader(header)
                .setClaims(body)
                .setId("00001") //设置这个jwt的唯一标识,不写也可以
                .signWith(SignatureAlgorithm.HS256,key) //使用公钥加密
                .compact();
        // 打印生成的jwt令牌token
        System.out.println(token);
        String[] split = token.split("\\.");
        System.out.println("header:"+split[0]);
        System.out.println("body:"+split[1]);

        // 解析jwt令牌,使用服务器的私钥解密
        Jwt res = null;
        try {
            res = Jwts
                    .parser()
                    .setSigningKey(key)
                    .parse(token);

            Header header_parser = res.getHeader();
            Object body_parser = res.getBody();
            System.out.println(header_parser);
            System.out.println(body_parser);

        } catch (SignatureException e) {
            System.out.println("秘钥错误!");
        }
    }
    /**
     * 生成公钥和私钥
     * @param seedPassword 提供种子密码
     * @return
     * @throws Exception
     */
    //生成自己的 秘钥/公钥 对
    public byte[][] GeneratPubPriKey(String seedPassword) throws Exception{
        // 使用RSA算法,获得RSA算法实例
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(seedPassword.getBytes());
        // 使用种子密码初始化
        keyPairGenerator.initialize(1024, secureRandom);
        // 获得密码对,编码为byte类型
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        //存入本地文件,可以使用最后面提供的代码读取秘钥文件,这里作为例子直接返回
//        FileUtil.writeBytes(publicKeyBytes, "d:\\pub.key");
//        FileUtil.writeBytes(privateKeyBytes, "d:\\pri.key");
        byte[][] key = new byte[][]{publicKeyBytes,privateKeyBytes};
        return key;
    }

    /**
     * 非对称加密算法:实现的是私钥加密,公钥解密
     * 一般用于身份识别,电子签名,确认加密的数据属于公钥下发方
     * 能用你的公钥解密,那么数据一定是你的(一定使用你的私钥加密的)
     * 用于识别服务器身份
     */
    @Test
    public void test_RS256() throws Exception {
        // 封装jwt的head部信息
        HashMap<String, Object> header = new HashMap<>();
        // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
        header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
        header.put("typ","JWT"); //指定令牌类型

        // 封装jwt的body部分
        HashMap<String, Object> body = new HashMap<>();
        body.put("account","mimiNick");
        body.put("phone","123456");
        body.put("role","user");

        // 获取公钥和私钥
        byte[][] keys = GeneratPubPriKey("8888888");
        // 获取私钥对象
        byte[] priKeyByte = keys[1];
        PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(spec2);

        // 生成JWT令牌
        String token = Jwts.builder()
                .setHeader(header)
                .setClaims(body)
                .setId("00001") //设置这个jwt的唯一标识,不写也可以
                .signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密
                .compact();

        // 打印生成的jwt令牌token
        System.out.println(token);
        String[] split = token.split("\\.");
        System.out.println("header:"+split[0]);
        System.out.println("body:"+split[1]);

        //获取公钥
        byte[] publicByte = keys[0];
        X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte);
        PublicKey publicKey = kf.generatePublic(spec);

        // 解析jwt令牌,使用服务器的私钥解密
        Jwt res = null;
        try {
            res = Jwts
                    .parser()
                    .setSigningKey(publicKey) //使用公钥解密
                    .parse(token);

            Header header_parser = res.getHeader();
            Object body_parser = res.getBody();
            System.out.println(header_parser);
            System.out.println(body_parser);

        } catch (SignatureException e) {
            System.out.println("秘钥错误!");
        }
    }



    @Test
    public void test_RS256_2() throws Exception {
        // 封装jwt的head部信息
        HashMap<String, Object> header = new HashMap<>();
        // SignatureAlgorithm.HS256.getValue() 使用HS256非对称加密算法
        header.put("arg",SignatureAlgorithm.RS256.getValue()); //指定加密算法,不使用加密算法的话设置value为none即可
        header.put("typ","JWT"); //指定令牌类型

        // 封装jwt的body部分
        HashMap<String, Object> body = new HashMap<>();
        body.put("account","mimiNick");
        body.put("phone","123456");
        body.put("role","user");

        // 获取公钥和私钥
        byte[][] keys = GeneratPubPriKey("8888888");
        // 获取私钥对象
        byte[] priKeyByte = keys[1];
        PKCS8EncodedKeySpec spec2 = new PKCS8EncodedKeySpec(priKeyByte);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = kf.generatePrivate(spec2);

        //获取公钥
        byte[] publicByte = keys[0];
        X509EncodedKeySpec spec = new X509EncodedKeySpec(publicByte);
        PublicKey publicKey = kf.generatePublic(spec);


        // 生成JWT令牌
        String token = Jwts.builder()
                .setHeader(header)
                .setClaims(body)
                .setId("00001") //设置这个jwt的唯一标识,不写也可以
                .signWith(SignatureAlgorithm.RS256,privateKey) //使用私钥加密
                .compact();

        // 打印生成的jwt令牌token
        System.out.println(token);
        String[] split = token.split("\\.");
        System.out.println("header:"+split[0]);
        System.out.println("body:"+split[1]);


        // 解析jwt令牌,使用服务器的私钥解密
        Jwt res = null;
        try {
            res = Jwts
                    .parser()
                    .setSigningKey(publicKey) //使用公钥解密
                    .parse(token);

            Header header_parser = res.getHeader();
            Object body_parser = res.getBody();
            System.out.println(header_parser);
            System.out.println(body_parser);

        } catch (SignatureException e) {
            System.out.println("秘钥错误!");
        }
    }
}

获取磁盘上的秘钥文件代码
    //获取私钥
    public PrivateKey getPriKey() throws Exception{
        InputStream resourceAsStream = 
            this.getClass().getClassLoader().getResourceAsStream("pri.key");
        DataInputStream dis = new DataInputStream(resourceAsStream);
        byte[] keyBytes = new byte[resourceAsStream.available()];
        dis.readFully(keyBytes);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    //获取公钥
    public PublicKey getPubKey() throws Exception{
        InputStream resourceAsStream = 
            this.getClass().getClassLoader().getResourceAsStream("pub.key");
        DataInputStream dis = new DataInputStream(resourceAsStream);
        byte[] keyBytes = new byte[resourceAsStream.available()];
        dis.readFully(keyBytes);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePublic(spec);
    }
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐