SpringMVC请求参数和响应结果全局加密和解密

前提

前段时间在做一个对外的网关项目,涉及到加密和解密模块,这里详细分析解决方案和适用的场景。为了模拟真实的交互场景,先定制一下整个交互流程。第三方传输(包括请求和响应)数据报文包括三个部分:

  • 1、timestamp,long类型,时间戳。
  • 2、data,String类型,实际的业务请求数据转化成的Json字符串再进行加密得到的密文。
  • 3、sign,签名,生成规则算法伪代码是SHA-256(data=xxx&timestamp=11111),防篡改。

为了简单起见,加密和解密采用AES,对称秘钥为"throwable"。上面的场景和加解密例子仅仅是为了模拟真实场景,安全系数低,切勿直接用于生产环境。

现在还有一个地方要考虑,就是无法得知第三方如何提交请求数据,假定都是采用POST的Http请求方法,提交报文的时候指定ContentType为application/json或者application/x-www-form-urlencoded,两种ContentType提交方式的请求体是不相同的:

//application/x-www-form-urlencoded
timestamp=xxxx&data=yyyyyy&sign=zzzzzzz

//application/json
{"timestamp":xxxxxx,"data":"yyyyyyyy","sign":"zzzzzzz"}

最后一个要考虑的地方是,第三方强制要求部分接口需要用明文进行请求,在提供一些接口方法的时候,允许使用明文交互。总结一下就是要做到以下三点:

  • 1、需要加解密的接口请求参数要进行解密,响应结果要进行加密。
  • 2、不需要加解密的接口可以用明文请求。
  • 3、兼容ContentType为application/json或者application/x-www-form-urlencoded两种方式。

上面三种情况要同时兼容算是十分严苛的场景,在生产环境中可能也是极少情况下才遇到,不过还是能找到相对优雅的解决方案。先定义两个特定场景的接口:

1、下单接口(加密)

  • URL:/order/save
  • HTTP METHOD:POST
  • ContentType:application/x-www-form-urlencoded
  • 原始参数:orderId=yyyyyyyyy&userId=xxxxxxxxx&amount=zzzzzzzzz
  • 加密参数:timestamp=xxxx&data=yyyyyy&sign=zzzzzzz

2、订单查询接口(明文)

  • URL:/order/query
  • ContentType:application/json
  • HTTP METHOD:POST
  • 原始参数:{"userId":"xxxxxxxx"}

两个接口的ContentType不相同是为了故意复杂化场景,在下面的可取方案中,做法是把application/x-www-form-urlencoded中的形式如xxx=yyy&aaa=bbb的表单参数和application/json中形式如{"key":"value"}的请求参数统一当做application/json形式的参数处理,这样的话,我们就可以直接在控制器方法中使用@RequestBody。

方案

我们首先基于上面说到的加解密方案,提供一个加解密工具类:

public enum EncryptUtils {

    /**
     * SINGLETON
     */
    SINGLETON;

    private static final String SECRET = "throwable";
    private static final String CHARSET = "UTF-8";

    public String sha(String raw) throws Exception {
        MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
        messageDigest.update(raw.getBytes(CHARSET));
        return Hex.encodeHexString(messageDigest.digest());
    }

    private Cipher createAesCipher() throws Exception {
        return Cipher.getInstance("AES");
    }
    
    public String encryptByAes(String raw) throws Exception {
        Cipher aesCipher = createAesCipher();
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET)));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        aesCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        byte[] bytes = aesCipher.doFinal(raw.getBytes(CHARSET));
        return Hex.encodeHexString(bytes);
    }

    public String decryptByAes(String raw) throws Exception {
        byte[] bytes = Hex.decodeHex(raw);
        Cipher aesCipher = createAesCipher();
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(128, new SecureRandom(SECRET.getBytes(CHARSET)));
        SecretKey secretKey = keyGenerator.generateKey();
        SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getEncoded(), "AES");
        aesCipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        return new String(aesCipher.doFinal(bytes), CHARSET);
    }
}

注意为了简化加解密操作引入了apache的codec依赖:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

上面的加解密过程中要注意两点:

  • 1、加密后的结果是byte数组,要把二进制转化为十六进制字符串。
  • 2、解密的时候要把原始密文由十六进制转化为二进制的byte数组。

上面两点必须注意,否则会产生乱码,这个和编码相关,具体可以看之前写的一篇博客。

不推荐的方案

