SpringMVC请求参数北京PK10平台出租和响应结果全局加密和解密

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

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数组。
上面两点必须注意北京PK10平台出租(www.1159880099.com)QQ1159880099,否则会产生乱码,这个和编码相关,具体可以看之前写的一篇博客。

不推荐的方案
其实最暴力的方案是直接定制每个控制器的方法参数类型,因为我们可以和第三方磋商哪些请求路径需要加密,哪些是不需要加密,甚至哪些是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的实现:

//CustomEncryptFilterbr/>@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();
}

}

//CustomEncryptConfigurationbr/>@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;

}

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

SpringMVC请求参数北京PK10平台出租和响应结果全局加密和解密

原文地址:http://blog.51cto.com/13922273/2159533

时间: 2024-10-13 10:38:11

SpringMVC请求参数北京PK10平台出租和响应结果全局加密和解密的相关文章

Android recycleView 的一北京PK10平台出租些优化与相关问题

北京PK10平台出租论坛:haozbbs.com Q1446595067 recycleView 也出来很长时间了,记录一些自己见到的recycleView优化吧. 1.recyclerView.setHasFixedSize(true); Item的高度是固定的,设置这个选项可以提高性能.总得来说就是就是避免整个布局绘制.就是避免requestLayout. 具体的可以看看stackoverflow的这个几个问题 Understanding RecyclerView setHasFixedSi

北京PK10平台出租matlab的快捷方式与使用技巧

Matlab常用小技巧一:北京PK10平台出租(www.1159880099.com)QQ1159880099 1. m文件如果是函数,保存的文件名最好与函数名一致,这点都很清楚.不过容易疏忽的是,m文件名的命名尽量不要是简单的英文单词,最好是由大小写英文/数字/下划线等组成.原因是简单的单词命名容易与matlab内部函数名同名,结果会出现一些莫名其妙的错误.例如,写个m文件,命名为spy,运行时就弹出一个怪怪的figure,呵呵,我当初还以为是什么bug. 2. 调试程序时,经常要屏蔽掉一整段

JAVA基础知识之北京pk-10平台出租流

一.北京pk-10平台出租Q1446595067流解释API文档说明:FileReader流是用于读取字符文件的便捷类.此类的构造函数假定默认字符编码和默认字节缓冲区大小是合适的.如果要自己指定这些值,那么需要通过FileInputStream流对象来构造InputStreamReader流对象即可 FileReader类继承InputStreamReader类 public class FileReader extends InputStreamReader{}1)构造函数假定默认字符编码和默

Java内部类北京PK10平台出租的使用小结

为什么要北京PK10平台出租(www.1159880099.com)QQ1159880099 使用内部类:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响.内部类:顾名思义就是把类放在某个范围里类可以放在包中类可以放在文件夹中类可以放在类中 //内部类类可以放在方法中 //内部类 一.成员内部类类中放置内部类,类里套类,类中类要使用内部类,就需要在外部类中实例化内部类的对象,然后通过对象打点调用内部类中的

北京赛车pk10平台出租

北京赛车pk10平台出租QQ1148916888 网址:www.1148916888.com 心灵不在它生活的地方,但在它所爱的地方. -- 英国谚语人要正直,因为在其中有雄辩和德行的秘诀,有道德的影响力. -- 阿米尔我们活着不能与草木同腐,不能醉生梦死,枉度人生,要有所做为. -- 方志敏现实是此岸,理想是彼岸,中间隔着湍急的河流,行动则是架在河上的桥梁. -- 克雷洛夫正像新生的婴儿一样,科学的真理必将在斗争中不断发展,广泛传播,无往而不胜. -- 富兰克林在一个崇高的目标支持下,不停地工

xUtils与FastJson获取和北京-赛车平台出租解析网络数据

发送一个北京-赛车平台出租异步任务就系Json数据 首先打开一个接口,里面是Json数据,通过xUtils框架来发送get请求,得到接口的Json字符串,将字符串用FastJson解析成一个实体类,保存在集合当中在Gradle里面添加库工程 xUtils框架:compile 'org.xutils:xutils:3.3.40' FastJson:compile 'com.alibaba:fastjson:1.1.56.android' (我也不知道为什么显示出来的android是红色大写,应当是

利用Java编码北京PK10平台制作测试CSRF令牌验证的Web API

拙文是利用了Jmeter来测试北京PK10平台制作(www.1159880099.com)QQ1159880099 带有CSRF令牌验证的Web API:最近几天趁着项目不忙,练习了用编码的方式实现. 有了之前Jmeter脚本的基础,基本上难点也就在两个地方:获取CSRF令牌.Cookie的传递. 首先添加依赖,在POM.xml中添加以下内容: <!-- https:// mvnrepository.com/artifact/org.apache.httpcomponents/httpclien

北京赛车平台出租度富文本编辑器UEditor的改造

在Java项目中,做内容管理功能时,需要用到富文本编辑器北京赛车平台出租(www.1159880099.com)QQ1159880099,目前流行的富文本编辑器还是比较多的,因为项目中用的是百度的UEditor,所以对UEditor使用中的一些问题做个总结吧. 因为是Java项目所以使用的是只能选择jsp版本的UEditor,使用方式还是比较简单的,按照UEditor官方的文档来就好了. 首先说下踩过的坑,我项目一开始是用的war部署的方式,大家都知道war部署时是会解压到tomcat的weba

JavaScript实现时间戳转为pk10平台出租搭建

/**pk10平台出租搭建(企 娥:217 1793 408) [DateToTime 时间戳转换为日期] @param {[type]} unixTime [时间戳] @param {String} type [Y-m-d,Y-m-d H:i:s,Y/m/d,Y/m/d H:i:s,Y年m月d日,Y年m月d日 H:i:s]/function DateToTime(unixTime,type="Y-M-D H:i:s"){var date = new Date(unixTime 100