编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

JWE:安全传输敏感数据的最佳实践 (中)

wxchong 2024-08-02 08:55:57 开源技术 12 ℃ 0 评论

接着上一话 JWE:安全传输敏感数据的最佳实践 (上) ,我们已经实现了接收密文,并进行了解密,但是这只是单方向的,如果我们想返回加密数据给客户端,应该怎么做???这时就需要用客户端的公钥加密数据,并返回了。但是我们服务器如果支持多个客户端,我们又怎样去根据客户端的不同,读取对应的公钥加密数据返回???接下来我们一步步去解决。(PS:接下来可能会接触到一些源码部分,可能比较晦涩难懂,我们不需要搞懂每一行代码,我们要站上帝的视角,去找出我们想要的东西,并加以利用)

就像这样,我们首先将客户端传过来的数据进行解密,然后自己手动赋值到对应的参数上,每次都写这样的加解密过程,未免有些不妥,我们想做到如图上的效果,自动赋值到controller的参数,我们直接请求一下,看结果怎样?

好吧,spring mvc根本不认识,你说它是json格式,它就用json的格式去解析,但根本解析不了

我们看下后端的控制台的报错信息,有一个警告。我们试着去打开这个类,并发现最终的报错信息是在这里,

试着在这里打一个断点,从调用堆栈中,寻找一下有没有什么突破点,我们分析下,在processDispatchResult之后的方法,都在怎么包装返回错误信息,我们就需要debug进processDispatchResult方法,在报错之前看,能不能找到什么有用信息

看这里,如果异常不为空,就直接走进异常的处理流程,我们就要返回去,看processDispatchResult之前的doDispatch方法

我们从这个方法一直跟进去,直到这个方法,从函数名称,我们大概可以猜出,这是将我们request body转换成对应controller上的参数

我们再往下走,来到这个循环里,可以看到,spring mvc默认有多个转换器

然后因为我们的content-type传的是application/json,MappingJackson2HttpMessageConverter 这个转换器就尝试解释我们的request body,很遗憾,转换失败

到这里,我们就有思路了,既然是因为没有合适的转换器去自动适配我们的数据,我们就应该造一个出来去自动适配我们的数据格式。

好了,下面就是代码的环节,把主要代码都贴出来,篇幅有限。

这个是jwe的加解密器,负责jwe的加解密

package cn.sakka.jwe.application.mvc;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSADecrypter;
import com.nimbusds.jose.crypto.RSAEncrypter;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
/**
* @author sakka
* @version 1.0
* @description: jwe加解密器
* @date 2023/3/30
*/
public class JweRSAEncryptionDecryption {
        public static String encrypt(byte[] payload, PublicKey publicKey) throws Exception {
                // 创建加密器
                JWEHeader header = new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM).build();
                JWEEncrypter jweEncrypter = new RSAEncrypter((RSAPublicKey) publicKey);
                // 加密JSON数据
                Payload jwePayload = new Payload(payload);
                JWEObject jweObject = new JWEObject(header, jwePayload);
                jweObject.encrypt(jweEncrypter);
                // 将JWE对象转换为JWE字符串
                return jweObject.serialize();
        }
        public static byte[] decrypt(String jwe, PrivateKey privateKey) throws Exception {
                // 创建解密器
                JWEDecrypter jweDecrypter = new RSADecrypter(privateKey);
                // 解密JWE字符串
                JWEObject jweObject = JWEObject.parse(jwe);
                jweObject.decrypt(jweDecrypter);
                // 将解密后的JSON数据转换为JSONObject对象
                Payload payload = jweObject.getPayload();
                return payload.toBytes();
        }
}

重点在JweMappingJackson2HttpMessageConverter这个类 canRead判断是否对body进行转换,read 读取body内容并进行转换,我们无需完全重新实现对json的解析转换,我们只需把密文进行解密,并传回父类AbstractJackson2HttpMessageConverter去解析就好了。也没有太多复杂的逻辑。

