SpringMVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析

這篇文章將為大家詳細(xì)講解有關(guān)Spring MVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。

創(chuàng)新互聯(lián)專注于瑤海企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站開發(fā),商城網(wǎng)站開發(fā)?,幒>W(wǎng)站建設(shè)公司,為瑤海等地區(qū)提供建站服務(wù)。全流程定制網(wǎng)站開發(fā),專業(yè)設(shè)計,全程項目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)

前提

前段時間在做一個對外的網(wǎng)關(guān)項目,涉及到加密和解密模塊,這里詳細(xì)分析解決方案和適用的場景。為了模擬真實的交互場景,先定制一下整個交互流程。第三方傳輸(包括請求和響應(yīng))數(shù)據(jù)報文包括三個部分:

1、timestamp,long類型,時間戳。

2、data,String類型,實際的業(yè)務(wù)請求數(shù)據(jù)轉(zhuǎn)化成的Json字符串再進(jìn)行加密得到的密文。

3、sign,簽名,生成規(guī)則算法偽代碼是SHA-256(data=xxx&timestamp=11111),防篡改。

為了簡單起見,加密和解密采用AES,對稱秘鑰為"throwable"。上面的場景和加解密例子僅僅是為了模擬真實場景,安全系數(shù)低,切勿直接用于生產(chǎn)環(huán)境。

現(xiàn)在還有一個地方要考慮,就是無法得知第三方如何提交請求數(shù)據(jù),假定都是采用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"}

最后一個要考慮的地方是,第三方強(qiáng)制要求部分接口需要用明文進(jìn)行請求,在提供一些接口方法的時候,允許使用明文交互。總結(jié)一下就是要做到以下三點:

1、需要加解密的接口請求參數(shù)要進(jìn)行解密,響應(yīng)結(jié)果要進(jìn)行加密。

2、不需要加解密的接口可以用明文請求。

3、兼容ContentType為application/json或者application/x-www-form-urlencoded兩種方式。

上面三種情況要同時兼容算是十分嚴(yán)苛的場景,在生產(chǎn)環(huán)境中可能也是極少情況下才遇到,不過還是能找到相對優(yōu)雅的解決方案。先定義兩個特定場景的接口:

1、下單接口(加密)

  • URL:/order/save

  • HTTP METHOD:POST

  • ContentType:application/x-www-form-urlencoded

  • 原始參數(shù):orderId=yyyyyyyyy&userId=xxxxxxxxx&amount=zzzzzzzzz

  • 加密參數(shù):timestamp=xxxx&data=yyyyyy&sign=zzzzzzz

2、訂單查詢接口(明文)

  • URL:/order/query

  • ContentType:application/json

  • HTTP METHOD:POST

  • 原始參數(shù):{"userId":"xxxxxxxx"}

兩個接口的ContentType不相同是為了故意復(fù)雜化場景,在下面的可取方案中,做法是把a(bǔ)pplication/x-www-form-urlencoded中的形式如xxx=yyy&aaa=bbb的表單參數(shù)和application/json中形式如{"key":"value"}的請求參數(shù)統(tǒng)一當(dāng)做application/json形式的參數(shù)處理,這樣的話,我們就可以直接在控制器方法中使用@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、加密后的結(jié)果是byte數(shù)組,要把二進(jìn)制轉(zhuǎn)化為十六進(jìn)制字符串。

2、解密的時候要把原始密文由十六進(jìn)制轉(zhuǎn)化為二進(jìn)制的byte數(shù)組。

上面兩點必須注意,否則會產(chǎn)生亂碼,這個和編碼相關(guān),具體可以看之前寫的一篇博客。

不推薦的方案

其實最暴力的方案是直接定制每個控制器的方法參數(shù)類型,因為我們可以和第三方磋商哪些請求路徑需要加密,哪些是不需要加密,甚至哪些是application/x-www-form-urlencoded,哪些是application/json的請求,這樣我們可以通過大量的硬編碼達(dá)到最終的目標(biāo)。舉個例子:

@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("驗證參數(shù)簽名失敗!");
  }
  //這里忽略實際的業(yè)務(wù)邏輯,簡單設(shè)置返回的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();
  //這里忽略實際的業(yè)務(wù)邏輯
  return ResponseEntity.ok(order);
 }
}

這種做法能在短時間完成對應(yīng)的加解密功能,不需要加解密的接口不用引入相關(guān)的代碼即可。缺陷十分明顯,存在硬編碼、代碼冗余等問題,一旦接口增多,項目的維護(hù)難度大大提高。因此,這種做法是不可取的。

混合方案之Filter和SpringMVC的Http消息轉(zhuǎn)換器

這里先說一點,這里是在SpringMVC中使用Filter。因為要兼容兩種contentType,我們需要做到幾點:

1、修改請求頭的contentType為application/json。

2、修改請求體中的參數(shù),統(tǒng)一轉(zhuǎn)化為InputStream。

3、定制URL規(guī)則,區(qū)別需要加解密和不需要加解密的URL。

使用Filter有一個優(yōu)點:不需要理解SpringMVC的流程,也不需要擴(kuò)展SpringMVC的相關(guān)組件。缺點也比較明顯:

1、如果需要區(qū)分加解密,只能通過URL規(guī)則進(jìn)行過濾。

2、需要加密的接口的SpringMVC控制器的返回參數(shù)必須是加密后的實體類,無法做到加密邏輯和業(yè)務(wù)邏輯完全拆分,也就是解密邏輯對接收的參數(shù)是無感知,但是加密邏輯對返回結(jié)果是有感知的。

PS:上面提到的幾個需要修改請求參數(shù)、請求頭等是因為特殊場景的定制,所以如果無此場景可以直接看下面的"單純的Json請求參數(shù)和Json響應(yīng)結(jié)果"小節(jié)。流程大致如下:

Spring MVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析

編寫Filter的實現(xiàn)和HttpServletRequestWrapper的實現(xiàn):

//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 {
  //這里忽略實際的業(yè)務(wù)邏輯,簡單設(shè)置返回的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();
  //這里忽略實際的業(yè)務(wù)邏輯
  return ResponseEntity.ok(order);
 }
}

這里可能有人有疑問,為什么不在Filter做加解密的操作?因為考慮到場景太特殊,要兼容兩種形式的表單提交參數(shù),如果在Filter做加解密操作,會影響到Controller的編碼,這就違反了全局加解密不影響到里層業(yè)務(wù)代碼的目標(biāo)。上面的Filter只會攔截URL滿足/e/*的請求,因此查詢接口/c/order/query不會受到影響。這里使用了標(biāo)識接口用于決定請求參數(shù)或者響應(yīng)結(jié)果是否需要加解密,也就是只需要在HttpMessageConverter中判斷請求參數(shù)的類型或者響應(yīng)結(jié)果的類型是否加解密標(biāo)識接口的子類:

@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("驗證參數(shù)簽名失敗!");
   }
   if (!inSign.equals(in.getSign())) {
    throw new IllegalArgumentException("驗證參數(shù)簽名失敗!");
   }
   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("參數(shù)簽名失敗!");
   }
   super.writeInternal(out, type, outputMessage);
  } else {
   super.writeInternal(object, type, outputMessage);
  }
 }
}

自實現(xiàn)的HttpMessageConverter主要需要判斷請求參數(shù)的類型和返回值的類型,從而判斷是否需要進(jìn)行加解密。

單純的Json請求參數(shù)和Json響應(yīng)結(jié)果的加解密處理最佳實踐

一般情況下,對接方的請求參數(shù)和響應(yīng)結(jié)果是完全規(guī)范統(tǒng)一使用Json(contentType指定為application/json,使用@RequestBody接收參數(shù)),那么所有的事情就會變得簡單,因為不需要考慮請求參數(shù)由xxx=yyy&aaa=bbb轉(zhuǎn)換為InputStream再交給SpringMVC處理,因此我們只需要提供一個MappingJackson2HttpMessageConverter子類實現(xiàn)(繼承它并且覆蓋對應(yīng)方法,添加加解密特性)。我們還是使用標(biāo)識接口用于決定請求參數(shù)或者響應(yīng)結(jié)果是否需要加解密:

@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("驗證參數(shù)簽名失敗!");
   }
   if (!inSign.equals(in.getSign())) {
    throw new IllegalArgumentException("驗證參數(shù)簽名失敗!");
   }
   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("參數(shù)簽名失敗!");
   }
   super.writeInternal(out, type, outputMessage);
  } else {
   super.writeInternal(object, type, outputMessage);
  }
 }
}

沒錯,代碼是拷貝上一節(jié)提供的HttpMessageConverter實現(xiàn),然后控制器方法的參數(shù)使用@RequestBody注解并且類型實現(xiàn)加解密標(biāo)識接口Encryptable即可,返回值的類型也需要實現(xiàn)加解密標(biāo)識接口Encryptable。這種做法可以讓控制器的代碼對加解密完全無感知。當(dāng)然,也可以不改變原來的MappingJackson2HttpMessageConverter實現(xiàn),使用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("驗證參數(shù)簽名失敗!");
   }
   if (!inSign.equals(in.getSign())) {
    throw new IllegalArgumentException("驗證參數(shù)簽名失敗!");
   }
   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("參數(shù)簽名失敗!");
   }
  } else {
   super.beforeBodyWriteInternal(bodyContainer, contentType, returnType, request, response);
  }
 }
}

單純的application/x-www-form-urlencoded表單請求參數(shù)和Json響應(yīng)結(jié)果的加解密處理最佳實踐

一般情況下,對接方的請求參數(shù)完全采用application/x-www-form-urlencoded表單請求參數(shù)返回結(jié)果全部按照J(rèn)son接收,我們也可以通過一個HttpMessageConverter實現(xiàn)就完成加解密模塊。

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("驗證參數(shù)簽名失敗!");
   }
   if (!inSign.equals(in.getSign())) {
    throw new IllegalArgumentException("驗證參數(shù)簽名失敗!");
   }
   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("參數(shù)簽名失敗!");
   }
  } else {
   String out = objectMapper.writeValueAsString(o);
   StreamUtils.copy(out.getBytes(Charset.forName("UTF-8")), outputMessage.getBody());
  }
 }
}

上面的HttpMessageConverter的實現(xiàn)可以參考o(jì)rg.springframework.http.converter.FormHttpMessageConverter。

小結(jié)

這篇文章強(qiáng)行復(fù)雜化了實際的情況(但是在實際中真的碰到過),一般情況下,現(xiàn)在流行使用Json進(jìn)行數(shù)據(jù)傳輸,在SpringMVC項目中,我們只需要針對性地改造MappingJackson2HttpMessageConverter即可(繼承并且添加特性),如果對SpringMVC的源碼相對熟悉的話,直接添加自定義的RequestBodyAdvice(RequestBodyAdviceAdapter)和ResponseBodyAdvice(JsonViewResponseBodyAdvice)實現(xiàn)也可以達(dá)到目的。至于為什么使用HttpMessageConverter做加解密功能,這里基于SpringMVC源碼的對請求參數(shù)處理的過程整理了一張?zhí)幚砹鞒虉D:

Spring MVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析

上面流程最核心的代碼可以看AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters和HandlerMethodArgumentResolverComposite#resolveArgument,畢竟源碼不會騙人??刂破鞣椒ǚ祷刂档奶幚砘谑菍ΨQ的,閱讀起來也比較輕松。

關(guān)于“Spring MVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,使各位可以學(xué)到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。

標(biāo)題名稱:SpringMVC請求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析
網(wǎng)頁URL:http://bm7419.com/article34/iihgpe.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供云服務(wù)器響應(yīng)式網(wǎng)站、關(guān)鍵詞優(yōu)化、動態(tài)網(wǎng)站、網(wǎng)站營銷、網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站制作