美文网首页Spring
在过滤器中实现修改http请求体和响应体

在过滤器中实现修改http请求体和响应体

作者: 砒霜拌辣椒 | 来源:发表于2020-07-27 19:49 被阅读0次

    在一些业务场景中,需要对http的请求体和响应体做加解密的操作,如果在controller中来调用加解密函数,会增加代码的耦合度,同时也会增加调试的难度。

    参考spring中http请求的链路,选择过滤器来对请求和响应做加解密的调用。只需要在过滤器中对符合条件的url做拦截处理即可。

    http处理链

    1、启动类配置注解

    新增注解@ServletComponentScan

    @SpringBootApplication
    @ServletComponentScan
    public class HttpdecryptApplication {
        public static void main(String[] args) {
            SpringApplication.run(HttpdecryptApplication.class, args);
        }
    }
    

    2、过滤器实现

    处理逻辑:

    1. 从servlet中读取原请求体(密文)。
    2. 调用解密函数获得明文。
    3. 构建新的请求对象,包装修改后的请求体(明文)。
    4. 构建新的响应对象,调用链调用应用层获得响应。
    5. 从新的响应对象中获得响应体(明文)。
    6. 调用加密函数对响应体进行加密。
    7. 用原响应对象的输出流,将加密后的密文响应体输出。

    用Base64算法做加解密示例

    @WebFilter(urlPatterns = {"/decrypt/*"}, filterName = "decryptFilter")
    @Slf4j
    public class DecryptFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            /**
             * 1.原请求/响应对象强转
             */
            HttpServletRequest originalRequest = (HttpServletRequest) request;
            HttpServletResponse originalResponse = (HttpServletResponse) response;
    
            /**
             * 2.读取原请求体(密文),执行修改请求体函数得到修改后的请求体(明文),然后构建新的请求对象(包含修改后的请求体)
             */
            String originalRequestBody = ServletUtil.readRequestBody(originalRequest); // 读取原请求体(密文)
            String modifyRequestBody = this.decryptBody(originalRequestBody); // 修改请求体(明文)
            HttpServletRequest orginalRequest = (HttpServletRequest) request;
            ModifyRequestBodyWrapper requestWrapper = new ModifyRequestBodyWrapper(orginalRequest, modifyRequestBody);
    
            /**
             * 3.构建新的响应对象,执行调用链(用新的请求对象和响应对象)
             * 得到应用层的响应后(明文),执行修改响应体函数,最后得到需要响应给调用方的响应体(密文)
             */
            ModifyResponseBodyWrapper responseWrapper = new ModifyResponseBodyWrapper(originalResponse);
            chain.doFilter(requestWrapper, responseWrapper);
            String originalResponseBody = responseWrapper.getResponseBody(); // 原响应体(明文)
            String modifyResponseBody = this.encryptBody(originalResponseBody); // 修改后的响应体(密文)
    
            /**
             * 4.将修改后的响应体用原响应对象的输出流来输出
             * 要保证响应类型和原请求中的一致,并重新设置响应体大小
             */
            originalResponse.setContentType(requestWrapper.getOrginalRequest().getContentType()); // 与请求时保持一致
            byte[] responseData = modifyResponseBody.getBytes(responseWrapper.getCharacterEncoding()); // 编码与实际响应一致
            originalResponse.setContentLength(responseData.length);
            @Cleanup ServletOutputStream out = originalResponse.getOutputStream();
            out.write(responseData);
        }
    
        /**
         * 解密函数,用Base64进行解密
         *
         * @param originalBody 加密的请求体(密文)
         * @return
         */
        private String decryptBody(String originalBody) {
            return Base64.decodeToString(originalBody);
        }
    
        /**
         * 加密函数,用Base64进行加密
         *
         * @param originalBody 需要加密的响应体(明文)
         * @return
         */
        private String encryptBody(String originalBody) {
            return Base64.encodeToString(originalBody);
        }
    }
    
    1. 实现Filter接口。
    2. 使用@WebFilter注解指定拦截的url,可以配置多个url。

    请求包装类

    /**
     * 修改http请求体和contentType后构建新的请求对象
     * 只针对请求体可读的请求类型
     *
     * @author zhaoxb
     * @create 2019-09-26 17:49
     */
    @Data
    public class ModifyRequestBodyWrapper extends HttpServletRequestWrapper {
        /**
         * 原请求对象
         */
        private HttpServletRequest orginalRequest;
        /**
         * 修改后的请求体
         */
        private String modifyRequestBody;
        /**
         * 修改后的请求类型
         */
        private String contentType;
    
        /**
         * 修改请求体,请求类型沿用原来的
         *
         * @param orginalRequest    原请求对象
         * @param modifyRequestBody 修改后的请求体
         */
        public ModifyRequestBodyWrapper(HttpServletRequest orginalRequest, String modifyRequestBody) {
            this(orginalRequest, modifyRequestBody, null);
        }
    
        /**
         * 修改请求体和请求类型
         *
         * @param orginalRequest    原请求对象
         * @param modifyRequestBody 修改后的请求体
         * @param contentType       修改后的请求类型
         */
        public ModifyRequestBodyWrapper(HttpServletRequest orginalRequest, String modifyRequestBody, String contentType) {
            super(orginalRequest);
            this.modifyRequestBody = modifyRequestBody;
            this.orginalRequest = orginalRequest;
            this.contentType = contentType;
        }
    
        /**
         * 构建新的输入流,在新的输入流中放入修改后的请求体(使用原请求中的字符集)
         *
         * @return 新的输入流(包含修改后的请求体)
         */
        @Override
        @SneakyThrows
        public ServletInputStream getInputStream() {
            return new ServletInputStream() {
                private InputStream in = new ByteArrayInputStream(modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()));
    
                @Override
                public int read() throws IOException {
                    return in.read();
                }
    
                @Override
                public boolean isFinished() {
                    return false;
                }
    
                @Override
                public boolean isReady() {
                    return false;
                }
    
                @Override
                public void setReadListener(ReadListener readListener) {
    
                }
            };
        }
    
        /**
         * 获取新的请求体大小
         *
         * @return
         */
        @Override
        @SneakyThrows
        public int getContentLength() {
            return modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()).length;
        }
    
        /**
         * 获取新的请求体大小
         *
         * @return
         */
        @Override
        @SneakyThrows
        public long getContentLengthLong() {
            return modifyRequestBody.getBytes(orginalRequest.getCharacterEncoding()).length;
        }
    
        /**
         * 获取新的请求类型,默认沿用原请求的
         *
         * @return
         */
        @Override
        public String getContentType() {
            return StringUtils.isBlank(contentType) ? orginalRequest.getContentType() : contentType;
        }
    
        /**
         * 修改contentType
         *
         * @param name 请求头
         * @return
         */
        @Override
        public Enumeration<String> getHeaders(String name) {
            if (null != name && name.replace("-", "").toLowerCase().equals("contenttype") && !StringUtils.isBlank(contentType)) {
                return new Enumeration<String>() {
                    private boolean hasGetted = false;
    
                    @Override
                    public boolean hasMoreElements() {
                        return !hasGetted;
                    }
    
                    @Override
                    public String nextElement() {
                        if (hasGetted) {
                            throw new NoSuchElementException();
                        } else {
                            hasGetted = true;
                            return contentType;
                        }
                    }
                };
            }
            return super.getHeaders(name);
        }
    }
    

    响应包装类

    /**
     * 构建新的响应对象,缓存响应体
     * 可以通过此对象获取响应体,然后进行修改,通过原响应流返回给调用方
     *
     * @author zhaoxb
     * @create 2019-09-26 17:52
     */
    @Data
    public class ModifyResponseBodyWrapper extends HttpServletResponseWrapper {
        /**
         * 原响应对象
         */
        private HttpServletResponse originalResponse;
        /**
         * 缓存响应体的输出流(低级流)
         */
        private ByteArrayOutputStream baos;
        /**
         * 输出响应体的高级流
         */
        private ServletOutputStream out;
        /**
         * 输出响应体的字符流
         */
        private PrintWriter writer;
    
        /**
         * 构建新的响应对象
         *
         * @param resp 原响应对象
         */
        @SneakyThrows
        public ModifyResponseBodyWrapper(HttpServletResponse resp) {
            super(resp);
            this.originalResponse = resp;
            this.baos = new ByteArrayOutputStream();
            this.out = new SubServletOutputStream(baos);
            this.writer = new PrintWriter(new OutputStreamWriter(baos));
        }
    
        /**
         * 获取输出流
         *
         * @return
         */
        @Override
        public ServletOutputStream getOutputStream() {
            return out;
        }
    
        /**
         * 获取输出流(字符)
         *
         * @return
         */
        @Override
        public PrintWriter getWriter() {
            return writer;
        }
    
        /**
         * 获取响应体
         *
         * @return
         * @throws IOException
         */
        public String getResponseBody() throws IOException {
            return this.getResponseBody(null);
        }
    
        /**
         * 通过指定字符集获取响应体
         *
         * @param charset 字符集,指定响应体的编码格式
         * @return
         * @throws IOException
         */
        public String getResponseBody(String charset) throws IOException {
            /**
             * 应用层会用ServletOutputStream或PrintWriter字符流来输出响应
             * 需要把这2个流中的数据强制刷到ByteArrayOutputStream这个流中,否则取不到响应数据或数据不完整
             */
            out.flush();
            writer.flush();
            return new String(baos.toByteArray(), StringUtils.isBlank(charset) ? this.getCharacterEncoding() : charset);
        }
    
        /**
         * 输出流,应用层会用此流来写出响应体
         */
        class SubServletOutputStream extends ServletOutputStream {
            private ByteArrayOutputStream baos;
    
            public SubServletOutputStream(ByteArrayOutputStream baos) {
                this.baos = baos;
            }
    
            @Override
            public void write(int b) {
                baos.write(b);
            }
    
            @Override
            public void write(byte[] b) {
                baos.write(b, 0, b.length);
            }
    
            @Override
            public boolean isReady() {
                return false;
            }
    
            @Override
            public void setWriteListener(WriteListener writeListener) {
    
            }
        }
    }
    

    3、测试验证

    @RestController
    @Slf4j
    @RequestMapping("/decrypt")
    public class WebController {
        @PostMapping("/test")
        public String test(@RequestBody String requestBody) {
            log.info("经过解密后的数据:{}", requestBody);
            return "success-交易成功";
        }
    }
    
    public class HttpdecryptApplicationTests {
        @Test
        public void test() {
            HttpResponse response = HttpRequest
                    .post("http://127.0.0.1:10400/decrypt/test")
                    .body("eyJlbmNyeXB0SW5mbyI6IuWKoOWvhuaVsOaNriIsInZlcnNpb24iOiIxLjAifQ==")
                    .send();
            String result = response.bodyText();
            System.out.println(Base64.decodeToString(result)); // success-交易成功
        }
    }
    

    4、优化改进

    对于过滤器中的处理逻辑,如果每次做加解密都要这样去实现,未免有些冗余;

    重新分析不难发现在过滤器中的处理逻辑始终都是不变的,对于不同的加解密方式只有加解密函数是变化的。为此可以引入函数式编程的方式,对于处理逻辑进行封装,每次只需要定义不同的加解密函数然后调用封装好的API即可。

    HttpUtil封装工具类

    @Slf4j
    public class HttpUtil {
        /**
         * 修改http请求体/响应体
         *
         * @param originalRequest       原请求对象
         * @param originalResponse      原响应对象
         * @param chain                 调用链
         * @param modifyRequestBodyFun  修改请求体函数
         * @param modifyResponseBodyFun 修改响应体函数
         * @throws IOException
         * @throws ServletException
         */
        public static void modifyHttpData(ServletRequest originalRequest, ServletResponse originalResponse, FilterChain chain,
                                          Function<String, String> modifyRequestBodyFun, Function<String, String> modifyResponseBodyFun) throws IOException, ServletException {
            modifyHttpData(originalRequest, originalResponse, chain, modifyRequestBodyFun, modifyResponseBodyFun, null);
        }
    
        /**
         * 修改http请求体/响应体
         *
         * @param request               原请求对象
         * @param response              原响应对象
         * @param chain                 调用链
         * @param modifyRequestBodyFun  修改请求体函数
         * @param modifyResponseBodyFun 修改响应体函数
         * @param requestContentType    修改后的请求类型
         * @throws IOException
         * @throws ServletException
         */
        public static void modifyHttpData(ServletRequest request, ServletResponse response, FilterChain chain,
                                          Function<String, String> modifyRequestBodyFun, Function<String, String> modifyResponseBodyFun,
                                          String requestContentType) throws IOException, ServletException {
            /**
             * 1.原请求/响应对象强转
             */
            HttpServletRequest originalRequest = (HttpServletRequest) request;
            HttpServletResponse originalResponse = (HttpServletResponse) response;
    
            /**
             * 2.读取原请求体(密文),执行修改请求体函数得到修改后的请求体(明文),然后构建新的请求对象(包含修改后的请求体)
             */
            String originalRequestBody = ServletUtil.readRequestBody(originalRequest); // 读取原请求体(密文)
            String modifyRequestBody = modifyRequestBodyFun.apply(originalRequestBody); // 修改请求体(明文)
            ModifyRequestBodyWrapper requestWrapper = modifyRequestBodyAndContentType(originalRequest, modifyRequestBody, requestContentType);
    
            /**
             * 3.构建新的响应对象,执行调用链(用新的请求对象和响应对象)
             * 得到应用层的响应后(明文),执行修改响应体函数,最后得到需要响应给调用方的响应体(密文)
             */
            ModifyResponseBodyWrapper responseWrapper = getHttpResponseWrapper(originalResponse);
            chain.doFilter(requestWrapper, responseWrapper);
            String originalResponseBody = responseWrapper.getResponseBody(); // 原响应体(明文)
            String modifyResponseBody = modifyResponseBodyFun.apply(originalResponseBody); // 修改后的响应体(密文)
    
            /**
             * 4.将修改后的响应体用原响应对象的输出流来输出
             * 要保证响应类型和原请求中的一致,并重新设置响应体大小
             */
            originalResponse.setContentType(requestWrapper.getOrginalRequest().getContentType()); // 与请求时保持一致
            byte[] responseData = modifyResponseBody.getBytes(responseWrapper.getCharacterEncoding()); // 编码与实际响应一致
            originalResponse.setContentLength(responseData.length);
            @Cleanup ServletOutputStream out = originalResponse.getOutputStream();
            out.write(responseData);
        }
    
        /**
         * 修改请求体
         *
         * @param request           原请求
         * @param modifyRequestBody 修改后的请求体
         * @return
         */
        public static ModifyRequestBodyWrapper modifyRequestBody(ServletRequest request, String modifyRequestBody) {
            return modifyRequestBodyAndContentType(request, modifyRequestBody, null);
        }
    
        /**
         * 修改请求体和请求类型
         *
         * @param request           原请求
         * @param modifyRequestBody 修改后的请求体
         * @param contentType       请求类型
         * @return
         */
        public static ModifyRequestBodyWrapper modifyRequestBodyAndContentType(ServletRequest request, String modifyRequestBody, String contentType) {
            log.debug("ContentType改为 -> {}", contentType);
            HttpServletRequest orginalRequest = (HttpServletRequest) request;
            return new ModifyRequestBodyWrapper(orginalRequest, modifyRequestBody, contentType);
        }
    
        /**
         * 用原响应对象来构建新的http响应包装对象
         *
         * @param response 原响应对象
         * @return
         */
        public static ModifyResponseBodyWrapper getHttpResponseWrapper(ServletResponse response) {
            HttpServletResponse originalResponse = (HttpServletResponse) response;
            return new ModifyResponseBodyWrapper(originalResponse);
        }
    }
    

    改进后的过滤器

    @WebFilter(urlPatterns = {"/decrypt/*"}, filterName = "decryptFilter")
    @Slf4j
    public class DecryptFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            Function<String, String> modifyRequestBodyFun = Base64::decodeToString; // 解密函数
            Function<String, String> modifyResponseBodyFun = Base64::encodeToString; // 加密函数
            HttpUtil.modifyHttpData(request, response, chain, modifyRequestBodyFun, modifyResponseBodyFun);
        }
    }
    
    1. 只需要在过滤器上配置需要拦截的url列表、定义加解密函数然后调用封装好的API即可。
    2. 过滤器中不会改变请求和响应的字符集,都是沿用原来的。
    3. 只能针对于带有请求体的请求做加解密处理。
    4. 另外modifyHttpData函数有另外的重载,支持修改Content-Type

    参考资料

    代码地址

    相关文章

      网友评论

        本文标题:在过滤器中实现修改http请求体和响应体

        本文链接:https://www.haomeiwen.com/subject/xhstrktx.html