美文网首页SpringBoot那些事Springboot
SpringBoot 请求消息体解密(通信加密解密)

SpringBoot 请求消息体解密(通信加密解密)

作者: BeRicher | 来源:发表于2018-12-24 15:52 被阅读0次

    介绍

    在一些安全性要求较高的项目中,我们希望客户端请求数据可以做到数据加密,服务器端进行解密。(单纯的HTTPS仍难以满足安全需要。)

    本文基于SpringBoot针对消息体进行解密,目前仅支持请求消息解密。(响应消息过大情况下,加密会带来严重的性能问题。)

    流程如下:
    使用DES cbc模式对称加密请求体。要求客户端请求前加对消息体进行加密,服务器端通过SpringMVC Advice拦截请求解密后,传给controller的方法。

    @ControllerAdvice与RequestBodyAdviceAdapter

    @ControllerAdvice注解可以扫描针对Controller层的扩展组件。通过@Sort注解可以使其支持顺序加载。
    RequestBodyAdviceAdapter是RequestBodyAdvice适配器类,可以方便的扩展所需要的方法。

    RequestBodyAdvice功能如下:
    允许在请求消息体在被读取及调用convert转换成实体之前做一些个人化操作,作用于含有@RequestBody注解的请求。实现此接口的类,需要在RequestMappingHandlerAdapter中配置或通过@ControllerAdvice注解配置。

    原文如下:

    /**
     * Allows customizing the request before its body is read and converted into an
     * Object and also allows for processing of the resulting Object before it is
     * passed into a controller method as an {@code @RequestBody} or an
     * {@code HttpEntity} method argument.
     *
     * <p>Implementations of this contract may be registered directly with the
     * {@code RequestMappingHandlerAdapter} or more likely annotated with
     * {@code @ControllerAdvice} in which case they are auto-detected.
     *
     * @author Rossen Stoyanchev
     * @since 4.2
     */
    

    完整代码如下:

    SecretRequestAdvice

    @Slf4j
    @ControllerAdvice
    @ConditionalOnProperty(prefix = "faster.secret", name = "enabled", havingValue = "true")
    @EnableConfigurationProperties({SecretProperties.class})
    @Order(1)
    public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
        @Autowired
        private SecretProperties secretProperties;
    
    
        /**
         * 是否支持加密消息体
         *
         * @param methodParameter methodParameter
         * @return true/false
         */
        private boolean supportSecretRequest(MethodParameter methodParameter) {
            if (!secretProperties.isScanAnnotation()) {
                return true;
            }
            //判断class是否存在注解
            if (methodParameter.getContainingClass().getAnnotation(secretProperties.getAnnotationClass()) != null) {
                return true;
            }
            //判断方法是否存在注解
            return methodParameter.getMethodAnnotation(secretProperties.getAnnotationClass()) != null;
        }
    
    
        @Override
        public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
            return true;
        }
    
        @Override
        public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
            //如果支持加密消息,进行消息解密。
            boolean supportSafeMessage = supportSecretRequest(parameter);
            String httpBody;
            if (supportSafeMessage) {
                httpBody = decryptBody(inputMessage);
                if (httpBody == null) {
                    throw new HttpMessageNotReadableException("request body decrypt error");
                }
            } else {
                httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
            }
            //返回处理后的消息体给messageConvert
            return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
        }
    
        /**
         * 解密消息体,3des解析(cbc模式)
         *
         * @param inputMessage 消息体
         * @return 明文
         */
        private String decryptBody(HttpInputMessage inputMessage) throws IOException {
            InputStream encryptStream = inputMessage.getBody();
            String encryptBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
            return DesCbcUtil.decode(encryptBody, secretProperties.getDesSecretKey(), secretProperties.getDesIv());
        }
    }
    

    SecretBody

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    public @interface SecretBody {
    }
    

    SecretHttpMessage

    @AllArgsConstructor
    @NoArgsConstructor
    public class SecretHttpMessage implements HttpInputMessage {
        private InputStream body;
        private HttpHeaders httpHeaders;
    
        @Override
        public InputStream getBody() {
            return this.body;
        }
    
        @Override
        public HttpHeaders getHeaders() {
            return this.httpHeaders;
        }
    }
    

    SecretProperties

    @ConfigurationProperties(prefix = "faster.secret")
    @Data
    public class SecretProperties {
        /**
         * 是否开启
         */
        private boolean enabled;
        /**
         * 是否扫描注解
         */
        private boolean scanAnnotation;
        /**
         * 扫描自定义注解
         */
        private Class<? extends Annotation> annotationClass = SecretBody.class;
    
        /**
         * 3des 密钥长度不得小于24
         */
        private String desSecretKey = "b2c17b46e2b1415392aab5a82869856c";
        /**
         * 3des IV向量必须为8位
         */
        private String desIv = "61960842";
    
    }
    

    DesCbcUtil

    @Slf4j
    public class DesCbcUtil {
        // 加解密统一使用的编码方式
        private final static String encoding = "UTF-8";
    
        /**
         * 3DES加密
         *
         * @param plainText 普通文本
         * @return 加密后的文本,失败返回null
         */
        public static String encode(String plainText, String secretKey, String iv) {
            String result = null;
            try {
                DESedeKeySpec deSedeKeySpec = new DESedeKeySpec(secretKey.getBytes());
                SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("desede");
                Key desKey = secretKeyFactory.generateSecret(deSedeKeySpec);
                Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
                IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
                cipher.init(Cipher.ENCRYPT_MODE, desKey, ips);
                byte[] encryptData = cipher.doFinal(plainText.getBytes(encoding));
                result = Base64Utils.encodeToString(encryptData);
            } catch (Exception e) {
                log.error("DesCbcUtil encode error : {}", e);
            }
            return result;
        }
    
        /**
         * 3DES解密
         *
         * @param encryptText 加密文本
         * @return 解密后明文,失败返回null
         */
        public static String decode(String encryptText, String secretKey, String iv) {
            String result = null;
            try {
                DESedeKeySpec spec = new DESedeKeySpec(secretKey.getBytes());
                SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("desede");
                Key desKey = secretKeyFactory.generateSecret(spec);
                Cipher cipher = Cipher.getInstance("desede/CBC/PKCS5Padding");
                IvParameterSpec ips = new IvParameterSpec(iv.getBytes());
                cipher.init(Cipher.DECRYPT_MODE, desKey, ips);
                byte[] decryptData = cipher.doFinal(Base64Utils.decodeFromString(encryptText));
                result = new String(decryptData, encoding);
            } catch (Exception e) {
                log.error("DesCbcUtil decode error : {}", e.getMessage());
            }
            return result;
        }
    
    }
    

    使用方式

    使用以下注解即可快速开启全部请求的服务器端消息体解密功能。

    faster:
      secret:
        enabled: true
    

    局部解密

    使用scan-annotation可开启注解所标注的Conrtoller的类或其方法的解密功能。将要解密的方法或类上添加@SecretBody注解。并开启以下配置:

    faster:
      secret:
        enabled: true
        scan-annotation: true
    

    可以使用annotation-class配置自己的自定义注解:

    faster:
      secret:
        enabled: true
        scan-annotation: true
        annotation-class: cn.test.xxx
    

    注解使用

    作用于整个类:

    @SecretBody
    public class DemoController {
    
    }
    

    作用于方法:

    
    public class DemoController {
       @PostMapping("secretBody")
       @SecretBody
        public int secretBody(@RequestBody UserEntity userEntity) {
            log.info("{}", userEntity);
            return 0;
        }
    }
    

    密钥

    默认密钥如下,可以自行修改

    faster:
      secret:
        enabled: true
        des-secret-key: b2c17b46e2b1415392aab5a82869856c
        des-iv: 61960842
    

    前端调用

    前端调用时,需先将要请求的消息体通过DEScbc模式加密消息体(如json字符串)后传输。一般在http工具的请求拦截器中进行处理。如为json,仍然需要指定content-type为application/json。
    postman请求示例如下:

    image.png

    快速开发框架
    高质量图片压缩工具

    相关文章

      网友评论

        本文标题:SpringBoot 请求消息体解密(通信加密解密)

        本文链接:https://www.haomeiwen.com/subject/hmjzkqtx.html