解密JWT的本质 通过java代码带你一步步还原Json Web Token的组成及验证原理

测试环境: jdk 11 maven 项目, 依赖包:

 <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
    </dependency>

<!--javax.xml.bind.DatatypeConverter 
JAXB API是java EE 的API,因此在java SE 9.0 中不再包含这个 Jar 包。
java 9 中引入了模块的概念,默认情况下,Java SE中将不再包含java EE 的Jar包而在 java 6/7 / 8 时关于这个API 都是捆绑在一起的-->
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.0</version>
    </dependency>

Json Web Token

简称jwt,是一个开放的协议,用于通信过程中传递安全可靠的信息。是一个信息的载体,更是一个通信的安全凭证。

使用场景

在前后端分离的架构下,用户在客户端输入用户名密码登录,客户端将用户名密码发送给服务端校验,校验通过后,服务端生成该用户的登录凭证(Jwt字符串)并返回给前端,前端收到后保存该凭证,再次访问服务端时,客户端自动携带该凭证即可。

凭证必须是安全可靠的,凭证支持多种加密方式,JWT协议还支持凭证的有效期,在服务端生成时指定凭证的有效期,逾期后无效,需要用户重新使用用户名密码获取新的凭证。由于凭证是信息的载体,所以,服务端在生成凭证时可以往里面设置基本的用户信息,如用户名,权限等,当然,服务端可以解密Jwt字符串并获取到相关信息的。

组成结构

JWT整体上是一段字符串,通过 “.”连接的三个小段组成:xxxxx.yyyyy.zzzzz,分别对应Header,Payload,Signature ,参考示例:

eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjIyODM2ODAsInVzZXJuYW1lIjoidGltIiwiaWQiOiIxODgiLCJyb2xlIjoiYWRtaW4ifQ.9USUq4RTQTKTeHqsWSVSJuvlcRIhFwgUOlJrd7MUVag

第一部分,为Header(头部),按照协议,主要包括两个部分,其一为“typ”,值为JWT,表明这段字符串为JWT字符串,另一为jwt所使用的签名算法alg的名称,常用的签名算法包括摘要算法HMAC,SHA256,非对称加密算法的RSA。

//定义头部内容信息
{
    "typ":"JWT",
    "alg":"HS256"
}
//使用Base64Url 将内容进行编码:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

支持的加密算法如下:

+--------------+-------------------------------+--------------------+
   | "alg" Param  | Digital Signature or MAC      | Implementation     |
   | Value        | Algorithm                     | Requirements       |
   +--------------+-------------------------------+--------------------+
   | HS256        | HMAC using SHA-256            | Required           |
   | HS384        | HMAC using SHA-384            | Optional           |
   | HS512        | HMAC using SHA-512            | Optional           |
   | RS256        | RSASSA-PKCS1-v1_5 using       | Recommended        |
   |              | SHA-256                       |                    |
   | RS384        | RSASSA-PKCS1-v1_5 using       | Optional           |
   |              | SHA-384                       |                    |
   | RS512        | RSASSA-PKCS1-v1_5 using       | Optional           |
   |              | SHA-512                       |                    |
   | ES256        | ECDSA using P-256 and SHA-256 | Recommended+       |
   | ES384        | ECDSA using P-384 and SHA-384 | Optional           |
   | ES512        | ECDSA using P-521 and SHA-512 | Optional           |
   | PS256        | RSASSA-PSS using SHA-256 and  | Optional           |
   |              | MGF1 with SHA-256             |                    |
   | PS384        | RSASSA-PSS using SHA-384 and  | Optional           |
   |              | MGF1 with SHA-384             |                    |
   | PS512        | RSASSA-PSS using SHA-512 and  | Optional           |
   |              | MGF1 with SHA-512             |                    |
   | none         | No digital signature or MAC   | Optional           |
   |              | performed                     |                    |
   +--------------+-------------------------------+--------------------+

第二部分为Payload,包含申明信息,比如用户的主体信息和其他附加信息,这里也可以划分为三个组成部分:

  • Registered claims:协议约定的申明字段,规范保留的一些关键字,如:iss (issuer签发人), exp (expiration time过期时间), sub (subject主题), aud (audience受众),nbf(Not Before生效时间),iat(Issued At签发时间),jti(JWT ID 编号),更多可以查看:https://www.iana.org/assignments/jwt/jwt.xhtml

  • Public claims:公开申明,这些我们就可以随意定义的信息,但是应该避免使用已注册的关键字,比如iss代表的是签发人,我们就不要用来定义其他含义的信息。

  • Private claims:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明,也不是公开声明。

    ​ 这里payload的组成划分,完全是语义上的,是一种约定规范,相当于通用语。实际使用过程中,根据通信各方的需要设置具体的申明项。如果是简单场景的话,简约就好。

    palyload 在JWT对象中表现为Claims对象,而Claims对象的本质为一个Map。