package cn.sakka.jwe.application.mvc;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.sakka.jwe.application.config.JweConfig;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* @author sakka
* @version 1.0
* @description: JweMappingJackson2HttpMessageConverter
* @date 2023/3/30
*/
public class JweMappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    /**
    * 配置文件
    */
    private final JweConfig.JweProperties jweProperties;
    /**
    * 服务器私钥
    */
    private PrivateKey serverPrivateKey;
    /**
    * 客户端公钥
    */
    private final Map<String, PublicKey> clientPublicKeys;

    public JweMappingJackson2HttpMessageConverter(JweConfig.JweProperties jweProperties) {
            this(jweProperties, Jackson2ObjectMapperBuilder.json().build());
    }

    public JweMappingJackson2HttpMessageConverter(JweConfig.JweProperties jweProperties, ObjectMapper objectMapper) {
            super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
            this.jweProperties = jweProperties;
            clientPublicKeys = new HashMap<>();
            init();
    }

    private void init() {
            try {
                    PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(jweProperties.getServerPrivateKey()));
                    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
                    serverPrivateKey = keyFactory.generatePrivate(keySpec);
                    for (JweConfig.ClientPublicKey clientPublicKey : jweProperties.getClientPublicKeys()) {
                    String clientId = clientPublicKey.getClientId();
                    PublicKey publicKey = getPublicKey(clientPublicKey.getClientKey());
                    clientPublicKeys.put(clientId, publicKey);
            }
            } catch (Exception e) {
                    throw new JweException(e);
            }
    }
    /**
    * String转公钥PublicKey
    *
    * @param key base64
    * @return 公钥
    * @throws Exception 异常
    */
    protected PublicKey getPublicKey(String key) throws Exception {
            byte[] keyBytes = Base64.decode(key);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            return keyFactory.generatePublic(keySpec);
    }

    @Override
    public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {
    				return super.canRead(type, contextClass, mediaType) && isJweEntity(type, contextClass);
    }

    protected boolean isJweEntity(Type type, Class<?> contextClass) {
        JavaType javaType = getJavaType(type, contextClass);
        return javaType.getRawClass().getDeclaredAnnotation(JweEntity.class) != null;
    }

    @Override
    public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
    		HttpHeaders headers = inputMessage.getHeaders();
        MediaType contentType = headers.getContentType();
        Charset charset = getCharset(contentType);
        InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody());
        String jwe = IoUtil.read(inputStream, charset);
        if (CharSequenceUtil.isEmpty(jwe)) {
        		throw new HttpMessageNotReadableException("I/O error while reading input message", inputMessage);
        }
        try {
        		byte[] decrypt = JweRSAEncryptionDecryption.decrypt(jwe, serverPrivateKey);
        		return super.read(type, contextClass, new HttpInputMessage() {
                              @Override
                              public InputStream getBody() {
                                  return new ByteArrayInputStream(decrypt);
                              }
                              @Override
                              public HttpHeaders getHeaders() {
                              		return headers;
                              }
                 });
   			} catch (Exception e) {
    				throw new JweException(e);
   		 }
    }
  }

这个注解主要是为了标记类是否需要进行解密,是配合上面的JweMappingJackson2HttpMessageConverter

使用。

package cn.sakka.jwe.application.mvc;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author sakka
* @version 1.0
* @description: //TODO
* @date 2023/3/30
*/
@Target({TYPE})
@Retention(RUNTIME)
public @interface JweEntity {
}

这个配置主要是把我们配置文件里的公钥私钥都加载进来,并初始化JweMappingJackson2HttpMessageConverter

