SpringMVC請求參數和響應結果全局加密和解密


前提

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

  • 1、timestamp,long類型,時間戳。
  • 2、data,String類型,實際的業務請求數據轉化成的Json字符串再進行加密得到的密文。
  • 3、sign,簽名,生成規則算法偽代碼是SHA-256(data=xxx&timestamp=11111),防篡改。

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

現在還有一個地方要考慮,就是無法得知第三方如何提交請求數據,假定都是采用POST的Http請求方法,提交報文的時候指定ContentType為application/json或者application/x-www-form-urlencoded,兩種ContentType提交方式的請求體是不相同的:

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

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

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

  • 1、需要加解密的接口請求參數要進行解密,響應結果要進行加密。
  • 2、不需要加解密的接口可以用明文請求。
  • 3、兼容ContentType為application/json或者application/x-www-form-urlencoded兩種方式。

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

1、下單接口(加密)

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

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

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

兩個接口的ContentType不相同是為了故意復雜化場景,在下面的可取方案中,做法是把application/x-www-form-urlencoded中的形式如xxx=yyy&aaa=bbb的表單參數和application/json中形式如{"key":"value"}的請求參數統一當做application/json形式的參數處理,這樣的話,我們就可以直接在控制器方法中使用@RequestBody。

方案

我們首先基於上面說到的加解密方案,提供一個加解密工具類:

public enum EncryptUtils {

	/**
	 * SINGLETON
	 */
	SINGLETON;

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

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

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

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

注意為了簡化加解密操作引入了apache的codec依賴:

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

上面的加解密過程中要注意兩點:

  • 1、加密后的結果是byte數組,要把二進制轉化為十六進制字符串。
  • 2、解密的時候要把原始密文由十六進制轉化為二進制的byte數組。

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

不推薦的方案

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

@RestController
public class Controller1 {

	@Autowired
	private ObjectMapper objectMapper;

	@PostMapping(value = "/order/save",
			consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
			produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ResponseEntity<EncryptModel> saveOrder(@RequestParam(name = "sign") String sign,
												  @RequestParam(name = "timestamp") Long timestamp,
												  @RequestParam(name = "data") String data) throws Exception {
		EncryptModel model = new EncryptModel();
		model.setData(data);
		model.setTimestamp(timestamp);
		model.setSign(sign);
		String inRawSign = String.format("data=%s&timestamp=%d", model.getData(), model.getTimestamp());
		String inSign = EncryptUtils.SINGLETON.sha(inRawSign);
		if (!inSign.equals(model.getSign())){
			throw new IllegalArgumentException("驗證參數簽名失敗!");
		}
		//這里忽略實際的業務邏輯,簡單設置返回的data為一個map
		Map<String, Object> result = new HashMap<>(8);
		result.put("code", "200");
		result.put("message", "success");
		EncryptModel out = new EncryptModel();
		out.setTimestamp(System.currentTimeMillis());
		out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(result)));
		String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
		out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
		return ResponseEntity.ok(out);
	}

	@PostMapping(value = "/order/query",
			consumes = MediaType.APPLICATION_JSON_VALUE,
			produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ResponseEntity<Order>  queryOrder(@RequestBody User user){
		Order order = new Order();
		//這里忽略實際的業務邏輯
		return ResponseEntity.ok(order);
	}
}

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

混合方案之Filter和SpringMVC的Http消息轉換器

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

  • 1、修改請求頭的contentType為application/json。
  • 2、修改請求體中的參數,統一轉化為InputStream。
  • 3、定制URL規則,區別需要加解密和不需要加解密的URL。

使用Filter有一個優點:不需要理解SpringMVC的流程,也不需要擴展SpringMVC的相關組件。缺點也比較明顯:

  • 1、如果需要區分加解密,只能通過URL規則進行過濾。
  • 2、需要加密的接口的SpringMVC控制器的返回參數必須是加密后的實體類,無法做到加密邏輯和業務邏輯完全拆分,也就是解密邏輯對接收的參數是無感知,但是加密邏輯對返回結果是有感知的。

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

sp-ed-1

編寫Filter的實現和HttpServletRequestWrapper的實現:

//CustomEncryptFilter
@RequiredArgsConstructor
public class CustomEncryptFilter extends OncePerRequestFilter {

