Spring MVC请求参数与响应结果全局加密和解密详解

前提

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

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响应结果"小节。流程大致如下:

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

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

参考资料:

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 快速解决SpringMVC @RequestBody 用map接收请求参数的问题

    一:遇到个跨域调用,因为传个我的参数不定,所以需要通过map来接收参数并进行签名验证等操作 理所当然的写出了下面的代码,但是发现map里并没有获取到传来的key-value值 @RequestMapping(value = "/callback", produces = "text/html;charset=UTF-8") @ResponseBody public String callback(@RequestBody Map<String, String&

  • 解决angular的post请求后SpringMVC后台接收不到参数值问题的方法

    这是我后台SpringMVC控制器接收isform参数的方法,只是简单的打出它的值: @RequestMapping(method = RequestMethod.POST) @ResponseBody public Map<String, Object> save( @RequestParam(value = "isform", required = false) String isform) { System.out.println("isform value

  • spring mvc中的@PathVariable获得请求url中的动态参数

    spring mvc中的@PathVariable是用来获得请求url中的动态参数的,十分方便,复习下: @Controller public class TestController { @RequestMapping(value="/user/{userId}/roles/{roleId}",method = RequestMethod.GET) public String getLogin(@PathVariable("userId") String user

  • Spring3 MVC请求参数获取的几种方法小结

    Spring3 MVC请求参数获取的几种方法 一.通过@PathVariabl获取路径中的参数 @RequestMapping(value="user/{id}/{name}",method=RequestMethod.GET) public String printMessage1(@PathVariable String id,@PathVariable String name, ModelMap model) { System.out.println(id); System.ou

  • axios发送post请求springMVC接收不到参数的解决方法

    axios发送post请求时,出现了参数后台接收不到的情况,分析了下请求,发现是请求头content-type不对,是application/json,正常应该是application/x-www-form-urlencoded. 解决方法有以下三种: 1.设置axios的默认请求头 //设置全局的 axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; var instance = a

  • 详解SpringMVC——接收请求参数和页面传参

    spring接收请求参数: 1,使用HttpServletRequest获取 @RequestMapping("/login.do") public String login(HttpServletRequest request){ String name = request.getParameter("name") String pass = request.getParameter("pass") } 2,Spring会自动将表单参数注入到方

  • Spring MVC请求参数接收的全面总结教程

    前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结.SpringMVC中处理控制器参数的接口是HandlerMethodArgumentResolver,此接口有众多子类,分别处理不同(注解类型)的参数,下面只列举几个子类: RequestParamMethodArgumentResolver:解析处理使用了@RequestParam注解的参数.MultipartFile类型参数和Simple类型(如long.int)参数. Reques

  • 学习SpringMVC——如何获取请求参数详解

    @RequestParam,你一定见过:@PathVariable,你肯定也知道:@QueryParam,你怎么会不晓得?!还有你熟悉的他(@CookieValue)!她(@ModelAndView)!它(@ModelAttribute)!没错,仅注解这块,spring mvc就为你打开了五彩斑斓的世界.来来来,不要兴(mi)奋(hu),坐下来,我们好好聊聊这么些个注解兄弟们~~~(wait, 都没有听过? 好,来,你坐前排,就你!)  一.spring mvc如何匹配请求路径--"请求路径哪家

  • Spring MVC请求参数与响应结果全局加密和解密详解

    前提 前段时间在做一个对外的网关项目,涉及到加密和解密模块,这里详细分析解决方案和适用的场景.为了模拟真实的交互场景,先定制一下整个交互流程.第三方传输(包括请求和响应)数据报文包括三个部分: 1.timestamp,long类型,时间戳. 2.data,String类型,实际的业务请求数据转化成的Json字符串再进行加密得到的密文. 3.sign,签名,生成规则算法伪代码是SHA-256(data=xxx&timestamp=11111),防篡改. 为了简单起见,加密和解密采用AES,对称秘钥

  • Spring MVC请求参数的深入解析

    请求参数解析 客户端请求在handlerMapping中找到对应handler后,将会继续执行DispatchServlet的doPatch()方法. 首先是找到handler对应的适配器. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 进入到getHandlerAdapter(mappedHandler.getHandler())方法中 protected HandlerAdapter getHandler

  • Spring MVC环境中文件上传功能的实现方法详解

    前言 我们在实际开发过程中,尤其是web项目开发,文件上传和下载的需求的功能非常场景,比如说用户头像.商品图片.邮件附件等等.其实文件上传下载的本质都是通过流的形式进行读写操作,而在开发中不同的框架都会对文件上传和下载有或多或少的封装,这里就以Spring MVC环境中文件的上传为例,讲解Spirng MVC环境下的文件上传功能实现.下面话不多说了,来一起看看详细的介绍吧. 一.客户端编程 由于多数文件上传都是通过表单形式提交给后台服务器的,因此,要实现文件上传功能,就需要提供一个文件上传的表单

  • Java spring mvc请求详情介绍

    目录 一.源码执行流程 二.源码执行流程图 三.spring mvc中的一核心组件 四.源码分析 五.获取组件相关逻辑: 六.获取参数,执行方法源码分析 七.渲染视图逻辑 前言: 本文源码基于spring-framework-5.3.10. mvc是spring源码中的一个子模块! 一.源码执行流程 用户发送请求至前端控制器DispatcherServlet. DispatcherServlet收到请求调用处理器映射器HandlerMapping.处理器映射器根据请求url找到具体的处理器,生成

  • Spring MVC的参数绑定和返回值问题

    一:参数绑定 参数绑定过程 在springMVC中,从前端(页面:jsp-)发送请求到后端(controller-),会包含一些数据,数据是如何到达Controller,这个过程就是参数绑定过程 1.默认支持的类型 SpringMVC有支持的默认的参数类型,在方法上给出默认的参数类型的声明就可以直接使用 HttpServletRequest request:通过request对象来获取请求的信息 HttpServletResponse response:通过response来处理响应信息 Htt

  • SpringBoot之自定义Filter获取请求参数与响应结果案例详解

    一个系统上线,肯定会或多或少的存在异常情况.为了更快更好的排雷,记录请求参数和响应结果是非常必要的.所以,Nginx 和 Tomcat 之类的 web 服务器,都提供了访问日志,可以帮助我们记录一些请求信息. 本文是在我们的应用中,定义一个Filter来实现记录请求参数和响应结果的功能. 有一定经验的都知道,如果我们在Filter中读取了HttpServletRequest或者HttpServletResponse的流,就没有办法再次读取了,这样就会造成请求异常.所以,我们需要借助 Spring

  • 基于spring mvc请求controller访问方式

    目录 spring mvc请求controller访问 1.一个Controller里含有不同的请求url 2.采用一个url访问 3.RequestMapping在Class上 4.在SpringMVC中常用的注解 springmvc请求一次,访问多个controller方法 举例 结论 spring mvc请求controller访问 1.一个Controller里含有不同的请求url @Controller //类似Struts的Action public class TestContro

  • 关于Spring MVC同名参数绑定问题的解决方法

    前言 最近在使用Spring MVC接收参数的时候,碰到个同名参数绑定的问题,参考了好几篇文章才解决问题,所以自己在这里总结一下,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍: 比如,我的表单是这样的: <form action="/test.action" method="post"> <input name="user.name"> <input name="acc.name"&g

随机推荐