spring cloud实战与思考(五) JWT之携带敏感信息

需求:

需要将一些敏感信息保存在JWT中,以便提高业务处理效率。

 

  众所周知JWT协议RFC7519使用Base64UrlHeaderPayloadJson字符串进行编解码。A JWT is represented as a sequence of URL-safe parts separated by period ('.') characters. Each part contains a base64url-encoded value.

  不幸地的是Base64Url编码方式不是加密手段。任何得到JWT的人都可以使用公开的解码方式将JWT的原文解析出来。所以很多介绍JWT的文章都提醒不要在JWT中携带敏感信息。但是在特定业务场景下,如果JWT中含有某些关键信息,就可以节省后台很多额外操作,例如数据库查询、服务接口访问等。进而缩短后台响应时间,改善用户体验。

  既然在JWT中携带敏感信息能带来这么大的好处,那么花点精力实现这个功能看起来是值得的。提到敏感信息的保密,自然会想到加密和解密。将敏感信息的密文放入JWT中,即使JWT泄露,由于没有密钥,获得JWT的人也无法对其进行解密。而服务器端只要增加一个解密过程就能提取出敏感信息,提高后续业务处理效率。

  加密算法主要分两大类:对称加密和非对称加密。因为JWT是由服务端创建,客户端转手后又发回服务端使用。所以加密和解密都发生在服务端,不涉及到密钥的分发,相较其他加密场景要简单很多。所以我选择了加解密运算速度快的对称加密算法AES作为敏感信息的加密方式。

  项目中使用JJWT java库创建和校验JWT,将AES加解密过程放入对JJWT封装的接口中。复用创建JWT的数字签名密钥作为AES的加密密钥和初始向量。加解密过程对调用者是透明的。

  Maven依赖:

 

        <!--Java JWT 依赖库-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency> 

 

  JJWT封装接口:

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtil {
    /**
     * 生成JWT签名字符串
     *
     * @param publicClaims      无需加密保持明文方式的JWT claims
     * @param privateClaims:   需要加密的JWT claims
     * @param ttlMillis:          JWT过期时长(毫秒)
     * @param key:             JWT HS256签名的密钥,也是AES加密的密钥
     *
     * @return JWT字符串
     *
     */
    public static String createJWT(Map<String, Object> publicClaims, Map<String, Object> privateClaims,long ttlMillis, String key) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();

        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(key);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        if(null != privateClaims && !privateClaims.isEmpty()) {
            String jsonStr = JsonUtil.map2JsonStr(privateClaims);
            //使用同一个密钥对私有声明进行加密
            String encrypteClaims = AesEncryptUtil.encrypt(jsonStr, key, key);
            if(null != encrypteClaims) {
                if(null == publicClaims) {
                    publicClaims = new HashMap<>();
                }
                publicClaims.put("privateClaims", encrypteClaims);
            }
        }

        JwtBuilder builder = Jwts.builder().signWith(signatureAlgorithm, signingKey);
        if(null != publicClaims && publicClaims.size() > 0) {
            builder.setClaims(publicClaims);
        }

        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }

        return builder.compact();
    }

    /**
     * 解析并校验JWT, 校验过程是JJWT内部实现的,会校验JWT是否过期,是否被篡改。
     *
     * @param jwt     JWT字符串
     * @param key:   JWT HS256签名的密钥,也是AES加密的密钥
     *
     * @return JWT claims的Map对象
     *
     */
    public static Map<String, Object> parseJWT(String jwt, String key){

        Map<String, Object> privateMap = null;

        //parser函数会在参数缺失、校验失败、token过期等情况下抛出runtime异常,所以调用者需要捕获该runtime异常
        Map<String, Object> originalMap = Jwts.parser()
                .setSigningKey(DatatypeConverter.parseBase64Binary(key))
                .parseClaimsJws(jwt).getBody();

        //解析私有声明
        if(null != originalMap && originalMap.containsKey("privateClaims")) {
            String encryptedStr = (String)originalMap.get("privateClaims");
            if(null != encryptedStr && !encryptedStr.isEmpty()) {
                String decryptedStr = AesEncryptUtil.decrypt(encryptedStr, key, key);
                if(null != decryptedStr && !decryptedStr.isEmpty()) {
                    privateMap = JsonUtil.jsonStr2Map(decryptedStr);
                }
            }

            originalMap.remove("privateClaims");
            if(null != privateMap && privateMap.size() > 0) {
                originalMap.putAll(privateMap);
            }
        }

        return originalMap;
    }
}

  AES加密和解密接口:

 

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;

public class AesEncryptUtil {
    private static final String encode = "UTF-8";
    private static final String mode = "AES/CBC/PKCS5Padding";

