最近要開發一個對接第三方的平台,雙方采用的是非對稱加密(RSA),由于雙方發送的封包與傳回結果都才去密文的形式,是以第一時間我就想到用AOP去進行統一處理,當然處理方法有很多這裡我采用一個過濾器來進行統一處理的;
接口的請求方法統一為POST ,由于request.getInputStream()是不可複用的,而我的需求又需要複用請求裡的參數,是以首先我對request和response進行了封裝;
public class RequestWrapper extends HttpServletRequestWrapper{
private final byte[] body;
public RequestWrapper(HttpServletRequest request) throws IOException{
super(request);
this.body = IOUtils.toString(request.getInputStream(), Charset.forName("UTF-8")).getBytes(Charset.forName("UTF-8"));
}
public RequestWrapper(HttpServletRequest request, byte[] body) throws IOException{
super(request);
this.body = body;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
};
}
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream bytes = new ByteArrayOutputStream();
private HttpServletResponse response;
private PrintWriter pwrite;
public ResponseWrapper(HttpServletResponse response) {
super(response);
this.response = response;
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
// 将資料寫到 byte 中
return new MyServletOutputStream(bytes);
}
@Override
public PrintWriter getWriter() {
try {
pwrite = new PrintWriter(new OutputStreamWriter(bytes, "utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return pwrite;
}
public String getResult() {
if (null != pwrite) {
pwrite.close();
return new String(bytes.toByteArray(), Charset.forName("UTF-8"));
}
if (null != bytes) {
try {
bytes.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
return new String(bytes.toByteArray(),Charset.forName("UTF-8"));
}
class MyServletOutputStream extends ServletOutputStream {
private ByteArrayOutputStream ostream;
public MyServletOutputStream(ByteArrayOutputStream ostream) {
this.ostream = ostream;
}
@Override
public void write(int b) throws IOException {
// 将資料寫到 stream 中
ostream.write(b);
}
}
}
過濾器中我的處理邏輯(從request裡把加密的參數讀出來 用私鑰解密 之後用第三方的公鑰驗簽,驗簽成功之後,将明文資訊 重新寫入到自己封裝的request body裡;傳回結果加密 也是同樣的方式 擷取明文傳回值 加密之後 重新寫會response):
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
String merchantPubKey = null;
String platPrivateKey = null;
HttpServletRequest request = (HttpServletRequest) servletRequest;
try {
String paramContext= IOUtils.toString(request.getInputStream(), Charset.forName("UTF-8"));
//參數為空無需解密 驗簽 直接跳過
if(StringUtils.isBlank(paramContext)){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String keyStorePath = WebConfig.getStorePath();
String keyPassword = WebConfig.getStorePassword();
String keyAlias = WebConfig.getCerAlias();
platPrivateKey = RSAUtils.getPrivateKey(keyStorePath,keyAlias,keyPassword);
String decryptBody = RSAUtils.decode(paramContext, platPrivateKey);
JSONObject params = JSONObject.parseObject(decryptBody);
if(params != null){
Integer merchantId = params.getInteger(MERCHANTID);
String content = params.getString(BODY);
String sign = params.getString(SIGN);
merchantPubKey = "pubKEY";
if(StringUtils.isNotBlank(merchantPubKey)){
boolean verify = RSAUtils.verify(content,sign,merchantPubKey);
System.out.println("解密請求參數:" + merchantId+","+content+","+sign);
System.out.println("驗簽請求參數:" + verify);
if(verify){
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper(request,(content.getBytes(Charset.forName("UTF-8"))));
requestWrapper.setAttribute(MERCHANTID,merchantId);
requestWrapper.setAttribute(CONTEXT,content);
}
}
}
}
} catch (Exception e) {
}
HttpServletResponse response = (HttpServletResponse) servletResponse;
ResponseWrapper myResponse = new ResponseWrapper(response);
if (null == requestWrapper) {
filterChain.doFilter(servletRequest, myResponse );
} else {
filterChain.doFilter(requestWrapper, myResponse );
}
//對傳回資料進行加密
PrintWriter out = servletResponse.getWriter();
try {
//取傳回的json串
String result = myResponse.getResult();
//加密
String encryptStr = signAndEncrypt(result,platPrivateKey,merchantPubKey);
out.write(encryptStr);
} catch (Exception e) {
logger.error( "doFilter", e);
} finally {
out.flush();
out.close();
}
}
以上的實作方式已經能解決這個問題了;但是這種方式僅限于請求時請求的 Content-type=application/json;而如果對方是form表單送出的參數類型Content-type=application/x-www-form-urlencoded 此時将無法從 request.getInputStream()中擷取參數;以上處理邏輯也就出現瑕疵;而最終商定的方式 也是采用key=value這種參數類型請求的;是以對之前的邏輯進行了優化
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
String merchantPubKey = null;
String platPrivateKey = null;
HttpServletRequest request = (HttpServletRequest) servletRequest;
try {
String paramMerchantId = request.getParameter(MERCHANTID);
String paramContext = request.getParameter(CONTEXT);
//參數為空無需解密 驗簽 直接跳過
if(StringUtils.isBlank(paramContext)){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String keyStorePath = WebConfig.getStorePath();
String keyPassword = WebConfig.getStorePassword();
String keyAlias = WebConfig.getCerAlias();
platPrivateKey = RSAUtils.getPrivateKey(keyStorePath,keyAlias,keyPassword);
String decryptBody = RSAUtils.decode(paramContext, platPrivateKey);
JSONObject params = JSONObject.parseObject(decryptBody);
if(params != null){
Integer merchantId = params.getInteger(MERCHANTID);
String content = params.getString(BODY);
String sign = params.getString(SIGN);
merchantPubKey = "";
if(StringUtils.isNotBlank(merchantPubKey)){
boolean verify = RSAUtils.verify(content,sign,merchantPubKey);
System.out.println("解密請求參數:" + merchantId+","+content+","+sign);
System.out.println("驗簽請求參數:" + verify);
if(verify){
if (servletRequest instanceof HttpServletRequest) {
requestWrapper = new RequestWrapper(request,(content.getBytes(Charset.forName("UTF-8"))));
requestWrapper.setAttribute(MERCHANTID,merchantId);
requestWrapper.setAttribute(CONTEXT,content);
}
}
}
}
} catch (Exception e) {
}
if (null == requestWrapper) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
}
這中處理并沒有對傳回值進行統一處理(加密)而是交由開發人員自己根據實際業務傳回進行手動處理;當然如果所有傳回值都需要加密 可以進行統一處理;拉回上述問題,Content-type=application/x-www-form-urlencoded 這種請求方式 @RequestBody則失效;故需要自己去寫一個參數解析器來對自己的參數類型做解析 來實作類似于spring 的RequestResponseBodyMethodProcessor提供的功能;我自己的解析器如下:
public class MyRequestBodyMethodArgumentResolver implements HandlerMethodArgumentResolver {
private static final String CONTEXT = "context";
/**
* 判斷是否支援要轉換的參數類型
*
* @param parameter
* @return
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(MyRequestBody.class)) {
return true;
}
return false;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String body = getRequestBody(webRequest);
Object val = null;
try {
System.err.println("參數解析器:" + body);
Class type = parameter.getParameterAnnotation(MyRequestBody.class).type();
val = new Gson().fromJson(body, type);
if (parameter.getParameterAnnotation(MyRequestBody.class).required() && val == null) {
throw new Exception(parameter.getParameterAnnotation(MyRequestBody.class).type() + "不能為空");
}
} catch (Exception e) {
e.printStackTrace();
}
return val;
}
private String getRequestBody(NativeWebRequest webRequest) {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
String jsonBody = (String) servletRequest.getAttribute(CONTEXT);
if (jsonBody == null) {
try {
jsonBody = IOUtils.toString(servletRequest.getInputStream());
servletRequest.setAttribute(CONTEXT, jsonBody);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return jsonBody;
}
}
spring xml 配置為(其實隻需要在你使用的HandlerAdapter下增加自定義的解析器就可以了,本身spring就預設增加了許多解析器,具體可以參看源碼 RequestMappingHandlerAdapter):
<mvc:annotation-driven conversion-service="conversionService">
<mvc:argument-resolvers>
<bean class="包名.MymRequestBodyMethodArgumentResolver"/>
</mvc:argument-resolvers>
</mvc:annotation-driven>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=UTF-8</value>
<value>text/html;charset=UTF-8</value>
<value>application/json;charset=UTF-8</value>
<value>application/xml;charset=UTF-8</value>
</list>
</property>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
</list>
</property>
<property name="customArgumentResolvers" >
<list>
<bean class="com.jimubox.ext.JmRequestBodyMethodArgumentResolver"/>
</list>
</property>
</bean>
controller 方法應用
@RequestMapping("/postData")
@ResponseBody
@APIRightAuth(APIRight.APIs4Demo)
public String postData(@MyRequestBody(type = DemoReqModel.class) DemoReqModel demoReqModel, HttpServletRequest request){
DemoRespModel demoRespModel = new DemoRespModel();
try{
System.out.println("參數為:"+ JSONObject.toJSONString(demoReqModel));
demoRespModel.setResult("請求成功");
CommonOutputModel commonOutputModel = new CommonOutputModel();
commonOutputModel.setMerchantId(demoReqModel.getTransInput().getMerchantId());
commonOutputModel.setOrderId(demoReqModel.getTransInput().getOrderId());
commonOutputModel.setStatus(ResponseStatusConstant.SUCCESS);
demoRespModel.setTransOutput(commonOutputModel);
}catch(Exception e){
e.printStackTrace();
demoRespModel.setTransOutput(InternalServerError(demoReqModel));
}
return signAndEncrypt(demoRespModel,demoReqModel.getTransInput().getMerchantId());
}
傳回值的處理其實還可以自己去實作HandlerMethodReturnValueHandler 來進行處理,方法其實有很多,能解決問題就是好方法
緻此 就解決了這個問題,貼中省略了很多代碼,有些說法可能不太正确,僅供參考