介紹
在一些安全性要求較高的項(xiàng)目中,我們希望客戶端請求數(shù)據(jù)可以做到數(shù)據(jù)加密,服務(wù)器端進(jìn)行解密。(單純的HTTPS仍難以滿足安全需要。)
本文基于SpringBoot針對消息體進(jìn)行解密,目前僅支持請求消息解密。(響應(yīng)消息過大情況下,加密會(huì)帶來嚴(yán)重的性能問題。)
流程如下:
使用DES cbc模式對稱加密請求體。要求客戶端請求前加對消息體進(jìn)行加密,服務(wù)器端通過SpringMVC Advice攔截請求解密后,傳給controller的方法。
@ControllerAdvice與RequestBodyAdviceAdapter
@ControllerAdvice注解可以掃描針對Controller層的擴(kuò)展組件。通過@Sort注解可以使其支持順序加載。
RequestBodyAdviceAdapter是RequestBodyAdvice適配器類,可以方便的擴(kuò)展所需要的方法。
RequestBodyAdvice功能如下:
允許在請求消息體在被讀取及調(diào)用convert轉(zhuǎn)換成實(shí)體之前做一些個(gè)人化操作,作用于含有@RequestBody注解的請求。實(shí)現(xiàn)此接口的類,需要在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 {
//如果支持加密消息,進(jìn)行消息解密。
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 {
// 加解密統(tǒng)一使用的編碼方式
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;
}
}
使用方式
使用以下注解即可快速開啟全部請求的服務(wù)器端消息體解密功能。
faster:
secret:
enabled: true
局部解密
使用scan-annotation可開啟注解所標(biāo)注的Conrtoller的類或其方法的解密功能。將要解密的方法或類上添加@SecretBody注解。并開啟以下配置:
faster:
secret:
enabled: true
scan-annotation: true
可以使用annotation-class配置自己的自定義注解:
faster:
secret:
enabled: true
scan-annotation: true
annotation-class: cn.test.xxx
注解使用
作用于整個(gè)類:
@SecretBody
public class DemoController {
}
作用于方法:
public class DemoController {
@PostMapping("secretBody")
@SecretBody
public int secretBody(@RequestBody UserEntity userEntity) {
log.info("{}", userEntity);
return 0;
}
}
密鑰
默認(rèn)密鑰如下,可以自行修改
faster:
secret:
enabled: true
des-secret-key: b2c17b46e2b1415392aab5a82869856c
des-iv: 61960842
前端調(diào)用
前端調(diào)用時(shí),需先將要請求的消息體通過DEScbc模式加密消息體(如json字符串)后傳輸。一般在http工具的請求攔截器中進(jìn)行處理。如為json,仍然需要指定content-type為application/json。
postman請求示例如下:
