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

1,003 阅读5分钟

接着上一话 JWE:安全传输敏感数据的最佳实践 (上) ,我们已经实现了接收密文,并进行了解密,但是这只是单方向的,如果我们想返回加密数据给客户端,应该怎么做???这时就需要用客户端的公钥加密数据,并返回了。但是我们服务器如果支持多个客户端,我们又怎样去根据客户端的不同,读取对应的公钥加密数据返回???接下来我们一步步去解决。(PS:接下来可能会接触到一些源码部分,可能比较晦涩难懂,我们不需要搞懂每一行代码,我们要站上帝的视角,去找出我们想要的东西,并加以利用) a6a681ebgy1gph28kxgc8j205i03zmwz.jpg image.png 就像这样,我们首先将客户端传过来的数据进行解密,然后自己手动赋值到对应的参数上,每次都写这样的加解密过程,未免有些不妥,我们想做到如图上的效果,自动赋值到controller的参数,我们直接请求一下,看结果怎样? image.png 好吧,spring mvc根本不认识,你说它是json格式,它就用json的格式去解析,但根本解析不了 a6a681ebgy1gp132feyooj20c80c8ab1.jpg 我们看下后端的控制台的报错信息,有一个警告。我们试着去打开这个类,并发现最终的报错信息是在这里, image.png image.png 试着在这里打一个断点,从调用堆栈中,寻找一下有没有什么突破点,我们分析下,在processDispatchResult之后的方法,都在怎么包装返回错误信息,我们就需要debug进processDispatchResult方法,在报错之前看,能不能找到什么有用信息 image.png image.png 看这里,如果异常不为空,就直接走进异常的处理流程,我们就要返回去,看processDispatchResult之前的doDispatch方法 image.png 我们从这个方法一直跟进去,直到这个方法,从函数名称,我们大概可以猜出,这是将我们request body转换成对应controller上的参数 image.png 我们再往下走,来到这个循环里,可以看到,spring mvc默认有多个转换器 image.png 然后因为我们的content-type传的是application/json,**MappingJackson2HttpMessageConverter **这个转换器就尝试解释我们的request body,很遗憾,转换失败 image.png 到这里,我们就有思路了,既然是因为没有合适的转换器去自动适配我们的数据,我们就应该造一个出来去自动适配我们的数据格式。 好了,下面就是代码的环节,把主要代码都贴出来,篇幅有限。 这个是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 {
}

image.png image.png 这个配置主要是把我们配置文件里的公钥私钥都加载进来,并初始化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

好了,接下来就是测试环节,是骡子是马就是这一刻!!! image.png image.png image.png 0033dr8Dgy1gsuhzhf8yrj609o09q75l02.jpg