這篇文章將為大家詳細(xì)講解有關(guān)Spring MVC請(qǐng)求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
創(chuàng)新互聯(lián)專注于瑤海企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站開發(fā),商城網(wǎng)站開發(fā)。瑤海網(wǎng)站建設(shè)公司,為瑤海等地區(qū)提供建站服務(wù)。全流程定制網(wǎng)站開發(fā),專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,創(chuàng)新互聯(lián)專業(yè)和態(tài)度為您提供的服務(wù)
前提
前段時(shí)間在做一個(gè)對(duì)外的網(wǎng)關(guān)項(xiàng)目,涉及到加密和解密模塊,這里詳細(xì)分析解決方案和適用的場(chǎng)景。為了模擬真實(shí)的交互場(chǎng)景,先定制一下整個(gè)交互流程。第三方傳輸(包括請(qǐng)求和響應(yīng))數(shù)據(jù)報(bào)文包括三個(gè)部分:
1、timestamp,long類型,時(shí)間戳。
2、data,String類型,實(shí)際的業(yè)務(wù)請(qǐng)求數(shù)據(jù)轉(zhuǎn)化成的Json字符串再進(jìn)行加密得到的密文。
3、sign,簽名,生成規(guī)則算法偽代碼是SHA-256(data=xxx×tamp=11111),防篡改。
為了簡(jiǎn)單起見,加密和解密采用AES,對(duì)稱秘鑰為"throwable"。上面的場(chǎng)景和加解密例子僅僅是為了模擬真實(shí)場(chǎng)景,安全系數(shù)低,切勿直接用于生產(chǎn)環(huán)境。
現(xiàn)在還有一個(gè)地方要考慮,就是無法得知第三方如何提交請(qǐng)求數(shù)據(jù),假定都是采用POST的Http請(qǐng)求方法,提交報(bào)文的時(shí)候指定ContentType為application/json或者application/x-www-form-urlencoded,兩種ContentType提交方式的請(qǐng)求體是不相同的:
//application/x-www-form-urlencoded timestamp=xxxx&data=yyyyyy&sign=zzzzzzz //application/json {"timestamp":xxxxxx,"data":"yyyyyyyy","sign":"zzzzzzz"}
最后一個(gè)要考慮的地方是,第三方強(qiáng)制要求部分接口需要用明文進(jìn)行請(qǐng)求,在提供一些接口方法的時(shí)候,允許使用明文交互。總結(jié)一下就是要做到以下三點(diǎn):
1、需要加解密的接口請(qǐng)求參數(shù)要進(jìn)行解密,響應(yīng)結(jié)果要進(jìn)行加密。
2、不需要加解密的接口可以用明文請(qǐng)求。
3、兼容ContentType為application/json或者application/x-www-form-urlencoded兩種方式。
上面三種情況要同時(shí)兼容算是十分嚴(yán)苛的場(chǎng)景,在生產(chǎn)環(huán)境中可能也是極少情況下才遇到,不過還是能找到相對(duì)優(yōu)雅的解決方案。先定義兩個(gè)特定場(chǎng)景的接口:
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"}
兩個(gè)接口的ContentType不相同是為了故意復(fù)雜化場(chǎng)景,在下面的可取方案中,做法是把a(bǔ)pplication/x-www-form-urlencoded中的形式如xxx=yyy&aaa=bbb的表單參數(shù)和application/json中形式如{"key":"value"}的請(qǐng)求參數(shù)統(tǒng)一當(dāng)做application/json形式的參數(shù)處理,這樣的話,我們就可以直接在控制器方法中使用@RequestBody。
方案
我們首先基于上面說到的加解密方案,提供一個(gè)加解密工具類:
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); } }
注意為了簡(jiǎn)化加解密操作引入了apache的codec依賴:
<dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.11</version> </dependency>
上面的加解密過程中要注意兩點(diǎn):
1、加密后的結(jié)果是byte數(shù)組,要把二進(jìn)制轉(zhuǎn)化為十六進(jìn)制字符串。
2、解密的時(shí)候要把原始密文由十六進(jìn)制轉(zhuǎn)化為二進(jìn)制的byte數(shù)組。
上面兩點(diǎn)必須注意,否則會(huì)產(chǎn)生亂碼,這個(gè)和編碼相關(guān),具體可以看之前寫的一篇博客。
不推薦的方案
其實(shí)最暴力的方案是直接定制每個(gè)控制器的方法參數(shù)類型,因?yàn)槲覀兛梢院偷谌酱枭棠男┱?qǐng)求路徑需要加密,哪些是不需要加密,甚至哪些是application/x-www-form-urlencoded,哪些是application/json的請(qǐng)求,這樣我們可以通過大量的硬編碼達(dá)到最終的目標(biāo)。舉個(gè)例子:
@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×tamp=%d", model.getData(), model.getTimestamp()); String inSign = EncryptUtils.SINGLETON.sha(inRawSign); if (!inSign.equals(model.getSign())){ throw new IllegalArgumentException("驗(yàn)證參數(shù)簽名失敗!"); } //這里忽略實(shí)際的業(yè)務(wù)邏輯,簡(jiǎn)單設(shè)置返回的data為一個(gè)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×tamp=%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(); //這里忽略實(shí)際的業(yè)務(wù)邏輯 return ResponseEntity.ok(order); } }
這種做法能在短時(shí)間完成對(duì)應(yīng)的加解密功能,不需要加解密的接口不用引入相關(guān)的代碼即可。缺陷十分明顯,存在硬編碼、代碼冗余等問題,一旦接口增多,項(xiàng)目的維護(hù)難度大大提高。因此,這種做法是不可取的。
混合方案之Filter和SpringMVC的Http消息轉(zhuǎn)換器
這里先說一點(diǎn),這里是在SpringMVC中使用Filter。因?yàn)橐嫒輧煞NcontentType,我們需要做到幾點(diǎn):
1、修改請(qǐng)求頭的contentType為application/json。
2、修改請(qǐng)求體中的參數(shù),統(tǒng)一轉(zhuǎn)化為InputStream。
3、定制URL規(guī)則,區(qū)別需要加解密和不需要加解密的URL。
使用Filter有一個(gè)優(yōu)點(diǎn):不需要理解SpringMVC的流程,也不需要擴(kuò)展SpringMVC的相關(guān)組件。缺點(diǎn)也比較明顯:
1、如果需要區(qū)分加解密,只能通過URL規(guī)則進(jìn)行過濾。
2、需要加密的接口的SpringMVC控制器的返回參數(shù)必須是加密后的實(shí)體類,無法做到加密邏輯和業(yè)務(wù)邏輯完全拆分,也就是解密邏輯對(duì)接收的參數(shù)是無感知,但是加密邏輯對(duì)返回結(jié)果是有感知的。
PS:上面提到的幾個(gè)需要修改請(qǐng)求參數(shù)、請(qǐng)求頭等是因?yàn)樘厥鈭?chǎng)景的定制,所以如果無此場(chǎng)景可以直接看下面的"單純的Json請(qǐng)求參數(shù)和Json響應(yīng)結(jié)果"小節(jié)。流程大致如下:
編寫Filter的實(shí)現(xiàn)和HttpServletRequestWrapper的實(shí)現(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 { //這里忽略實(shí)際的業(yè)務(wù)邏輯,簡(jiǎn)單設(shè)置返回的data為一個(gè)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(); //這里忽略實(shí)際的業(yè)務(wù)邏輯 return ResponseEntity.ok(order); } }
這里可能有人有疑問,為什么不在Filter做加解密的操作?因?yàn)榭紤]到場(chǎng)景太特殊,要兼容兩種形式的表單提交參數(shù),如果在Filter做加解密操作,會(huì)影響到Controller的編碼,這就違反了全局加解密不影響到里層業(yè)務(wù)代碼的目標(biāo)。上面的Filter只會(huì)攔截URL滿足/e/*的請(qǐng)求,因此查詢接口/c/order/query不會(huì)受到影響。這里使用了標(biāo)識(shí)接口用于決定請(qǐng)求參數(shù)或者響應(yīng)結(jié)果是否需要加解密,也就是只需要在HttpMessageConverter中判斷請(qǐng)求參數(shù)的類型或者響應(yīng)結(jié)果的類型是否加解密標(biāo)識(shí)接口的子類:
@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×tamp=%d", in.getData(), in.getTimestamp()); String inSign; try { inSign = EncryptUtils.SINGLETON.sha(inRawSign); } catch (Exception e) { throw new IllegalArgumentException("驗(yàn)證參數(shù)簽名失敗!"); } if (!inSign.equals(in.getSign())) { throw new IllegalArgumentException("驗(yàn)證參數(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×tamp=%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); } } }
自實(shí)現(xiàn)的HttpMessageConverter主要需要判斷請(qǐng)求參數(shù)的類型和返回值的類型,從而判斷是否需要進(jìn)行加解密。
單純的Json請(qǐng)求參數(shù)和Json響應(yīng)結(jié)果的加解密處理最佳實(shí)踐
一般情況下,對(duì)接方的請(qǐng)求參數(shù)和響應(yīng)結(jié)果是完全規(guī)范統(tǒng)一使用Json(contentType指定為application/json,使用@RequestBody接收參數(shù)),那么所有的事情就會(huì)變得簡(jiǎn)單,因?yàn)椴恍枰紤]請(qǐng)求參數(shù)由xxx=yyy&aaa=bbb轉(zhuǎn)換為InputStream再交給SpringMVC處理,因此我們只需要提供一個(gè)MappingJackson2HttpMessageConverter子類實(shí)現(xiàn)(繼承它并且覆蓋對(duì)應(yīng)方法,添加加解密特性)。我們還是使用標(biāo)識(shí)接口用于決定請(qǐng)求參數(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×tamp=%d", in.getData(), in.getTimestamp()); String inSign; try { inSign = EncryptUtils.SINGLETON.sha(inRawSign); } catch (Exception e) { throw new IllegalArgumentException("驗(yàn)證參數(shù)簽名失敗!"); } if (!inSign.equals(in.getSign())) { throw new IllegalArgumentException("驗(yàn)證參數(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×tamp=%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); } } }
沒錯(cuò),代碼是拷貝上一節(jié)提供的HttpMessageConverter實(shí)現(xiàn),然后控制器方法的參數(shù)使用@RequestBody注解并且類型實(shí)現(xiàn)加解密標(biāo)識(shí)接口Encryptable即可,返回值的類型也需要實(shí)現(xiàn)加解密標(biāo)識(shí)接口Encryptable。這種做法可以讓控制器的代碼對(duì)加解密完全無感知。當(dāng)然,也可以不改變?cè)瓉淼腗appingJackson2HttpMessageConverter實(shí)現(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×tamp=%d", in.getData(), in.getTimestamp()); String inSign; try { inSign = EncryptUtils.SINGLETON.sha(inRawSign); } catch (Exception e) { throw new IllegalArgumentException("驗(yàn)證參數(shù)簽名失敗!"); } if (!inSign.equals(in.getSign())) { throw new IllegalArgumentException("驗(yàn)證參數(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×tamp=%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表單請(qǐng)求參數(shù)和Json響應(yīng)結(jié)果的加解密處理最佳實(shí)踐
一般情況下,對(duì)接方的請(qǐng)求參數(shù)完全采用application/x-www-form-urlencoded表單請(qǐng)求參數(shù)返回結(jié)果全部按照J(rèn)son接收,我們也可以通過一個(gè)HttpMessageConverter實(shí)現(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×tamp=%d", in.getData(), in.getTimestamp()); String inSign; try { inSign = EncryptUtils.SINGLETON.sha(inRawSign); } catch (Exception e) { throw new IllegalArgumentException("驗(yàn)證參數(shù)簽名失敗!"); } if (!inSign.equals(in.getSign())) { throw new IllegalArgumentException("驗(yàn)證參數(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×tamp=%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的實(shí)現(xiàn)可以參考o(jì)rg.springframework.http.converter.FormHttpMessageConverter。
小結(jié)
這篇文章強(qiáng)行復(fù)雜化了實(shí)際的情況(但是在實(shí)際中真的碰到過),一般情況下,現(xiàn)在流行使用Json進(jìn)行數(shù)據(jù)傳輸,在SpringMVC項(xiàng)目中,我們只需要針對(duì)性地改造MappingJackson2HttpMessageConverter即可(繼承并且添加特性),如果對(duì)SpringMVC的源碼相對(duì)熟悉的話,直接添加自定義的RequestBodyAdvice(RequestBodyAdviceAdapter)和ResponseBodyAdvice(JsonViewResponseBodyAdvice)實(shí)現(xiàn)也可以達(dá)到目的。至于為什么使用HttpMessageConverter做加解密功能,這里基于SpringMVC源碼的對(duì)請(qǐng)求參數(shù)處理的過程整理了一張?zhí)幚砹鞒虉D:
上面流程最核心的代碼可以看AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters和HandlerMethodArgumentResolverComposite#resolveArgument,畢竟源碼不會(huì)騙人??刂破鞣椒ǚ祷刂档奶幚砘谑菍?duì)稱的,閱讀起來也比較輕松。
關(guān)于“Spring MVC請(qǐng)求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。
標(biāo)題名稱:SpringMVC請(qǐng)求參數(shù)與響應(yīng)結(jié)果全局加密和解密的示例分析
網(wǎng)頁URL:http://aaarwkj.com/article34/iihgpe.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供云服務(wù)器、響應(yīng)式網(wǎng)站、關(guān)鍵詞優(yōu)化、動(dòng)態(tài)網(wǎng)站、網(wǎng)站營銷、網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)