示例:

//定义palyload内容
{"iss":"tim","username":"tim","id":"188","role":"admin"}

//使用 Base64Url 编码:
eyJpc3MiOiJ0aW0iLCJ1c2VybmFtZSI6InRpbSIsImlkIjoiMTg4Iiwicm9sZSI6ImFkbWluIn0

第三部分为Signature,签名部分。这部分是通过前两部分内容生成出来的,使用第一部分Header中指定的加密算法和秘钥对前两部分的Base64Url编码内容进行加密得到。

//Header中指定的alg=HS256 (HMACSHA256)
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)    
    
    //得到签名
    _95SdnCWoY9CLn2zYbuLYSnV8D0xeidwtnc124rA66U

示例组装好的token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aW0iLCJ1c2VybmFtZSI6InRpbSIsImlkIjoiMTg4Iiwicm9sZSI6ImFkbWluIn0._95SdnCWoY9CLn2zYbuLYSnV8D0xeidwtnc124rA66U

使用下述方法自己组装JWT:

/**
     * 对Header 及 Payload 原始字符串进行Base64Url编码
     * @param plainStr
     * @return
     */
    public String encode64Url(String plainStr) {
        byte[] bytes = plainStr.getBytes(StandardCharsets.UTF_8);
        String encodeStr = Base64UrlCodec.BASE64URL.encode(bytes);
        return encodeStr;
    }

 /**
     * 获得签名
     * @param header Header内容的Base64Url编码字符串
     * @param payload Payload 内容的Base64Url编码字符串
     * @return
     */
    public String getSignature(String header,String payload){
        String s=header+"."+payload;
        MacSigner macSigner = new MacSigner(SignatureAlgorithm.HS256,SECRET_KEY.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = macSigner.sign(s.getBytes(StandardCharsets.UTF_8));
        return Base64UrlCodec.BASE64URL.encode(bytes);
    }

    /**
     * 验证签名
     * @param jwtToken  JWTToken字符串
     * @return
     */
    public boolean verify(String jwtToken){
        String[] splitStrs = jwtToken.split("\\.");
        String signature = getSignature(splitStrs[0] , splitStrs[1]);
        if(signature.equals(splitStrs[2])){
            return  true;
        }else{
            return false;
        }
    }

验证签名

签名只能证明这个JWTtoken是否被篡改或是伪造的,也可以近一步验证它是否过期。所以,在payload中,我们通常不应该放敏感信息,因为base64Url编码是可逆的!

验证签名只需要将获取到的Jwt token进行“.”分割,将header和payload再次进行签名,签名结果和分割得到的签名一致,就表示合法,否则,就存在被篡改或伪造的风险。如果token合法,再解码得到payload原始内容,并取出里面的exp申明,如果该时间小于当前时间则表示已过期。

自己组装的JWT token是否成功,可以通过官网提供的在线调试器进行验证,地址:https://jwt.io/#debugger-io

注意:在右下角填上自己的秘钥。
在这里插入图片描述

工具类

public class JWTDemo {

    //加密的
    private static final String SECRET_KEY = "123456789";


    /**
     * 生成JWT
     * @param exp
     * @return
     */
    public String buildJwt(Date exp) {
        HashMap<String, Object> header=new HashMap<>();
        header.put("alg","HS256");
        header.put("typ","JWT");
        String jwtToken = Jwts.builder()
                .setHeader(header)
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)//指定HS256加密算法,本质是mac摘要算法,需要设置秘钥
                .setExpiration(exp)     //expTime是过期时间
                .claim("iss","tim.dao") //根据需要设置需要申明的内容,未加密的情况下,请勿设置敏感信息
                .claim("iat",new Date())
                .claim("username","tim")
                .claim("id","188")
                .claim("role", "admin")
                .compact();
        return jwtToken;
    }

    /**
     * 验证签名并解析出里面的内容
     * @param jwt
     * @return
     */
    public boolean verifyToken(String jwt) {
        try {
            //解析JWT字符串中的数据,并进行最基础的验证
            Claims claims = Jwts.parser()
                    .setSigningKey(SECRET_KEY)  //SECRET_KEY是加密算法对应的密钥,jjwt可以自动判断加密算法
                    .parseClaimsJws(jwt)    //jwt是JWT字符串
                    .getBody();

            System.out.println("解析得到申明内容:");
            for (Map.Entry<String, Object> entry : claims.entrySet()) {
                System.out.println(entry.getKey() + "-->" + entry.getValue());
            }
            
            return true;
        }
        //抛出SignatureException异常,说明该JWTtoken存在被篡改或伪造的风险
        //"过期时间字段"已经早于当前时间,将会抛出ExpiredJwtException异常,说明token已失效
        catch (SignatureException | ExpiredJwtException e) {
            e.printStackTrace();
            return false;
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值