JWT快速入门及理论知识

代码部分

代码部分内容全部默认使用JJWT,且参考于JJWT GitHub仓库的自述文件(readme.md)

快速开始

这里使用封装好的JJWT进行JWT的生成和解密

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <!-- jdk11以上注释bcprov-jdk15on依赖-->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.60</version>
            <scope>runtime</scope>
        </dependency>

JWT生成

因为这里使用了builder(建造者模式)的原因,复杂的操作全部都在这两行简单代码之下

        //快速开始
        //签名使用加密算法设置
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        //sub 可达鸭
        String compact = Jwts.builder().setSubject("可达鸭").signWith(secretKey).compact();
        System.out.println(compact);
        //运行结果:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlj6_ovr7puK0ifQ.9YyUs3TafLrJoXmBwMswFHW_ycQkTlhqaKti4Wh2aoM
        //签名部分与我不同属于正常现象

这里我们都做了些什么呢?
1.创建一个JWT并设置sub值为可达鸭
2.使用适用于HmacSHA256算法的密钥对JWT进行签名
3.最后将其压缩成一个String,一般被签名的JWT也被叫做JWS

JWT验证

        //jwt解码器建造工厂 设置jwt解码器签名加密方式 建造 用此解码器验证jwt(exception则表示jwt不合法或者签名加密方式不对)
        //获取jwt payload(body)部分 获取subject
        String subject = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(compact).getBody().getSubject();
        System.out.println(subject);
        //运行结果 可达鸭

需要注意,如果签名(secretKey)有误,则会产生SignatureException,也可直接处理JwtException

完整步骤

创建JWT

创建JWT的步骤如下:
1.使用Jwts.builder()方法创建JwtBuilder实例
2.调用JwtBuilder方法(setHeaderParam/claim)以根据需要添加标头参数和声明
3.指定要用于对JWT进行签名的SecretKey或不对称PrivateKey
4.最后,调用compact()方法进行压缩和签名,生成最终的jwt。
例:

        String jwt = Jwts.builder() //构建Jwt建造者
                .setHeaderParam("typ", "JWT") //设置标头参数
                .claim("hello", "world") //设置索偿(payload jwt主体内容)
                .claim("name", "可达鸭")
                .signWith(secretKey) //设置加密使用的密钥及加密算法(加密算法可不指定)
                .compact();

标头参数

JWT标头应当写明有关JWT的格式和加密操作的相关数据。
如果需要设置一个或多个JWT标头参数,例如kid (Key ID)标头参数,则可以使用setHeaderParam方法根据需要调用 一次或多次。
例:

        String jwt = Jwts.builder() //构建Jwt建造者
                .setHeaderParam("typ", "JWT") //设置标头参数
                //.setHeaderParam("kid","myKey") 等等

注意:调用setHeader如果key值相等会出现覆盖标头参数的情况,任何情况下,JJWT都会覆盖已经被设置好的alg和zip标头。

载荷

JWT的载荷用于存储需要的数据,可以通过调用一次或多次claim存储载荷,也可以通过setClaims直接存储一个Map进去
例:

        Map<String,Object> testMap = new HashMap<String, Object>(){
            {
                put("hello","hashMap");
                put("name","psyDuck");
            }
        };
        String jwt = Jwts.builder() //构建Jwt建造者
                .setHeaderParam("typ", "JWT") //设置标头参数
                .claim("hello", "world") //设置索偿(payload jwt主体内容)
                .claim("name", "可达鸭")
                .setClaims(testMap)//存入HashMap键值对

注意:调用claim/setClaims如果key值相等会出现覆盖载荷参数的情况,在调用setClaims时会清空所有使用claim存入的值。

签名

我们可以通过以下代码自动生成一个随机的key

SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //也可以是HS384或HS512等等等等

如果需要保存这个Key,可以使用以下代码

String secretString = Encoders.BASE64.encode(key.getEncoded());

之后对已经设置好标头和载荷的JWT进行加密,加密算法会自动选择一个最适合的使用。

        String jwt = Jwts.builder() //构建Jwt建造者
                .setHeaderParam("typ", "JWT") //设置标头参数
                .claim("hello", "world") //设置索偿(payload jwt主体内容)
                .claim("name", "可达鸭")
                .setClaims(testMap)//存入HashMap键值对
                .signWith(secretKey) //设置加密使用的密钥及加密算法(加密算法可不指定)
                .compact();

加密结束之后通过.compact完成JWT的生成。

解析JWT

相对于创建JWT,解析同样用到了Builder(建造者模式),我们只需要提供加密时所提供的密钥,就可以构建一个可重复使用的JWT解析器用以解析JWT。

        //读取JWT
        String s = new String(Base64.encodeBase64URLSafe(secretKey.getEncoded()));
        System.out.println(s);
        JwtParser jwtParser = Jwts.parserBuilder() //构建JWT解析器建造者
                .setSigningKey(s) //指定要解析的JWT所使用的secretKey用以判断签名是否正确 不正确代表JWT已不可信(会抛出异常)
                .build(); //调用build来返回线程安全的JWT解析器
                //填入需要解析的JWT
        Jws<Claims> jws = jwtParser.parseClaimsJws(jwt);
        //header={typ=JWT, alg=HS256},body={name=psyDuck, hello=hashMap},signature=9tT7x5dvEbjr1KffJy9r-r1sOspJ5Jj83swCyjK2ERc
        System.out.println(jws);

