JWT,我们应该咋用?

632 阅读4分钟

jwt说明

JWT(Json Web Token),遵循json格式,用于保存用户信息,存放于客户端;用户请求数据时,带上此token进行身份验证。jwt中的数据基本上是明文传输,本身不能提供网络安全保障。
JWT的结构,用符号“.”将之分为三部分,header、payload和signature。

  • header:包含jwt的配置信息,例如签名加密算法(alg),类型(默认为JWT),算法密钥信息(kid);可以写入自定义属性,格式为json;
  • payload:token的实体部分,用来保存需要传递的数据;协议中规定了几个字段,iss(签发人),exp(token过期时间),sub(主题),adu(受众),nbf(生效时间),iat(签发时间),jti(编号),可以写入自定义属性,格式为json;
  • signature:签名部分主要用来进行数据完整性校验,非必须;签名的算法比较简单,signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload) , secret )

jwt生成

java环境需要添加maven依赖

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.18.1</version>
        </dependency>

生成的token中,如果不需要签名【程序调试阶段】,使用:Algorithm.none();如果需要用HS256对称加密算法生成签名,则使用:Algorithm.HMAC256("secret-code");其中secret-code是对称加密的密码。jwt还提供RS256和ES256非对称加密算法来生成签名。

  • 注意此处的加密说明,虽然文档中提及了对称加密,但是经过多次验证,无论是2.2.0版本的【new JWTSigner(SECRET).sign(claims)】,还是当前的3.8.1版本,都可以用Base64.decode()的方式,直接解码出来。
  • 加密了,但是没有完全加密【dog】

下面代码展示了生成token的过程,其中header和payload都可以自定义属性。

    @Test
    public void testPayload(){
        Map<String, Object> headers = new HashMap<>();
        headers.put("head_user", "tom");
        headers.put("contextType", "json");
        headers.put("alg", "HS256");
        headers.put("typ", "JWT");
        // 如果用Algorithm.none(),则表示不生成签名 "alg":"none"
        // Algorithm algorithm = Algorithm.none();
        // 生成签名,"alg":"HS256",密钥为:secret-code
         Algorithm algorithm = Algorithm.HMAC256("secret-code");
        JWTCreator.Builder builder = JWT.create()
                // head数据定义 ===============================================
                // head部分的kid属性
                .withKeyId("key_233221")
                // head部分定义其他属性
                .withHeader(headers)
                // payload数据定义 ============================================
                // 签发人
                .withIssuer("clientId")
                // 主题
                .withSubject("主题")
                // 受众
                .withAudience("观众1","观众2")
                // 生效时间
                .withNotBefore(new Date(System.currentTimeMillis() + 10 * 1000))
                // 签发时间
                .withIssuedAt(new Date(System.currentTimeMillis()))
                // 过期时间
                .withExpiresAt(new Date(System.currentTimeMillis() + 100 * 1000))
                // 编号
                .withJWTId("jwt_id_111")
                // 声明自定义属性
                .withClaim("test", 23333)
                .withClaim("userId", 1003)
                // 声明自定义数组属性
                .withArrayClaim("auditor", new String[]{"张三", "李四", "Shierly"})
                ;
        String token = builder.sign(algorithm);
        // 生成的token
        System.out.println(token);
    }

上面这段代码生成的token:

eyJraWQiOiJrZXlfMjMzMjIxIiwiY29udGV4dFR5cGUiOiJqc29uIiwidHlwIjoiSldUIiwiaGVhZF91c2VyIjoidG9tIiwiYWxnIjoiSFMyNTYifQ.eyJzdWIiOiLkuLvpopgiLCJhdWQiOlsi6KeC5LyXMSIsIuinguS8lzIiXSwibmJmIjoxNTk3ODIwNDA5LCJ0ZXN0IjoyMzMzMywiaXNzIjoiY2xpZW50SWQiLCJhdWRpdG9yIjpbIuW8oOS4iSIsIuadjuWbmyIsIlNoaWVybHkiXSwiZXhwIjoxNTk3ODIwNDk5LCJpYXQiOjE1OTc4MjAzOTksInVzZXJJZCI6MTAwMywianRpIjoiand0X2lkXzExMSJ9.9KH6EoPqSC_g9LX9JM-tbjNKlBFjvZaaWKocOykv6HM

