天天看點

平台統一加解密處理方式

最近要開發一個對接第三方的平台,雙方采用的是非對稱加密(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 來進行處理,方法其實有很多,能解決問題就是好方法

緻此 就解決了這個問題,貼中省略了很多代碼,有些說法可能不太正确,僅供參考

繼續閱讀