    /**
     * JDK只支持AES-128加密,也就是密钥长度必须是128bit;参数为密钥key,key的长度小于16字符时用"0"补充,key长度大于16字符时截取前16位
     **/
    private static SecretKeySpec get128BitsKey(String key) {
        if (key == null) {
            key = "";
        }
        byte[] data = null;
        StringBuffer buffer = new StringBuffer(16);
        buffer.append(key);
        //小于16后面补0
        while (buffer.length() < 16) {
            buffer.append("0");
        }
        //大于16,截取前16个字符
        if (buffer.length() > 16) {
            buffer.setLength(16);
        }
        try {
            data = buffer.toString().getBytes(encode);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new SecretKeySpec(data, "AES");
    }

    /**
     * 创建128位的偏移量,iv的长度小于16时后面补0,大于16,截取前16个字符;
     *
     * @param iv
     * @return
     */
    private static IvParameterSpec get128BitsIV(String iv) {
        if (iv == null) {
            iv = "";
        }
        byte[] data = null;
        StringBuffer buffer = new StringBuffer(16);
        buffer.append(iv);
        while (buffer.length() < 16) {
            buffer.append("0");
        }
        if (buffer.length() > 16) {
            buffer.setLength(16);
        }
        try {
            data = buffer.toString().getBytes(encode);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return new IvParameterSpec(data);
    }

    /**
     * 填充方式为Pkcs5Padding的加密函数
     * 填充方式为Pkcs5Padding时,最后一个块需要填充χ个字节,填充的值就是χ,也就是填充内容由JDK确定
     *
     * @param srcContent:  明文
     * @param password:   加密密钥(不足128bits时,填"0"补足)
     * @param iv:           初始向量(不足128bits时,填"0"补足)
     *
     * @return 密文(16进制表示)
     *
     */
    public static String encrypt(String srcContent, String password, String iv) {
        SecretKeySpec key = get128BitsKey(password);
        IvParameterSpec ivParameterSpec = get128BitsIV(iv);
        try {
            Cipher cipher = Cipher.getInstance(mode);
            cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
            byte[] byteContent = srcContent.getBytes(encode);
            byte[] encryptedContent = cipher.doFinal(byteContent);
            String result = HexUtil.byte2HexStr(encryptedContent);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 填充方式为Pkcs5Padding的解密函数
     * 填充方式为Pkcs5Padding时,最后一个块需要填充χ个字节,填充的值就是χ,也就是填充内容由JDK确定
     *
     * @param encryptedContent:  密文
     * @param password:          加密密钥(不足128bits时,填"0"补足)
     * @param iv:                 初始向量(不足128bits时,填"0"补足)
     *
     * @return 密文(16进制表示)
     *
     */
    public static String decrypt(String encryptedContent, String password, String iv) {
        SecretKeySpec key = get128BitsKey(password);
        IvParameterSpec ivParameterSpec = get128BitsIV(iv);
        try {
            byte[] content = HexUtil.hexStr2Byte(encryptedContent);
            Cipher cipher = Cipher.getInstance(mode);
            cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
            byte[] decryptedContent = cipher.doFinal(content);
            String result = new String(decryptedContent);
            return result;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

 

    WTUtil的创建接口“createJWT()”允许调用者指定那些JWT的声明需要加密,那些声明保持为明文的状态。例如JWT的过期时间“exp”,客户端可能需要这个值进行Token有效性判断,所以该claim就需要保持为明文。在调用者解析JWT的时候,JWTUtil的解析接口会将公开明文的publicClaims和解密后的privateClaims放到一个Map中,所以publicClaimsprivateClaims Mapkey值不要有重名的现象。相信实际的应用场景也不需要重名的Claims

 

    将以下3claims生成JWT

 

{
  "groupId": "6fd5a193016d",
  "userName": "test123",
  "exp": 1528771799
}

 

  未使用AES加密的JWT字符串:

 

eyJhbGciOiJIUzI1NiJ9.eyJncm91cElkIjoiNmZkNWExOTMwMTZkIiwidXNlck5hbWUiOiJ0ZXN0MTIzIiwiZXhwIjoxNTI4NzcxNzk5fQ.vJQ4CeYKx3n6B709w35Xdv4fVB2YTr-tsAWr3tCe6A8

 

  使用https://jwt.io/#debugger解析后的原文:

  将groupId”和“userName”作为敏感信息使用AES加密后的JWT字符串:

eyJhbGciOiJIUzI1NiJ9.eyJwcml2YXRlQ2xhaW1zIjoiMUYyOTMxNjM2NzlBMDM2NDI5RkE3NzMwRTc2OUQyQUY0NjdEMkM3M0Y1NDQxNEExMTVCQUI4MzdCQTEwODQ2NjU0QjA2MTE0OTEzQkJGOUNDMkRCQjdFQzM0RTc2NjIwIiwiZXhwIjoxNTI4Nzc5NjAzfQ.5xb_uxBHMAPvShsOC-pQIS746OjW5XMjj5tAcxwFCq8

  使用https://jwt.io/#debugger解析后的JWT原文:

 

  使用JWTUtil.parseJWT()”接口解析后的JWT原文Map对象:

 

{groupId=6fd5a193016d, userName=test123, exp=1528771799}

 

  可以看到AES加密后,“groupId”和“userName”变成了JWT中“privateClaims”对应的密文。这样敏感信息就不怕泄露了。这个方法的缺点主要就是JWT字符串的长度从151增加到了243。如果JWT长度增加的太多,JJWT的接口还可以使用压缩算法对JWT字符串进行压缩。

 

转载于:https://www.cnblogs.com/standup/p/9188432.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Cloud是一个基于Spring Boot的开发工具,用于快速构建分布式系统的微服务框架。它提供了一系列的组件,包括服务注册与发现、配置中心、负载均衡、断路器、网关等,使得开发者可以更加方便地构建和管理微服务。 Spring Security是Spring框架中的一个安全框架,用于提供身份认证和授权功能。它可以与Spring Cloud集成,为微服务提供安全保障。 JWT(JSON Web Token)是一种轻量级的身份认证和授权机制,它使用JSON格式来传递信息。在Spring Cloud中,可以使用JWT来实现微服务之间的安全通信,保证数据的安全性和完整性。 ### 回答2: Spring Cloud是一个开源的分布式系统开发框架,它基于Spring Boot,能够帮助开发者快速构建云原生应用程序。Spring Cloud提供了一系列的组件,例如服务注册与发现(Eureka)、服务网关(Zuul)、配置中心(Config)等,可以协助开发者实现微服务架构。 Spring Security是Spring框架提供的一种安全框架,它能够为应用程序提供认证和授权的功能。Spring Security基于过滤器链的机制,可以对请求进行安全验证。开发者可以通过配置来定义访问控制规则,保护应用程序的资源。 JWT(JSON Web Token)是一种用于身份验证和访问控制的标准方法,它通过在身份验证完成后生成一个令牌,并将该令牌发送给客户端,客户端在后续的请求中通过该令牌进行身份验证。JWT由三部分组成,分别是头部、载荷和签名。头部和载荷使用Base64进行编码,然后使用一个密钥进行签名,确保JWT的安全性。 在使用Spring CloudSpring Security构建分布式系统时,可以使用JWT作为认证和授权的方式。具体做法是,当用户进行身份验证成功后,生成一个JWT令牌,并将该令牌返回给客户端。客户端在后续的请求中,将令牌作为Authorization头部的内容发送给服务端。服务端接收到请求后,解析JWT令牌,验证其合法性,并根据令牌中的信息来判断用户的身份和权限。通过这种方式,可以实现无状态的分布式身份验证和访问控制。 总结来说,Spring Cloud可以帮助开发者构建分布式系统,Spring Security可以提供身份验证和授权的功能,而JWT可以作为一种安全的认证和授权方式在分布式系统中使用。这三者相互结合,可以帮助开发者构建安全、可靠的分布式应用程序。 ### 回答3: Spring Cloud是一个基于Spring Boot的开发工具集,它提供了一系列的分布式系统开发工具,其中包括了分布式配置中心、服务注册与发现、消息总线、负载均衡、熔断器、数据流处理等。Spring Cloud的目标是帮助开发者快速构建适应于云环境的分布式系统。 Spring Security是Spring官方提供的安全框架,它可以用于保护Spring应用程序免受各种攻击,例如身份验证、授权、防止跨站点请求伪造等。Spring Security使用一种基于过滤器链的方式来处理HTTP请求,通过配置一系列的过滤器,可以实现对请求的鉴权和授权处理。 JWT(JSON Web Token)是一种用于跨域身份验证的开放标准。它可以在用户和服务器之间传输信息,并且能够对信息进行校验和解析。JWT一般由三部分组成:头部、载荷和签名。头部包含了令牌的类型和加密算法,载荷包含了需要传输的信息,签名用于验证令牌的合法性。 在使用Spring Cloud时,可以结合Spring Security和JWT来进行身份验证和授权。我们可以通过配置Spring Security的过滤器链来验证JWT的有效性,并在每个请求中进行用户身份的鉴权。通过使用JWT,我们可以避免传统的基于Session的身份验证方式,实现无状态的分布式身份验证。 总结起来,Spring Cloud是一个分布式系统开发工具集,Spring Security是一个安全框架,JWT是一种用于跨域身份验证的开放标准。在使用Spring Cloud进行分布式系统开发时,可以结合Spring Security和JWT来实现身份验证和授权的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值