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

440 阅读3分钟

在上一篇,安全传输敏感数据的最佳实践 (中),我们已经成功将加密数据解密,并赋值到参数上,接下我们就要加密数据,并返回给客户端。根据之前提到,加密数据就要用到客户端的公钥,我们怎么获取到客户端的公钥? a6a681ebgy1gp12do7du8j20a00a0aa5.jpg 其实有很多种方法,在这里我提供我的实现方式。我们可以设置一个请求头X-JWE-CLIENT,然后再取出其中的值,在配置文件application.yaml中,各个客户端对应他的clientId和base64的公钥字符串,这样我们就能够根据请求头,去取出对应客户端的公钥进行加密。为了方便一键安装使用,封装了一个jwe-security-spring-boot-starter。 新建一个模块 image.png

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.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 image.png

org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.sakka.jwe.security.JweSecurityAutoConfiguration

后了,starter完成后,在项目中引用 image.png 最后,看看输出结果,输出jwe内容(PS:如果大家觉得文章写的不错点赞关注收藏走一个) github地址 image.png

a6a681ebgy1gq1lfd5wv6j20og0ogwif.jpg