Spring请求参数和响应结果全局加密和解密(2)

阅读文本大概需要25分钟。

单纯的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&timestamp=%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&timestamp=%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&timestamp=%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&timestamp=%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&timestamp=%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&timestamp=%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

毕竟源码不会骗人。控制器方法返回值的处理是基本对称的,阅读起来也比较轻松。

来源:https://www.cnblogs.com/throwable/p/9471938.html

往期精彩

01 漫谈发版哪些事,好课程推荐

02 Linux的常用最危险的命令

03 精讲Spring&nbsp;Boot—入门+进阶+实例

04 优秀的Java程序员必须了解的GC哪些

05 互联网支付系统整体架构详解

关注我

每天进步一点点

很干!在看吗?☟

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值