前提

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

  • 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#readWithMessageConvertersHandlerMethodArgumentResolverComposite#resolveArgument,毕竟源码不会骗人。控制器方法返回值的处理是基本对称的,阅读起来也比较轻松。

参考资料:

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

(本文完 c-d-4)

原文链接

图片挂了可以看原文,一般是云存储换了导致...

SpringMVC请求参数和响应结果全局加密和解密的更多相关文章

  1. Fiddler插件 --- 解密Elong Mapi请求参数及响应内容

    当前问题: 在我们日常的Web/App测试过程中, Fiddler是一大辅助利器:在我们团队,也经常使用Fiddler进行App抓包测试. 艺龙 App使用的REST(内部称为Mapi)接口,在使用过 ...

  2. SpringMVC请求参数接收总结

    前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结.SpringMVC中处理控制器参数的接口是HandlerMethodArgumentRes ...

  3. SpringMVC请求参数接收总结(一)

    前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结.SpringMVC中处理控制器参数的接口是HandlerMethodArgumentRes ...

  4. SpringMVC请求参数总结

    前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结.SpringMVC中处理控制器参数的接口是HandlerMethodArgumentRes ...

  5. 2.5万字长文简单总结SpringMVC请求参数接收

    这是公众号<Throwable文摘>发布的第22篇原创文章,暂时收录于专辑<架构与实战>.暂定下一篇发布的长文是<图文分析JUC同步器框架>,下一篇发布的短文是&l ...

  6. springmvc请求参数异常统一处理

    1.ExceptionHandlerController package com.oy.controller; import java.text.MessageFormat; import org.s ...

  7. springmvc请求参数异常统一处理,结合钉钉报告信息定位bug位置

    参考之前一篇博客:springmvc请求参数异常统一处理 1.ExceptionHandlerController package com.oy.controller; import java.tex ...

  8. springboot springmvc拦截器 拦截POST、PUT、DELETE请求参数和响应数据,并记录操作日志

    1.操作日志实体类 @Document(collection = "operation_log") @Getter @Setter @ToString public class O ...

  9. 【C#】WebApi 添加过滤器,实现对请求参数和响应内容的日志记录

    filter的介绍 filter在Web API中经常会用到,主要用于记录日志,安全验证,全局错误处理等:Web API提供两种过滤器的基本类型:actionfilterattribute,excep ...

随机推荐

  1. Circular dependencies cannot exist in RelativeLayout

    循环布局错误!!! <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:an ...

  2. Opencv学习笔记3:边缘检测算子的实现方法

    一.边缘检测概念 图像的边缘检测的原理是检测出图像中所有灰度值变化较大的点,而且这些点连接起来就构成了若干线条,这些线条就可以称为图像的边缘.效果如图: 接下来介绍一下边缘提取的几种算子,具体证明过程 ...

  3. WPF常用控件样式( 内含一简单插件式开发示例)

    最近离职,离职前面的一份外派然后又外包的工作,总觉得不妥,之后外派的办个入职手续都一再失约,干脆推了.恰逢清明时节,暴雨纷纷,于是打算先休息休息调整下状态,工作的事情还是谨慎点的好,免得影响心情.话说 ...

  4. luogu P1965 转圈游戏

    题目描述 n 个小伙伴(编号从 0 到 n-1)围坐一圈玩游戏.按照顺时针方向给 n 个位置编号,从0 到 n-1.最初,第 0 号小伙伴在第 0 号位置,第 1 号小伙伴在第 1 号位置,……,依此 ...

  5. Codeforces 30 E. Tricky and Cleve Password

    \(>Codeforces \space 30\ E. Tricky\ and\ Cleve\ Password<\) 题目大意 : 给出一个串 \(S\),让你找出 \(A, B, C\ ...

  6. [SimpleOJ233]a xor b

    题目大意: 给你一个数列,求所有区间最大值和次大只异或的最大值. 思路: 很容易想到一个O(n^2)的暴力. O(n)的单调栈做法似乎也很好想,不过考场上没想出来. 对于数列上的某一个数,我们维护在它 ...

  7. [CodeForces-513E2]Subarray Cuts

    题目大意: 给你一个数列,从中选出k个互不重叠的非空子串,定义s[i]为第i个子串的和,求|s[1]-s[2]|+|s[2]-s[3]|+...+|s[k-1]-s[k]|的最大值. 思路: 考虑将绝 ...

  8. python开发_类型转换convert

    在python的开发过程中,难免会遇到类型转换,这里给出常见的类型转换demo: int(x [,base ]) 将x转换为一个整数 long(x [,base ]) 将x转换为一个长整数 float ...

  9. [转]Jquery实现页面定时跳转

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  10. Codeforces Round #245 (Div. 2) B. Balls Game 并查集

    B. Balls Game Time Limit: 1 Sec  Memory Limit: 256 MB 题目连接 http://codeforces.com/contest/430/problem ...