接着上一话 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
好了,接下来就是测试环节,是骡子是马就是这一刻!!!
本文暂时没有评论,来添加一个吧(●'◡'●)