package cn.sakka.jwe.application.config;
import cn.sakka.jwe.application.mvc.JweMappingJackson2HttpMessageConverter;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* @author sakka
* @version 1.0
* @description: //TODO
* @date 2023/3/30
*/
@Configuration
public class JweConfig {
        @Bean
        public JweMappingJackson2HttpMessageConverter jweMappingJackson2HttpMessageConverter(@Autowired JweProperties jweProperties) {
       				 return new JweMappingJackson2HttpMessageConverter(jweProperties);
        }
        @ConfigurationProperties(prefix = "jwe")
        @Bean
        public JweProperties jweProperties() {
        				return new JweProperties();
        }
        @Data
        public static class JweProperties {
                private String serverPrivateKey;
                private List<ClientPublicKey> clientPublicKeys;
        }
        @Data
        public static class ClientPublicKey {
               private String clientId;
               private String clientKey;
        }
}
jwe: 
	server-privateKey: MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCyoFNg6oFTmt2l1Ff3+jDGLScezGzIav/w9ZBnwa09CKVd825AQVY7TWjRlTwCCf7DNq2kYI7AiBJoQXinq4ji3KELwGWeRBk38ZyNuvKWPMitGW7w0qwu7jRJA7cJhr6nsMDfql1f+cxD1Bx5J4Zb17sTlha6IyFQOCpZUR2ZHFf5vF+g1ikG02W/LF4vQwoQhRWWKRgp75TEqGI9FzCMEiIcp+xjY+zM/iHjzlSNwha97ImMG7ycSlSAdSpQd0DWebfZaZQxw6FNTjW1fpmhgrqpQF41fjGd84buOcTvpXPZByxVOedOpND/3efEQitmxJ9D+93OLwuvJjbFLQSZAgMBAAECggEAJHg4XcazPeMWEufuR/pkX+nTHWYmZar283bnk0+HM7lirfJoFaVhWj09Q+EgvdfVlHzC6hcuvh9qBrArVqxeh9b86H3RIYWM0o+5Y3SCV+s0G6dgL7oLno9SzH9+LOs+XNVpI6FQbCp/qm+RmqjXtUOv9dlEbZ+DizHUb6TwkpRPMo3BHPUKGpajmP0pgSkQz8x4MWJ0a6SVST+/E7ZbwF8PobzOQCxND2yZQah/TY1EvCCyOM6chGm0loCyC4HGTBmDm/0LcWRqgiI8GiglEijGu2Iha6UR11JaEStHoc1Sy38Orj377bhndm2l19HNOQIslp9m0VP4WUSG3GJDKwKBgQC55+HY29F0H2jy7p94+snPCKVG0uJHu0IKhFN09fChP/1q0PLJuz3Vj07YUjGneSL3RHM1D+HbMYAGnZ+7GFmwIi8FLKBVZDx4L7ENhcPMUM2q0rsBphZuHfD14Hf9I0xUiNrpyTLuVPJfK3kzx2NYQmRGo6tY+INDuI5L2HEELwKBgQD1+c4ilEQtTRvoIa+BfZ/oOBeFaYsQGlLL+8yVbsPsfH+0MMoiIY++my5rDhT//8QXNhnOI1cs7CFKELA/99Mk7y9G/4o+cMb1kAyZJU6zNoSe9Eitdyo0qQQ20NVAW+YWX0zAuAm5bttk1RvVGz/wiuOotVw6oCSR0uUYUWCptwKBgB2psy6f/G6z6FIC4y0xjuva7Ew9r99UMLhu3sYly+xewne9uU+Y8cfWovT/QG8BdCPSJzPLQfVwk4X6tpbqzry855XCxh557PAcY/rNYi2Cox5jm3Uq5B9T5bPFyj9412ARqiRtdxPyN+4ZiLBLWz2k8k0XJmr+1CsFEqdldLr/AoGAEjyqLuAlSeKMriJJO+WPhI0cGVUg7Vm2R89sdKvYtODqKvbvFaa9XJlu0JsjrXNOG5Z0RVdTcE41jaM9HhEGw5dEPxRVMJn19mDuvjAI7LqfDJX6CXprU6owWMwU84ecwI3iR+udNPVmKMywGpXBoNj7VhfUNbiH3ZPwTmRCMXMCgYBi5I1KJ7kyaHKTilJEAhiYv6XBwsJScJkdAXWuA/SdG3aWQAEc4SOrRwqmqbHWYOm827tb20kG09rHnVS+tVSShvCkv4OcAr2X0L04IX9OfvUqI5pPWiQd/VzCQrTPcelixgkUkG4Sc2dRr6gvvFOKFAAVTQrePh9clk8kd3bg+g==
	client-publicKeys: 
		- client-id: test
  		client-key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsqBTYOqBU5rdpdRX9/owxi0nHsxsyGr/8PWQZ8GtPQilXfNuQEFWO01o0ZU8Agn+wzatpGCOwIgSaEF4p6uI4tyhC8BlnkQZN/GcjbryljzIrRlu8NKsLu40SQO3CYa+p7DA36pdX/nMQ9QceSeGW9e7E5YWuiMhUDgqWVEdmRxX+bxfoNYpBtNlvyxeL0MKEIUVlikYKe+UxKhiPRcwjBIiHKfsY2PszP4h485UjcIWveyJjBu8nEpUgHUqUHdA1nm32WmUMcOhTU41tX6ZoYK6qUBeNX4xnfOG7jnE76Vz2QcsVTnnTqTQ/93nxEIrZsSfQ/vdzi8LryY2xS0EmQIDAQAB

好了,接下来就是测试环节,是骡子是马就是这一刻!!!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表