在上一篇,安全传输敏感数据的最佳实践 (中),我们已经成功将加密数据解密,并赋值到参数上,接下我们就要加密数据,并返回给客户端。根据之前提到,加密数据就要用到客户端的公钥,我们怎么获取到客户端的公钥?
其实有很多种方法,在这里我提供我的实现方式。我们可以设置一个请求头X-JWE-CLIENT,然后再取出其中的值,在配置文件application.yaml中,各个客户端对应他的clientId和base64的公钥字符串,这样我们就能够根据请求头,去取出对应客户端的公钥进行加密。为了方便一键安装使用,封装了一个jwe-security-spring-boot-starter。
新建一个模块
package cn.sakka.jwe.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author sakka
* @version 1.0
* @description: jwe security 自动配置类
* @date 2023/4/5
*/
@ConditionalOnProperty(value = "jwe.security.enabled", havingValue = "true")
@Configuration
public class JweSecurityAutoConfiguration {
@Bean
public JweSecurityMappingJackson2HttpMessageConverter jweMappingJackson2HttpMessageConverter(@Autowired JweSecurityProperties jweSecurityProperties) {
return new JweSecurityMappingJackson2HttpMessageConverter(jweSecurityProperties);
}
@ConfigurationProperties(prefix = "jwe.security")
@Bean
public JweSecurityProperties jweSecurityProperties() {
return new JweSecurityProperties();
}
}
package cn.sakka.jwe.security;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author sakka
* @version 1.0
* @description: //TODO
* @date 2023/3/30
*/
@Target({TYPE})
@Retention(RUNTIME)
public @interface JweSecurityEntity {
}
package cn.sakka.jwe.security;
public class JweSecurityException extends RuntimeException {
public JweSecurityException() {
}
public JweSecurityException(String message) {
super(message);
}
public JweSecurityException(String message, Throwable cause) {
super(message, cause);
}
public JweSecurityException(Throwable cause) {
super(cause);
}
public JweSecurityException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
主要多了writeInternal方法,该方法读取客户端公钥并进行加密,canWrite判断数据是否需要jwe加密
package cn.sakka.jwe.security;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.CharSequenceUtil;
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.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.StreamUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.WebRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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: //TODO
* @date 2023/3/30
*/
public class JweSecurityMappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public static final String X_JWE_CLIENT = "X-JWE-CLIENT";
/**
* 配置文件
*/
private final JweSecurityProperties jweSecurityProperties;
/**
* 客户端公钥
*/
private final Map<String, PublicKey> clientPublicKeys;
/**
* 服务器私钥
*/
private PrivateKey serverPrivateKey;
public JweSecurityMappingJackson2HttpMessageConverter(JweSecurityProperties jweSecurityProperties) {
this(jweSecurityProperties, Jackson2ObjectMapperBuilder.json().build());
}
public JweSecurityMappingJackson2HttpMessageConverter(JweSecurityProperties jweSecurityProperties, ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
this.jweSecurityProperties = jweSecurityProperties;
clientPublicKeys = new HashMap<>();
init();
}
private void init() {
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(jweSecurityProperties.getServerPrivateKey()));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
serverPrivateKey = keyFactory.generatePrivate(keySpec);
for (JweSecurityProperties.ClientPublicKey clientPublicKey : jweSecurityProperties.getClientPublicKeys()) {
String clientId = clientPublicKey.getClientId();
PublicKey publicKey = getPublicKey(clientPublicKey.getClientKey());
clientPublicKeys.put(clientId, publicKey);
}
} catch (Exception e) {
throw new JweSecurityException(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);
}
@Override
public boolean canWrite(Type type, Class<?> contextClass, MediaType mediaType) {
return super.canWrite(type, contextClass, mediaType) && isJweEntity(type, contextClass);
}
protected boolean isJweEntity(Type type, Class<?> contextClass) {
JavaType javaType = getJavaType(type, contextClass);
return javaType.getRawClass().getDeclaredAnnotation(JweSecurityEntity.class) != null;
}
@Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
HttpHeaders headers = inputMessage.getHeaders();
String clientId = headers.getFirst(X_JWE_CLIENT);
if (CharSequenceUtil.isNotEmpty(clientId)) {
RequestContextHolder.currentRequestAttributes().setAttribute(X_JWE_CLIENT, clientId, WebRequest.SCOPE_REQUEST);
}
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 = JweSecurityRSAEncryptionDecryption.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 JweSecurityException(e);
}
}
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//读取请求头
String clientId = (String) RequestContextHolder.currentRequestAttributes().getAttribute(X_JWE_CLIENT, WebRequest.SCOPE_REQUEST);
if (CharSequenceUtil.isNotEmpty(clientId)) {
HttpHeaders headers = outputMessage.getHeaders();
MediaType contentType = headers.getContentType();
Charset charset = getCharset(contentType);
PublicKey publicKey = clientPublicKeys.get(clientId);
//如果公钥为空,直接抛出异常
if (publicKey == null) {
throw new JweSecurityException(CharSequenceUtil.format("Not found {} client publicKey"));
}
//找到对应的公钥,就加密数据并返回
OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
try {
String encrypt = JweSecurityRSAEncryptionDecryption.encrypt(getObjectMapper().writeValueAsString(object).getBytes(charset), publicKey);
outputStream.write(encrypt.getBytes(charset));
} catch (Exception e) {
throw new JweSecurityException(e);
}
} else {
//如果没有带上请求头,就走原来的路
super.writeInternal(object, type, outputMessage);
}
}
}
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 {
}
package cn.sakka.jwe.security;
import lombok.Data;
import java.util.List;
@Data
public class JweSecurityProperties {
private boolean enabled;
private String serverPrivateKey;
private List<ClientPublicKey> clientPublicKeys;
@Data
public static class ClientPublicKey {
private String clientId;
private String clientKey;
}
}
package cn.sakka.jwe.security;
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 JweSecurityRSAEncryptionDecryption {
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();
}
}
新建文件resources\META-INF\spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.sakka.jwe.security.JweSecurityAutoConfiguration
后了,starter完成后,在项目中引用
最后,看看输出结果,输出jwe内容(PS:如果大家觉得文章写的不错点赞~关注~收藏走一个)
github地址
本文暂时没有评论,来添加一个吧(●'◡'●)