	private final ObjectMapper objectMapper;

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

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

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

//CustomEncryptHttpWrapper
public class CustomEncryptHttpWrapper extends HttpServletRequestWrapper {

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

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

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

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

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

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

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

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

			@Override
			public void setReadListener(ReadListener listener) {

			}

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

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

//CustomEncryptConfiguration
@Configuration
public class CustomEncryptConfiguration {

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

控制器代碼:

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


@Data
public class Order implements Encryptable{

	private Long userId;
}

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

@RequiredArgsConstructor
@RestController
public class Controller {

	private final ObjectMapper objectMapper;

	@PostMapping(value = "/e/order/save",
			consumes = MediaType.APPLICATION_JSON_VALUE,
			produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public EncryptResponse<Order> saveOrder(@RequestBody Order order) throws Exception {
		//這里忽略實際的業務邏輯,簡單設置返回的data為一個map
		EncryptResponse<Order> response = new EncryptResponse<>();
		response.setCode(200);
		response.setData(order);
		return response;
	}

	@PostMapping(value = "/c/order/query",
			consumes = MediaType.APPLICATION_JSON_VALUE,
			produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
	public ResponseEntity<Order> queryOrder(@RequestBody User user) {
		Order order = new Order();
		//這里忽略實際的業務邏輯
		return ResponseEntity.ok(order);
	}
}

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

@RequiredArgsConstructor
public class CustomEncryptHttpMessageConverter extends MappingJackson2HttpMessageConverter {

	private final ObjectMapper objectMapper;

	@Override
	protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {
		if (Encryptable.class.isAssignableFrom(clazz)) {
			EncryptModel in = objectMapper.readValue(StreamUtils.copyToByteArray(inputMessage.getBody()), EncryptModel.class);
			String inRawSign = String.format("data=%s&timestamp=%d", in.getData(), in.getTimestamp());
			String inSign;
			try {
				inSign = EncryptUtils.SINGLETON.sha(inRawSign);
			} catch (Exception e) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			if (!inSign.equals(in.getSign())) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			try {
				return objectMapper.readValue(EncryptUtils.SINGLETON.decryptByAes(in.getData()), clazz);
			} catch (Exception e) {
				throw new IllegalArgumentException("解密失敗!");
			}
		} else {
			return super.readInternal(clazz, inputMessage);
		}
	}

	@Override
	protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		Class<?> clazz = (Class) type;
		if (Encryptable.class.isAssignableFrom(clazz)) {
			EncryptModel out = new EncryptModel();
			out.setTimestamp(System.currentTimeMillis());
			try {
				out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(object)));
				String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
				out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
			} catch (Exception e) {
				throw new IllegalArgumentException("參數簽名失敗!");
			}
			super.writeInternal(out, type, outputMessage);
		} else {
			super.writeInternal(object, type, outputMessage);
		}
	}
}

自實現的HttpMessageConverter主要需要判斷請求參數的類型和返回值的類型,從而判斷是否需要進行加解密。

單純的Json請求參數和Json響應結果的加解密處理最佳實踐

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

@RequiredArgsConstructor
public class CustomEncryptHttpMessageConverter extends MappingJackson2HttpMessageConverter {

	private final ObjectMapper objectMapper;

	@Override
	protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException {
		if (Encryptable.class.isAssignableFrom(clazz)) {
			EncryptModel in = objectMapper.readValue(StreamUtils.copyToByteArray(inputMessage.getBody()), EncryptModel.class);
			String inRawSign = String.format("data=%s&timestamp=%d", in.getData(), in.getTimestamp());
			String inSign;
			try {
				inSign = EncryptUtils.SINGLETON.sha(inRawSign);
			} catch (Exception e) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			if (!inSign.equals(in.getSign())) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			try {
				return objectMapper.readValue(EncryptUtils.SINGLETON.decryptByAes(in.getData()), clazz);
			} catch (Exception e) {
				throw new IllegalArgumentException("解密失敗!");
			}
		} else {
			return super.readInternal(clazz, inputMessage);
		}
	}

