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