SpringBoot那些事Springboot

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

2018-12-24  本文已影响0人  BeRicher

介绍

在一些安全性要求较高的项目中,我们希望客户端请求数据可以做到数据加密,服务器端进行解密。(单纯的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

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

上一篇 下一篇

猜你喜欢

热点阅读