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

网站首页 > 开源技术 正文

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

wxchong 2024-08-02 08:56:01 开源技术 20 ℃ 0 评论

在上一篇,安全传输敏感数据的最佳实践 (中),我们已经成功将加密数据解密,并赋值到参数上,接下我们就要加密数据,并返回给客户端。根据之前提到,加密数据就要用到客户端的公钥,我们怎么获取到客户端的公钥?

其实有很多种方法,在这里我提供我的实现方式。我们可以设置一个请求头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地址

Tags:

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

欢迎 发表评论:

最近发表
标签列表