前文有交代,jwt是明文传输的,这里用代码解析一下刚刚生成的token:

    @Test
    public void testRead() {
        // header部分
        byte[] bytes = Base64Utils.decodeFromUrlSafeString("eyJraWQiOiJrZXlfMjMzMjIxIiwiY29udGV4dFR5cGUiOiJqc29uIiwidHlwIjoiSldUIiwiaGVhZF91c2VyIjoidG9tIiwiYWxnIjoiSFMyNTYifQ");
        System.out.println(new String(bytes));
        // payload部分
        bytes = Base64Utils.decodeFromUrlSafeString("eyJzdWIiOiLkuLvpopgiLCJhdWQiOlsi6KeC5LyXMSIsIuinguS8lzIiXSwibmJmIjoxNTk3ODE4NzgxLCJ0ZXN0IjoyMzMzMywiaXNzIjoiY2xpZW50SWQiLCJhdWRpdG9yIjpbIuW8oOS4iSIsIuadjuWbmyIsIlNoaWVybHkiXSwiZXhwIjoxNTk3ODE4ODcxLCJpYXQiOjE1OTc4MTg3NzEsInVzZXJJZCI6MTAwMywianRpIjoiand0X2lkXzExMSJ9");
        System.out.println(new String(bytes));
    }

输出信息

{"kid":"key_233221","contextType":"json","typ":"JWT","head_user":"tom","alg":"HS256"}
{"sub":"主题","aud":["观众1","观众2"],"nbf":1597818781,"test":23333,"iss":"clientId","auditor":["张三","李四","Shierly"],"exp":1597818871,"iat":1597818771,"userId":1003,"jti":"jwt_id_111"}

jwt校验

这里主要校验token的完整性,防止数据被篡改。
校验的代码

    @Test
    public void testVerifySecret(){
        String token2 = "eyJraWQiOiJraWQtMSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJpc3MiOiJjbGllbnRJZCIsInVzZXJJZCI6MTAwMywidXNlcm5hbWUiOiLlvKDkuIkifQ.GuesgTUyP6InsY6dLIq1Kc8zsb8WCLxGw2zgWL5i2Hc";
        // 修改密码,The Token's Signature resulted invalid when verified using the Algorithm: HmacSHA256
        Algorithm algorithm = Algorithm.HMAC256("secret-code");
        // 这里列举需要校验的属性,如果没有列出,则不需要比较
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer("clientId")
                .withClaim("userId", 1003)
                .build();
        DecodedJWT jwt = verifier.verify(token2);
        System.out.println(new String(Base64Utils.decodeFromString(jwt.getHeader())));
        System.out.println(new String(Base64Utils.decodeFromString(jwt.getPayload())));
    }

jwt校验源代码


    /**
     * jwt校验源码 
     * @param jwt token的解码对象,DecodedJWT jwt = new JWTDecoder(parser, token)
     * @return DecodedJWT 返回方法的参数
     */
    public DecodedJWT verify(DecodedJWT jwt) throws JWTVerificationException {
        verifyAlgorithm(jwt, algorithm);
        algorithm.verify(jwt);
        verifyClaims(jwt, claims);
        return jwt;
    }

这里简单说一下校验过程

  1. 校验签名算法是否一致,verifyAlgorithm(jwt, algorithm);
  2. 用服务端的密钥重新计算签名,和token参数中签名进行比较,如果不一致,校验失败;algorithm.verify(jwt);如果服务端的密钥泄露,则这一步的校验则无任何意义。
  3. 比较JWTVerifier对象中列举的属性,如果属性值不一致,则校验失败;

补充说明:
第二步:如果密钥泄露,那么还有第三步勉强挽救一下,比如校验通过了第二步,在第三步时,userId和服务端数据库中保存的不一致,那么也会校验失败。
第三步:这里不会比较header中的属性,也不会比较JWTVerifier对象中没有列举的属性;比如我们的token中有username属性,但是JWTVerifier只列举了Issuer和userId【上述的例子】,则username属性不参与校验。