理论部分

什么是JWT规范?

JWT规范将一个token分为3部分,分别是标头(header)、载荷(payload)、签名(verify signature)三部分并以.进行分割,也就是说一个符合JWT规范所生成的token格式应当如下:
xxxxx.yyyyy.zzzzz
tip:JWT官网:https://jwt.io/

标头(header)

标头首先是一个JSON,通常包含两部分。分别是代表令牌类型和所使用的签名的加密算法的typ和alg,
例:

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

之后将此JSON以Base64URL加密作为标头

JWT推荐使用的加密方式

HS256、HS384、HS512,
RS256、RS384、RS512,
ES256、ES384、ES512,
PS256、PS384;
tip:大部分JWT的封装库还另外实现了ES256K、PS512、EdDSA等数种加密方式

载荷(payload)

载荷部分说的通俗一点就是你希望传递的信息部分,这部分开始也是一个JSON,JWT规范将JSON的key叫做索赔(Claim)。
索赔分为3种,已注册的、公开的、私人的
已注册的索赔是JWT在IANA“ JSON Web令牌声明”注册表中注册过的,
公开的索赔是由我们,也就是开发人员随意定义的,
私人的索赔是由JWT的生产者、消费者所共同定义的索赔,
需要注意的是,私人索赔更易发生冲突,应谨慎使用,同时索赔最好只有3个字符,因为JWT是紧凑(compact)的。
例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

之后将此JSON以Base64URL加密作为载荷。

已注册的索赔

请注意,已注册的索赔全部为可用可不用,并不一定要使用以下索赔

名称备注
iss(发布者)其值为区分大小写的字符串或StringOrURI
sub(主题)该值应当是局部或全局唯一的,且是区分大小写的字符串或StringOrURI
aud(收听者)其值为区分大小写的字符串数组,每个字符串都是StringOrURI,如果只有一个收听者的情况下,其值可以仅为一个StringOrURI
exp(到期时间)其值应当为一个NumericDate,如果处理此JWT的日期晚于exp,则弃用此JWT
nbf(不早于)其值应当为一个NumericDate,如果处理此JWT的日期早于nbf,则弃用此JWT
iat(签发时间)其值应当为一个NumericDate,用以说明此JWT生成的时间
jti(JWT ID)其值应当为一个区分大小写的字符串,主要用来规避重放攻击,需要考虑多个发布者和收听者时可能出现的冲突

通俗解释名词
StringOrURI:JSON数据
NumericDate:时间戳

签名(verify signature)

JWT的签名首先需要你随机生成一个密钥,例:

        String secret = "123456";//普通密钥
        secret = new String(Base64.encodeBase64URLSafe("123456".getBytes("UTF-8")));//Base64加密密钥
        //......各种特殊加密密钥

密钥可以被接受的几种格式如下

        //key值至少应为32个可见数字 也就是 32*8=256位 否则Keys会因为key值位数不够抛出一个异常
        String key = "11111111111111111111111111111111";
//        密钥可以为 一个原始字符串
//        SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes());
//        密钥可以为 一个byte数组
//        SecretKey secretKey = Keys.hmacShaKeyFor(new byte[]{
//                1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
//                ,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17
//        });
        //密钥可以为 一个Base64编码后的String
        //在使用BASE64编码key时,相同数字会被压缩 所以我们需要提供额外X个可见数字
//        key += "34567891011";
//        SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key));

        //密钥可以为 一个Base64URL编码后的String 同样需要额外提供X个可见数字
//        key += "34567891011";
//        SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(key));

之后将标头(已base64url加密的)和载荷(已base64url加密的)用.拼接成一个完整的字符串作为message
将生成的密钥作为secret进行标头中设置好的加密方式的加密,默认是HS256的话也就是进行一次HmacSHA256加密。
因为这个签名始终是一个byte类型的数组,所以我们需要对其进行base64URL编码使其变成一个正常的字符串,不过简单看一下jjwt的源码,jjwt在最终获取key时已经进行了一次base64编码,所以我们可以直接使用其作为签名。

    public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException {
        Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
        Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty.");
        Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures.  If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
        SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
        return this.signWith((Key)key, (SignatureAlgorithm)alg);
    }

    public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
        Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty.");
        Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures.  If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
        byte[] bytes = (byte[])Decoders.BASE64.decode(base64EncodedSecretKey);
        return this.signWith(alg, bytes);
    }

最后更新于2021年5月28日
原创不易,如果该文章对你有所帮助,望左上角点击关注~如有任何技术相关问题,可通过评论联系我讨论,我会在力所能及之内进行相应回复以及开单章解决该问题.

该文章如有任何错误请在评论中指出,感激不尽,转载请附出处!
*个人博客首页:https://blog.csdn.net/yjrguxing ——您的每个关注和评论都对我意义重大

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值