	@Override
	protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		Class<?> clazz = (Class) type;
		if (Encryptable.class.isAssignableFrom(clazz)) {
			EncryptModel out = new EncryptModel();
			out.setTimestamp(System.currentTimeMillis());
			try {
				out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(object)));
				String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
				out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
			} catch (Exception e) {
				throw new IllegalArgumentException("參數簽名失敗!");
			}
			super.writeInternal(out, type, outputMessage);
		} else {
			super.writeInternal(object, type, outputMessage);
		}
	}
}

沒錯,代碼是拷貝上一節提供的HttpMessageConverter實現,然后控制器方法的參數使用@RequestBody注解並且類型實現加解密標識接口Encryptable即可,返回值的類型也需要實現加解密標識接口Encryptable。這種做法可以讓控制器的代碼對加解密完全無感知。當然,也可以不改變原來的MappingJackson2HttpMessageConverter實現,使用RequestBodyAdvice和ResponseBodyAdvice完成相同的功能:

@RequiredArgsConstructor
public class CustomRequestBodyAdvice extends RequestBodyAdviceAdapter {

	private final ObjectMapper objectMapper;

	@Override
	public boolean supports(MethodParameter methodParameter, Type targetType,
							Class<? extends HttpMessageConverter<?>> converterType) {
		Class<?> clazz = (Class) targetType;
		return Encryptable.class.isAssignableFrom(clazz);
	}

	@Override
	public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
										   Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
		Class<?> clazz = (Class) targetType;
		if (Encryptable.class.isAssignableFrom(clazz)) {
			String content = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
			EncryptModel in = objectMapper.readValue(content, EncryptModel.class);
			String inRawSign = String.format("data=%s&timestamp=%d", in.getData(), in.getTimestamp());
			String inSign;
			try {
				inSign = EncryptUtils.SINGLETON.sha(inRawSign);
			} catch (Exception e) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			if (!inSign.equals(in.getSign())) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			ByteArrayInputStream inputStream = new ByteArrayInputStream(in.getData().getBytes(Charset.forName("UTF-8")));
			return new MappingJacksonInputMessage(inputStream, inputMessage.getHeaders());
		} else {
			return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
		}
	}
}

@RequiredArgsConstructor
public class CustomResponseBodyAdvice extends JsonViewResponseBodyAdvice {

	private final ObjectMapper objectMapper;

	@Override
	public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
		Class<?> parameterType = returnType.getParameterType();
		return Encryptable.class.isAssignableFrom(parameterType);
	}

	@Override
	protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType,
										   MethodParameter returnType, ServerHttpRequest request,
										   ServerHttpResponse response) {
		Class<?> parameterType = returnType.getParameterType();
		if (Encryptable.class.isAssignableFrom(parameterType)) {
			EncryptModel out = new EncryptModel();
			out.setTimestamp(System.currentTimeMillis());
			try {
				out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(bodyContainer.getValue())));
				String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
				out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
				out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
			} catch (Exception e) {
				throw new IllegalArgumentException("參數簽名失敗!");
			}
		} else {
			super.beforeBodyWriteInternal(bodyContainer, contentType, returnType, request, response);
		}
	}
}

單純的application/x-www-form-urlencoded表單請求參數和Json響應結果的加解密處理最佳實踐

一般情況下,對接方的請求參數完全采用application/x-www-form-urlencoded表單請求參數返回結果全部按照Json接收,我們也可以通過一個HttpMessageConverter實現就完成加解密模塊。

public class FormHttpMessageConverter implements HttpMessageConverter<Object> {

	private final List<MediaType> mediaTypes;
	private final ObjectMapper objectMapper;

	public FormHttpMessageConverter(ObjectMapper objectMapper) {
		this.objectMapper = objectMapper;
		this.mediaTypes = new ArrayList<>(1);
		this.mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
	}

