最近要开发一个对接第三方的平台,双方采用的是非对称加密(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 来进行处理,方法其实有很多,能解决问题就是好方法
致此 就解决了这个问题,贴中省略了很多代码,有些说法可能不太正确,仅供参考