JWT生成的token——中间部分Payload的坑

发布时间 2023-11-23 10:34:41作者: 沧海一滴

 

JWT进行token认证应该都用过,标准的加密(Base64 编码)后的token是这样的三段式的:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWQiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjA1MDAxNzQyLCJpYXQiOjE2MDQ5OTQ1NDIsImp0aSI6IjU5YjI2NDEzLTE4MjMtNDVlZS1iZTI1LTA5M2ZjMjlhMmYzOCJ9.FMpVjuTUSOY5sbYqbJslJCnIvExfhPciSHXFZ9B8nH0
他是一个三段式的,中间使用两个.(点)隔开的。

JWT包含了三部分:
Header 头部
Payload 负载
Signature 签名/签证

这里不在详细介绍了,主要说一下JWT组成部分中间的这一段(Payload 负载)自己遇到的坑:

这是上面列举的token中间一段,由于是base64编码可以直接解密出来的,

在进行渗透测试的时候发现敏感信息暴露,所以需要对信息进行加密。自己的思路就是在生成token后,进行二次加密,如下图:
AES加密后Token: VnkrQUODp5CiSeiX4jMIqE6i6CT29r13fk8k7D39UIMmc3VN6KqM8tfgtkH4si8mHf07Uc2R6Lw2JljJnfoBsmCnEKPwumbxvMaoxdz3cXvLYs4CD2zNHKceguK1ktLCvP4KQrdSrUZRP1HGfJnAdMDYJY748Q7n0LPA+KHtEIn0nYHrZj5x9VRzHHfU2otDwm5yzMV1wjnm2wzKJExYXjyT4zY07vYPtkyCtw6W3KtgeIEYWZyyPfzxvGMGaGJJzPeE563TkpUldF8QAnCXiQ==

这样就没法直接解密出来了。

在解密的时候,先对生成的AES加密信息先解密一次,转成jwt标准的三段式:

接下来就说到坑了:由于我在token生成时经过AES加密后,HttpServletRequest中此时不是JWT三段式token,这就导致后面接口使用JWT.decode方法获取token信息的时候报错:

没办法影响到全局功能只能改了,后面查看资料参考这篇资料启发

https://blog.csdn.net/u010698072/article/details/79973830

在token解密验签成功后,我加个反射将解密后的JWT格式token通过反射,修改token值加入到HttpServletRequest中去:

 

 

这样就就把之前AES解密后的JWT格式的token塞入到httpServletRequest中去了,后面接口获取token信息就正常了。

最后贴上相关的完整代码:

JwUtil:

package token;

import com.shop.result.CommonResult;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.http.MimeHeaders;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public class JwtUtil {

    public static final String TOKEN_SECTURE_KEY = "XXXXX";

    /**
     * 自定义密钥
     */
    private static final String ASSETS_SECRET_KEY = "XXXXX";

    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法  私匙使用用户密码
     *
     * @param ttlMillis jwt过期时间
     * @return
     */
    public static String createJWT(long ttlMillis, Map<String, Object> paramMap) {
        //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("id", paramMap.get("id").toString());

        //生成签发人
        String subject = paramMap.get("id").toString();

        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重复攻击。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, TOKEN_SECTURE_KEY);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }
        //这里为了避免中间一段base64被解析出来,这里进行aes二次加密
        String token = builder.compact();
        //返回二次加密后的token
        return AesUtil.encrypt(token, ASSETS_SECRET_KEY);
    }


    /**
     * 校验token
     * 
     *
     * @param token
     * @return
     */
    public static CommonResult isVerify(HttpServletRequest httpServletRequest, String token) {
        try {
            //先进行二次加密解密获取原来token,后面继续解密
            String aesToken = AesUtil.decrypt(token, ASSETS_SECRET_KEY);

            //根据二次解密token进行后续操作
            //指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
            //得到DefaultJwtParser
            Claims claims = Jwts.parser()
                    //设置签名的秘钥
                    .setSigningKey(TOKEN_SECTURE_KEY)
                    //设置需要解析的jwt
                    .parseClaimsJws(aesToken).getBody();

            //在验签成功后,使用反射,将解密后的token设置到request中的的header中去
            reflectSetTokenValue(httpServletRequest,"token",aesToken);

            return CommonResult.success(true);
        } catch (SignatureException | MalformedJwtException e) {
            return CommonResult.failedToken();
        } catch (ExpiredJwtException e) {
            return CommonResult.failedToken();
        }

    }

   /**
     * 修改header信息,将修改后的token的key-value键值对儿加入到header中
     * @param request
     * @param key token名称
     * @param value 解密后的JWT格式token
     */
    private static void reflectSetTokenValue(HttpServletRequest request,String key,String value){
        Class<? extends HttpServletRequest> requestClass = request.getClass();
        try {
            Field request1 = requestClass.getDeclaredField("request");
            request1.setAccessible(true);
            Object o = request1.get(request);
            Field coyoteRequest = o.getClass().getDeclaredField("coyoteRequest");
            coyoteRequest.setAccessible(true);
            Object o1 = coyoteRequest.get(o);
            Field headers = o1.getClass().getDeclaredField("headers");
            headers.setAccessible(true);
            MimeHeaders o2 = (MimeHeaders)headers.get(o1);
            //将AES格式的token替换成JWT格式token
            o2.getValue(key).setString(value);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

 

https://blog.csdn.net/weixin_29634843/article/details/109668875