	@Override
	public boolean canRead(Class<?> clazz, MediaType mediaType) {
		return Encryptable.class.isAssignableFrom(clazz) && mediaTypes.contains(mediaType);
	}

	@Override
	public boolean canWrite(Class<?> clazz, MediaType mediaType) {
		return Encryptable.class.isAssignableFrom(clazz) && mediaTypes.contains(mediaType);
	}

	@Override
	public List<MediaType> getSupportedMediaTypes() {
		return mediaTypes;
	}

	@Override
	public Object read(Class<?> clazz, HttpInputMessage inputMessage) throws
			IOException, HttpMessageNotReadableException {
		if (Encryptable.class.isAssignableFrom(clazz)) {
			String content = StreamUtils.copyToString(inputMessage.getBody(), Charset.forName("UTF-8"));
			EncryptModel in = objectMapper.readValue(content, EncryptModel.class);
			String inRawSign = String.format("data=%s&timestamp=%d", in.getData(), in.getTimestamp());
			String inSign;
			try {
				inSign = EncryptUtils.SINGLETON.sha(inRawSign);
			} catch (Exception e) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			if (!inSign.equals(in.getSign())) {
				throw new IllegalArgumentException("驗證參數簽名失敗!");
			}
			try {
				return objectMapper.readValue(EncryptUtils.SINGLETON.decryptByAes(in.getData()), clazz);
			} catch (Exception e) {
				throw new IllegalArgumentException("解密失敗!");
			}
		} else {
			MediaType contentType = inputMessage.getHeaders().getContentType();
			Charset charset = (contentType != null && contentType.getCharset() != null ?
					contentType.getCharset() : Charset.forName("UTF-8"));
			String body = StreamUtils.copyToString(inputMessage.getBody(), charset);

			String[] pairs = StringUtils.tokenizeToStringArray(body, "&");
			MultiValueMap<String, String> result = new LinkedMultiValueMap<>(pairs.length);
			for (String pair : pairs) {
				int idx = pair.indexOf('=');
				if (idx == -1) {
					result.add(URLDecoder.decode(pair, charset.name()), null);
				} else {
					String name = URLDecoder.decode(pair.substring(0, idx), charset.name());
					String value = URLDecoder.decode(pair.substring(idx + 1), charset.name());
					result.add(name, value);
				}
			}
			return result;
		}
	}

	@Override
	public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		Class<?> clazz = o.getClass();
		if (Encryptable.class.isAssignableFrom(clazz)) {
			EncryptModel out = new EncryptModel();
			out.setTimestamp(System.currentTimeMillis());
			try {
				out.setData(EncryptUtils.SINGLETON.encryptByAes(objectMapper.writeValueAsString(o)));
				String rawSign = String.format("data=%s&timestamp=%d", out.getData(), out.getTimestamp());
				out.setSign(EncryptUtils.SINGLETON.sha(rawSign));
				StreamUtils.copy(objectMapper.writeValueAsString(out)
						.getBytes(Charset.forName("UTF-8")), outputMessage.getBody());
			} catch (Exception e) {
				throw new IllegalArgumentException("參數簽名失敗!");
			}
		} else {
			String out = objectMapper.writeValueAsString(o);
			StreamUtils.copy(out.getBytes(Charset.forName("UTF-8")), outputMessage.getBody());
		}
	}
}

上面的HttpMessageConverter的實現可以參考org.springframework.http.converter.FormHttpMessageConverter。

小結

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

sp-ed-2

上面流程最核心的代碼可以看AbstractMessageConverterMethodArgumentResolver#readWithMessageConvertersHandlerMethodArgumentResolverComposite#resolveArgument,畢竟源碼不會騙人。控制器方法返回值的處理是基本對稱的,閱讀起來也比較輕松。

參考資料:

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

(本文完 c-d-4)

原文鏈接

圖片掛了可以看原文,一般是雲存儲換了導致...

技術公眾號(《Throwable文摘》),不定期推送筆者原創技術文章(絕不抄襲或者轉載):

娛樂公眾號(《天天沙雕》),甄選奇趣沙雕圖文和視頻不定期推送,緩解生活工作壓力:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM