单纯的Json请求参数和Json响应结果的加解密处理最佳实践
一般情况下,对接方的请求参数和响应结果是完全规范统一使用Json(contentType指定为application/json,使用@RequestBody接收参数),那么所有的事情就会变得简单,因为不需要考虑请求参数由xxx=yyy&aaa=bbb转换为InputStream再交给SpringMVC处理,因此我们只需要提供一个MappingJackson2HttpMessageConverter子类实现(继承它并且覆盖对应方法,添加加解密特性)。我们还是使用标识接口用于决定请求参数或者响应结果是否需要加解密:
@RequiredArgsConstructor
public class CustomEncryptHttpMessageConverter extends MappingJackson2HttpMessageConverter {
private final ObjectMapper objectMapper;
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
if (Encryptable.class.isAssignableFrom(clazz)) {
EncryptModel in = objectMapper.readValue(StreamUtils.copyToByteArray(inputMessage.getBody()), EncryptModel.class);
String inRawSign = String.format("data=%s×tamp=%d", in.getData(), in.getTimestamp());
String inSign;
try {
inSign = EncryptUtils.SINGLETON.sha(inRawSign);
} catch (Exception e) {
throw new IllegalArgumentException("验证参数签名失败!");
}
if (!inSign.equals(in.getSign())) {
throw new IllegalArgumentException("验证参数签名失败!");
}
try {
return objectMapper.readValue(EncryptUtils.SINGLETON.decryptByAes(in.getData()), clazz);
} catch (Exception e) {
throw new IllegalArgumentException("解密失败!");
}
} else {
return super.readInternal(clazz, inputMessage);
}
}
@Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Class<?> clazz = (Class) type;
if (Encryptable.class.isAssignableFrom(clazz)) {
EncryptModel out = new EncryptModel();
out.setTimestamp(System.currentTimeMillis());
try {
out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(object)));
String rawSign = String.format("data=%s×tamp=%d", out.getData(), out.getTimestamp());
out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
} catch (Exception e) {
throw new IllegalArgumentException("参数签名失败!");
}
super.writeInternal(out, type, outputMessage);
} else {
super.writeInternal(object, type, outputMessage);
}
}
}
没错,代码是拷贝上一节提供的HttpMessageConverter实现,然后控制器方法的参数使用@RequestBody注解并且类型实现加解密标识接口Encryptable即可,返回值的类型也需要实现加解密标识接口Encryptable。这种做法可以让控制器的代码对加解密完全无感知。当然,也可以不改变原来的MappingJackson2HttpMessageConverter实现,使用RequestBodyAdvice和ResponseBodyAdvice完成相同的功能:
@RequiredArgsConstructor
public class CustomRequestBodyAdvice extends RequestBodyAdviceAdapter {
private final ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
Class<?> clazz = (Class) targetType;
return Encryptable.class.isAssignableFrom(clazz);
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
Class<?> clazz = (Class) targetType;
if (Encryptable.class.isAssignableFrom(clazz)) {
String content = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
EncryptModel in = objectMapper.readValue(content, EncryptModel.class);
String inRawSign = String.format("data=%s×tamp=%d", in.getData(), in.getTimestamp());
String inSign;
try {
inSign = EncryptUtils.SINGLETON.sha(inRawSign);
} catch (Exception e) {
throw new IllegalArgumentException("验证参数签名失败!");
}
if (!inSign.equals(in.getSign())) {
throw new IllegalArgumentException("验证参数签名失败!");
}
ByteArrayInputStream inputStream = new ByteArrayInputStream(in.getData().getBytes(Charset.forName("UTF-8")));
return new MappingJacksonInputMessage(inputStream, inputMessage.getHeaders());
} else {
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
}
@RequiredArgsConstructor
public class CustomResponseBodyAdvice extends JsonViewResponseBodyAdvice {
private final ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
Class<?> parameterType = returnType.getParameterType();
return Encryptable.class.isAssignableFrom(parameterType);
}
@Override
protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
MethodParameter returnType, ServerHttpRequest request,
ServerHttpResponse response) {
Class<?> parameterType = returnType.getParameterType();
if (Encryptable.class.isAssignableFrom(parameterType)) {
EncryptModel out = new EncryptModel();
out.setTimestamp(System.currentTimeMillis());
try {
out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(bodyContainer.getValue())));
String rawSign = String.format("data=%s×tamp=%d", out.getData(), out.getTimestamp());
out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
} catch (Exception e) {
throw new IllegalArgumentException("参数签名失败!");
}
} else {
super.beforeBodyWriteInternal(bodyContainer, contentType, returnType, request, response);
}
}
}
单纯的application/x-www-form-urlencoded表单请求参数和Json响应结果的加解密处理最佳实践
一般情况下,对接方的请求参数完全采用application/x-www-form-urlencoded表单请求参数返回结果全部按照Json接收,我们也可以通过一个HttpMessageConverter实现就完成加解密模块。
public class FormHttpMessageConverter implements HttpMessageConverter<Object> {
private final List<MediaType> mediaTypes;
private final ObjectMapper objectMapper;
public FormHttpMessageConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.mediaTypes = new ArrayList<>(1);
this.mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return Encryptable.class.isAssignableFrom(clazz) && mediaTypes.contains(mediaType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return Encryptable.class.isAssignableFrom(clazz) && mediaTypes.contains(mediaType);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return mediaTypes;
}
@Override
public Object read(Class<?> clazz, HttpInputMessage inputMessage) throws
IOException, HttpMessageNotReadableException {
if (Encryptable.class.isAssignableFrom(clazz)) {
String content = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
EncryptModel in = objectMapper.readValue(content, EncryptModel.class);
String inRawSign = String.format("data=%s×tamp=%d", in.getData(), in.getTimestamp());
String inSign;
try {
inSign = EncryptUtils.SINGLETON.sha(inRawSign);
} catch (Exception e) {
throw new IllegalArgumentException("验证参数签名失败!");
}
if (!inSign.equals(in.getSign())) {
throw new IllegalArgumentException("验证参数签名失败!");
}
try {
return objectMapper.readValue(EncryptUtils.SINGLETON.decryptByAes(in.getData()), clazz);
} catch (Exception e) {
throw new IllegalArgumentException("解密失败!");
}
} else {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = (contentType != null && contentType.getCharset() != null ?
contentType.getCharset() : Charset.forName("UTF-8"));
String body = StreamUtils.copyToString(inputMessage.getBody(), charset);
String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
for (String pair : pairs) {
int idx = pair.indexOf('=');
if (idx == -1) {
result.add(URLDecoder.decode(pair, charset.name()), null);
} else {
String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
result.add(name, value);
}
}
return result;
}
}
@Override
public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Class<?> clazz = o.getClass();
if (Encryptable.class.isAssignableFrom(clazz)) {
EncryptModel out = new EncryptModel();
out.setTimestamp(System.currentTimeMillis());
try {
out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(o)));
String rawSign = String.format("data=%s×tamp=%d", out.getData(), out.getTimestamp());
out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
StreamUtils.copy(objectMapper.writeValueAsString(out)
.getBytes(Charset.forName("UTF-8")), outputMessage.getBody());
} catch (Exception e) {
throw new IllegalArgumentException("参数签名失败!");
}
} else {
String out = objectMapper.writeValueAsString(o);
StreamUtils.copy(out.getBytes(Charset.forName("UTF-8")), outputMessage.getBody());
}
}
}
上面的HttpMessageConverter的实现可以参考org.springframework.http.converter.FormHttpMessageConverter。
小结这篇文章强行复杂化了实际的情况(但是在实际中真的碰到过),一般情况下,现在流行使用Json进行数据传输,在SpringMVC项目中,我们只需要针对性地改造MappingJackson2HttpMessageConverter即可(继承并且添加特性),如果对SpringMVC的源码相对熟悉的话,直接添加自定义的RequestBodyAdvice(RequestBodyAdviceAdapter)和ResponseBodyAdvice(JsonViewResponseBodyAdvice)实现也可以达到目的。至于为什么使用HttpMessageConverter做加解密功能,这里基于SpringMVC源码的对请求参数处理的过程整理了一张处理流程图:
上面流程最核心的代码可以看
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
和
HandlerMethodArgumentResolverComposite#resolveArgument
毕竟源码不会骗人。控制器方法返回值的处理是基本对称的,阅读起来也比较轻松。