如何在过滤器中修改http请求体和响应体

2023-08-23 17:34:08 浏览数 (1)

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

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

一般在过滤器中修改请求体和响应体,以往需要自行创建Wrapper包装类,从原请求Request对象中读取原请求体,修改后重新放入新的请求对象中等等操作……非常麻烦。如果可以在过滤器中只定义加解密的函数,然后调用一个API传入这些加解密函数,中间操作统统不管,这样用起来岂不是更爽!

1、启动类配置注解

新增注解@ServletComponentScan

代码语言:javascript复制
@SpringBootApplication
@ServletComponentScan
public class HttpdecryptApplication {
    public static void main(String[] args) {
        SpringApplication.run(HttpdecryptApplication.class, args);
    }
}

2、过滤器实现

2.1、用Base64算法做加解密示例
代码语言:javascript复制
@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。

处理逻辑

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

函数中使用的请求包装类ModifyRequestBodyWrapper和响应包装类ModifyResponseBodyWrapper在文末附录中贴出,可以直接copy到项目工程中使用。

3、测试验证

代码语言:javascript复制
@RestController
@Slf4j
@RequestMapping("/decrypt")
public class WebController {
    @PostMapping("/test")
    public String test(@RequestBody String requestBody) {
        log.info("经过解密后的数据:{}", requestBody);
        return "success-交易成功";
    }
}
代码语言:javascript复制
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即可。

改进后的过滤器

代码语言:javascript复制
@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

HttpUtil也在文末附录中贴出,直接copy到项目工程中使用。

对于函数式编程不熟悉的同学可以去学习下Java中如何使用 lambda 表达式和Java的几种内置的函数接口(JDK1.8版本及以上才支持);上面的lambda 表达式其实是一种简写的方式,还可以用其最一般化的方式来表示。

代码语言:javascript复制
Function<String, String> modifyRequestBodyFun = (originalBody) -> {
    return Base64.decodeToString(originalBody);
};
Function<String, String> modifyResponseBodyFun = (originalBody) -> {
    return Base64.encodeToString(originalBody);
};

参考链接

  • SpringBoot框架中,使用过滤器进行加密解密操作

代码地址

  • github:https://github.com/senlinmu1008/spring-boot/tree/master/httpdecrypt
  • gitee:https://gitee.com/ppbin/spring-boot/tree/master/httpdecrypt

附录

请求包装类

代码语言:javascript复制
/**
 * 修改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);
    }
}

响应包装类

代码语言:javascript复制
/**
 * 构建新的响应对象,缓存响应体
 * 可以通过此对象获取响应体,然后进行修改,通过原响应流返回给调用方
 *
 * @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) {

        }
    }
}

HttpUtil封装工具类

代码语言:javascript复制
@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);
    }
}

0 人点赞