其实最暴力的方案是直接定制每个控制器的方法参数类型,因为我们可以和第三方磋商哪些请求路径需要加密,哪些是不需要加密,甚至哪些是application/x-www-form-urlencoded,哪些是application/json的请求,这样我们可以通过大量的硬编码达到最终的目标。举个例子:

@RestController
public class Controller1 {

    @Autowired
    private ObjectMapper objectMapper;

    @PostMapping(value = "/order/save",
            consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<EncryptModel> saveOrder(@RequestParam(name = "sign") String sign,
                                                  @RequestParam(name = "timestamp") Long timestamp,
                                                  @RequestParam(name = "data") String data) throws Exception {
        EncryptModel model = new EncryptModel();
        model.setData(data);
        model.setTimestamp(timestamp);
        model.setSign(sign);
        String inRawSign = String.format("data=%s&timestamp=%d", model.getData(), model.getTimestamp());
        String inSign = EncryptUtils.SINGLETON.sha(inRawSign);
        if (!inSign.equals(model.getSign())){
            throw new IllegalArgumentException("验证参数签名失败!");
        }
        //这里忽略实际的业务逻辑,简单设置返回的data为一个map
        Map<String, Object> result = new HashMap<>(8);
        result.put("code", "200");
        result.put("message", "success");
        EncryptModel out = new EncryptModel();
        out.setTimestamp(System.currentTimeMillis());
        out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(result)));
        String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
        out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
        return ResponseEntity.ok(out);
    }

    @PostMapping(value = "/order/query",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<Order>  queryOrder(@RequestBody User user){
        Order order = new Order();
        //这里忽略实际的业务逻辑
        return ResponseEntity.ok(order);
    }
}

这种做法能在短时间完成对应的加解密功能,不需要加解密的接口不用引入相关的代码即可。缺陷十分明显,存在硬编码、代码冗余等问题,一旦接口增多,项目的维护难度大大提高。因此,这种做法是不可取的。

混合方案之Filter和SpringMVC的Http消息转换器

这里先说一点,这里是在SpringMVC中使用Filter。因为要兼容两种contentType,我们需要做到几点:

  • 1、修改请求头的contentType为application/json。
  • 2、修改请求体中的参数,统一转化为InputStream。
  • 3、定制URL规则,区别需要加解密和不需要加解密的URL。

使用Filter有一个优点:不需要理解SpringMVC的流程,也不需要扩展SpringMVC的相关组件。缺点也比较明显:

  • 1、如果需要区分加解密,只能通过URL规则进行过滤。
  • 2、需要加密的接口的SpringMVC控制器的返回参数必须是加密后的实体类,无法做到加密逻辑和业务逻辑完全拆分,也就是解密逻辑对接收的参数是无感知,但是加密逻辑对返回结果是有感知的。

PS:上面提到的几个需要修改请求参数、请求头等是因为特殊场景的定制,所以如果无此场景可以直接看下面的"单纯的Json请求参数和Json响应结果"小节。流程大致如下:

sp-ed-1

编写Filter的实现和HttpServletRequestWrapper的实现:

//CustomEncryptFilter
@RequiredArgsConstructor
public class CustomEncryptFilter extends OncePerRequestFilter {

    private final ObjectMapper objectMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //Content-Type
        String contentType = request.getContentType();
        String requestBody = null;
        boolean shouldEncrypt = false;
        if (StringUtils.substringMatch(contentType, 0, MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            shouldEncrypt = true;
            requestBody = convertFormToString(request);
        } else if (StringUtils.substringMatch(contentType, 0, MediaType.APPLICATION_JSON_VALUE)) {
            shouldEncrypt = true;
            requestBody = convertInputStreamToString(request.getInputStream());
        }
        if (!shouldEncrypt) {
            filterChain.doFilter(request, response);
        } else {
            CustomEncryptHttpWrapper wrapper = new CustomEncryptHttpWrapper(request, requestBody);
            wrapper.putHeader("Content-Type", MediaType.APPLICATION_PROBLEM_JSON_UTF8_VALUE);
            filterChain.doFilter(wrapper, response);
        }
    }

