需求背景
在我们日常的Java开发中,免不了和其他系统的业务交互,或者微服务之间的接口调用
如果我们想保证数据传输的安全,对接口出参加密,入参解密。
但是不想写重复代码,我们可以提供一个通用starter,提供通用加密解密功能
开源项目crypto-spring-boot-starter
项目地址
https://gitee.com/springboot-hlh/spring-boot-csdn/tree/master/09-spring-boot-interface-crypto
实现原理
使用EncryptResponseBodyAdvice
对返回数据实体类做加密。针对指定的接口的响应数据做加密处理。
@ControllerAdvice
public class EncryptResponseBodyAdvice implements ResponseBodyAdvice<Result<?>> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
ParameterizedTypeImpl genericParameterType = (ParameterizedTypeImpl)returnType.getGenericParameterType();
// 如果直接是Result并且有解密注解,则处理
if (genericParameterType.getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {
return true;
}
// 如果不是ResponseBody或者是Result,则放行
if (genericParameterType.getRawType() != ResponseEntity.class) {
return false;
}
// 如果是ResponseEntity<Result>并且有解密注解,则处理
for (Type type : genericParameterType.getActualTypeArguments()) {
if (((ParameterizedTypeImpl) type).getRawType() == Result.class && returnType.hasMethodAnnotation(EncryptionAnnotation.class)) {
return true;
}
}
return false;
}
@SneakyThrows
@Override
public Result<?> beforeBodyWrite(Result<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 真实数据
Object data = body.getData();
// 如果data为空,直接返回
if (data == null) {
return body;
}
// 如果是实体,并且继承了Request,则放入时间戳
if (data instanceof RequestBase) {
((RequestBase)data).setCurrentTimeMillis(System.currentTimeMillis());
}
String dataText = JSONUtil.toJsonStr(data);
// 如果data为空,直接返回
if (StringUtils.isBlank(dataText)) {
return body;
}
// 如果位数小于16,报错
if (dataText.length() < 16) {
throw new CryptoException("加密失败,数据小于16位");
}
String encryptText = AESUtil.encryptHex(dataText);
return Result.builder()
.status(body.getStatus())
.data(encryptText)
.message(body.getMessage())
.build();
}
}
DecryptRequestBodyAdvice
对请求实体类做解密操作,将类型是RequestData
的数据解析成@RequestBody
修饰的类。解密类对于响应实体类的接口发送时间做了限制,在指定时间内有效。
@ControllerAdvice
public class DecryptRequestBodyAdvice implements RequestBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
/**
* 方法上有DecryptionAnnotation注解的,进入此拦截器
* @param methodParameter 方法参数对象
* @param targetType 参数的类型
* @param converterType 消息转换器
* @return true,进入,false,跳过
*/
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(DecryptionAnnotation.class);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
return inputMessage;
}
/**
* 转换之后,执行此方法,解密,赋值
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@SneakyThrows
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// 获取request
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
if (servletRequestAttributes == null) {
throw new ParamException("request错误");
}
HttpServletRequest request = servletRequestAttributes.getRequest();
// 获取数据
ServletInputStream inputStream = request.getInputStream();
RequestData requestData = objectMapper.readValue(inputStream, RequestData.class);
if (requestData == null || StringUtils.isBlank(requestData.getText())) {
throw new ParamException("参数错误");
}
// 获取加密的数据
String text = requestData.getText();
// 放入解密之前的数据
request.setAttribute(CryptoConstant.INPUT_ORIGINAL_DATA, text);
// 解密
String decryptText = null;
try {
decryptText = AESUtil.decrypt(text);
} catch (Exception e) {
throw new ParamException("解密失败");
}
if (StringUtils.isBlank(decryptText)) {
throw new ParamException("解密失败");
}
// 放入解密之后的数据
request.setAttribute(CryptoConstant.INPUT_DECRYPT_DATA, decryptText);
// 获取结果
Object result = objectMapper.readValue(decryptText, body.getClass());
// 强制所有实体类必须继承RequestBase类,设置时间戳
if (result instanceof RequestBase) {
// 获取时间戳
Long currentTimeMillis = ((RequestBase) result).getCurrentTimeMillis();
// 有效期 60秒
long effective = 60*1000;
// 时间差
long expire = System.currentTimeMillis() - currentTimeMillis;
// 是否在有效期内
if (Math.abs(expire) > effective) {
throw new ParamException("时间戳不合法");
}
// 返回解密之后的数据
return result;
} else {
throw new ParamException(String.format("请求参数类型:%s 未继承:%s", result.getClass().getName(), RequestBase.class.getName()));
}
}
/**
* 如果body为空,转为空对象
* @param body spring解析完的参数
* @param inputMessage 输入参数
* @param parameter 参数对象
* @param targetType 参数类型
* @param converterType 消息转换类型
* @return 真实的参数
*/
@SneakyThrows
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
String typeName = targetType.getTypeName();
Class<?> bodyClass = Class.forName(typeName);
return bodyClass.newInstance();
}
}
加密解密,读取配置文件crypto.properties
里面的数据
@Configuration
@ConfigurationProperties(prefix = "crypto")
@PropertySource("classpath:crypto.properties")
@Data
@EqualsAndHashCode
@Getter
public class CryptConfig implements Serializable {
private Mode mode;
private Padding padding;
private String key;
private String iv;
}
根据配置文件封装成具体的加解密工具类
@Configuration
public class AppConfig {
@Resource
private CryptConfig cryptConfig;
@Bean
public AES aes() {
return new AES(cryptConfig.getMode(), cryptConfig.getPadding(), cryptConfig.getKey().getBytes(StandardCharsets.UTF_8), cryptConfig.getIv().getBytes(StandardCharsets.UTF_8));
}
}
开源项目encrypt-body-spring-boot-starter
项目地址
https://gitee.com/licoy/encrypt-body-spring-boot-starter
对应的maven依赖
<dependency>
<groupId>cn.licoy</groupId>
<artifactId>encrypt-body-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
实现原理
和上面的项目基本上一致。
核心类仍然是DecryptRequestBodyAdvice
和EncryptResponseBodyAdvice
,对响应和请求数据做处理。
该组件较上面的优势在于,支持了加解密方式的可配置,支持方法,实体类和参数的不同粒度加解密。