    private String convertFormToString(HttpServletRequest request) {
        Map<String, String> result = new HashMap<>(8);
        Enumeration<String> parameterNames = request.getParameterNames();
        while (parameterNames.hasMoreElements()) {
            String name = parameterNames.nextElement();
            result.put(name, request.getParameter(name));
        }
        try {
            return objectMapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private String convertInputStreamToString(InputStream inputStream) throws IOException {
        return StreamUtils.copyToString(inputStream, Charset.forName("UTF-8"));
    }
}

//CustomEncryptHttpWrapper
public class CustomEncryptHttpWrapper extends HttpServletRequestWrapper {

    private final Map<String, String> headers = new HashMap<>(8);
    private final byte[] data;

    public CustomEncryptHttpWrapper(HttpServletRequest request, String content) {
        super(request);
        data = content.getBytes(Charset.forName("UTF-8"));
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            headers.put(key, request.getHeader(key));
        }
    }

    public void putHeader(String key, String value) {
        headers.put(key, value);
    }

    @Override
    public String getHeader(String name) {
        return headers.get(name);
    }

    @Override
    public Enumeration<String> getHeaders(String name) {
        return Collections.enumeration(Collections.singletonList(headers.get(name)));
    }

    @Override
    public Enumeration<String> getHeaderNames() {
        return  Collections.enumeration(headers.keySet());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return !isReady();
            }

            @Override
            public boolean isReady() {
                return inputStream.available() > 0;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return super.getReader();
    }
}

//CustomEncryptConfiguration
@Configuration
public class CustomEncryptConfiguration {

    @Bean
    public FilterRegistrationBean<CustomEncryptFilter> customEncryptFilter(ObjectMapper objectMapper){
        FilterRegistrationBean<CustomEncryptFilter> bean = new FilterRegistrationBean<>(new CustomEncryptFilter(objectMapper));
        bean.addUrlPatterns("/e/*");
        return bean;
    }
}

控制器代码:

//可加密的,空接口
public interface Encryptable {
}


@Data
public class Order implements Encryptable{

    private Long userId;
}

@Data
public class EncryptResponse<T> implements Encryptable {
    
    private Integer code;
    private T data;
}

@RequiredArgsConstructor
@RestController
public class Controller {

    private final ObjectMapper objectMapper;

    @PostMapping(value = "/e/order/save",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public EncryptResponse<Order> saveOrder(@RequestBody Order order) throws Exception {
        //这里忽略实际的业务逻辑,简单设置返回的data为一个map
        EncryptResponse<Order> response = new EncryptResponse<>();
        response.setCode(200);
        response.setData(order);
        return response;
    }

    @PostMapping(value = "/c/order/query",
            consumes = MediaType.APPLICATION_JSON_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<Order> queryOrder(@RequestBody User user) {
        Order order = new Order();
        //这里忽略实际的业务逻辑
        return ResponseEntity.ok(order);
    }
}

这里可能有人有疑问,为什么不在Filter做加解密的操作?因为考虑到场景太特殊,要兼容两种形式的表单提交参数,如果在Filter做加解密操作,会影响到Controller的编码,这就违反了全局加解密不影响到里层业务代码的目标。上面的Filter只会拦截URL满足/e/*的请求,因此查询接口/c/order/query不会受到影响。这里使用了标识接口用于决定请求参数或者响应结果是否需要加解密,也就是只需要在HttpMessageConverter中判断请求参数的类型或者响应结果的类型是否加解密标识接口的子类:

@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主要需要判断请求参数的类型和返回值的类型,从而判断是否需要进行加解密。

单纯的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源码的对请求参数处理的过程整理了一张处理流程图:

sp-ed-2

上面流程最核心的代码可以看AbstractMessageConverterMethodArgumentResolver#readWithMessageConvertersHandlerMethodArgumentResolverComposite#resolveArgument,毕竟源码不会骗人。控制器方法返回值的处理基于是对称的,阅读起来也比较轻松。

参考资料:

  • spring-boot-web-starter:2.0.3.RELEASE源码。

(本文完 c-d-4)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 一、对全局返回json加密 1、了解HttpMessageConverter接口 HttpMessageConve...
    huangxiongbiao阅读 10,279评论 0 1
  • 前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。Sp...
    zhrowable阅读 2,009评论 0 15
  • 对于java中的思考的方向,1必须要看前端的页面,对于前端的页面基本的逻辑,如果能理解最好,不理解也要知道几点。 ...
    神尤鲁道夫阅读 802评论 0 0
  • “嘿,朱隽,俞闫斌,这附近新开了一家酒吧很不错,下了班我们一起去呀?” “不了,你们去吧。”朱隽想都没想就拒绝了。...
    www妞儿阅读 388评论 0 2