SpringBoot 过滤器更改 Request body ,并实现数据解密

springboot,request,body · 浏览次数 : 2

小编点评

本文介绍了一种在SpringBoot项目中实现客户端与服务端网络通信加解密的方法。为了解决请求体在请求到达Controller之前被读取的问题,以及HttpServletRequest对象body数据只能get不能set的问题,我们自定义了一个HttpServletRequestWrapper类,并使用了一个Filter过滤器来处理请求。 **1. 自定义HttpServletRequestWrapper** 自定义类`CustomHttpServletRequestWrapper`继承了`HttpServletRequestWrapper`,并增加了赋值方法,以满足需求。在这个类中,我们使用了一个`StringBuilder`来保存请求的body内容,并在构造函数中初始化。同时,我们实现了`getInputStream()`和`getReader()`方法,以便在解密后重新设置请求的body数据。 **2. 自定义Filter** 自定义类`RequestWrapperFilter`实现了`Filter`接口,用于拦截请求并解析body。在这个类中,我们首先检查请求方法是否为POST,然后创建一个`CustomHttpServletRequestWrapper`对象,并调用`preHandle()`方法来处理请求。`preHandle()`方法将原始请求body解密,并将其重新设置到请求对象中。 **3. 配置Filter** 在`WebMvcConfigurer`中配置并使用`RequestWrapperFilter`,并将其注册为全局过滤器。通过设置`order`属性,我们可以控制过滤器的执行顺序。 **总结** 通过这种方式,我们可以在不干扰Controller业务逻辑的情况下,实现对请求报文的加解密操作。这种方法具有较低的耦合度,易于维护,并且可以应用于Web项目和非Web项目。

正文

客户端、服务端网络通信,为了安全,会对报文数据进行加解密操作。

在SpringBoot项目中,最好使用参考AOP思想,加解密与Controller业务逻辑解耦,互不影响。

以解密为例:需要在request请求到达Controller之前进行拦截,获取请求body中的密文并对其进行解密,然后把解密后的明文重新设置到request的body上。

拦截器、过滤器、Controller之间的关系

如图所示,在Request对象进入Controller之前,可使用Filter过滤器或者过滤器和Interceptor拦截器进行拦截。

过滤器、拦截器主要差异:

1、过滤器来自于 Servlet;而拦截器来自于 Spring 框架;
2、触发时机不同:请求的执行顺序是:请求进入容器 > 进入过滤器 > 进入 Servlet > 进入拦截器 > 执行控制器(Controller)
3、过滤器是基于方法回调实现的;拦截器是基于动态代理(底层是反射)实现的;
4、过滤器是 Servlet 规范中定义的,所以过滤器要依赖 Servlet 容器,它只能用在 Web 项目中;拦截器是 Spring 中的一个组件,因此拦截器既可以用在 Web 项目中,同时还可以用在 Application 或 Swing 程序中;
5、过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、字符集编码设置、响应数据压缩等功能;拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:登录判断、权限判断、日志记录等业务;

对于我们当前应用场景来说,区别就是过滤器更适用于修改request body。

为响应数据报文统一加密,也可参考上述过程实现;

具体实现分析

修改请求,会有两个问题:
1、请求体的输入流被读取,它就不能再被其他组件读取,因为输入流只能被标记、重置,并且在读取后会被消耗。
2、HttpServletRequest对象的body数据只能get,不能set,不能再次赋值。而咱们的需求是需要给HttpServletRequest body解密并重新赋值。
基于以上两个问题,咱们需要定义一个HttpServletRequest实现类,增加赋值方法,来满足我们的需求。
CustomHttpServletRequestWrapper.java

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * 自定义HttpServletRequestWrapper
 * qxc
 * 20240622
 */
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private String body;

    public CustomHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {

        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBody() {
        return this.body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

接下来,继续写Filter类,用于拦截请求,解析body, 解密报文,替换请求body数据。
RequestWrapperFilter.java


import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义Filter 
 * qxc
 * 20240622
 */
@Slf4j
public class RequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        CustomHttpServletRequestWrapper customHttpServletRequestWrapper = null;
        try {
            HttpServletRequest req = (HttpServletRequest) request;
            customHttpServletRequestWrapper = new CustomHttpServletRequestWrapper(req);
            preHandle(customHttpServletRequestWrapper);
        } catch (Exception e) {
            log.warn("customHttpServletRequestWrapper Error:", e);
        }

        chain.doFilter((Objects.isNull(customHttpServletRequestWrapper) ? request : customHttpServletRequestWrapper), response);
    }

    public void preHandle(CustomHttpServletRequestWrapper request) throws Exception {
        //仅当请求方法为POST时修改请求体
        if (!request.getMethod().equalsIgnoreCase("POST")) {
            return;
        }
        //读取原始请求体
        StringBuilder originalBody = new StringBuilder();
        String line;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) {
            while ((line = reader.readLine()) != null) {
                originalBody.append(line);
            }
        }
        String bodyText = originalBody.toString();
        //json字符串转成map集合
        Map<String, String> map = getMap(bodyText);
        //获取解密参数,解密数据
        if (map != null && map.containsKey("time") && map.containsKey("data")) {
            String time = map.get("time");
            String key = "基于时间戳等参数生成密钥、此处请换成自己的密钥";
            String data = map.get("data");
            //解密数据
            String decryptedData = Cipher.decrypt(key, data);
            //为请求对象重新设置body
            request.setBody(decryptedData);
        }
    }

    private Map<String, String> getMap(String text) {
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 将JSON字符串转换为Map
            Map<String, String> map = objectMapper.readValue(text, Map.class);
            return map;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}

AES加解密算法封装
Cipher.java

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

import javax.crypto.spec.SecretKeySpec;

/**
 * 自定义AES加解密算法类 
 * qxc
 * 20240622
 */
public class Cipher {
    public static String encrypt(String key, String text){
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
            cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, secretKeySpec);
            byte[] encryptedBytes = cipher.doFinal(text.getBytes("UTF-8"));
            String cipherText = base64Encode(encryptedBytes);
            return cipherText;
        }catch (Exception ex){
            ex.printStackTrace();
            return "";
        }
    }

    public static String decrypt(String key, String cipherText) {
        try {
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES");
            cipher.init(javax.crypto.Cipher.DECRYPT_MODE, secretKeySpec);
            byte[] decryptedBytes = cipher.doFinal(base64Decode(cipherText));
            return new String(decryptedBytes, StandardCharsets.UTF_8);
        }catch (Exception ex){
            ex.printStackTrace();
            return "";
        }
    }

    public static String getMd5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(input.getBytes());
            byte[] digest = md.digest();
            BigInteger bigInt = new BigInteger(1, digest);
            String md5Hex = bigInt.toString(16);
            while (md5Hex.length() < 32) {
                md5Hex = "0" + md5Hex;
            }
            return md5Hex;
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

    public static String base64Encode(byte[] bytes) {
        if (bytes != null && bytes.length > 0) {
            return Base64.getEncoder().encodeToString(bytes);
        }
        return "";
    }

    public static byte[] base64Decode(String base64Str) {
        if (base64Str != null && base64Str.length() > 0) {
            return Base64.getDecoder().decode(base64Str);
        }
        return new byte[]{};
    }
}

最后,需要在WebMvcConfigurer中配置并使用RequestWrapperFilter

import com.qxc.server.encryption.RequestWrapperFilter;
import com.qxc.server.jwt.JwtInterceptor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@SpringBootApplication
public class ServerApplication implements WebMvcConfigurer {

    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }

    @Bean
    public FilterRegistrationBean servletRegistrationBean() {
        RequestWrapperFilter userInfoFilter = new RequestWrapperFilter();
        FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(userInfoFilter);
        bean.setName("requestFilter");
        bean.addUrlPatterns("/*");
        bean.setOrder(Ordered.LOWEST_PRECEDENCE);

        return bean;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        
    }
}

这样,就实现了请求报文的解密、数据替换,而且对Controller逻辑没有影响。

与SpringBoot 过滤器更改 Request body ,并实现数据解密相似的内容:

SpringBoot 过滤器更改 Request body ,并实现数据解密

客户端、服务端网络通信,为了安全,会对报文数据进行加解密操作。 在SpringBoot项目中,最好使用参考AOP思想,加解密与Controller业务逻辑解耦,互不影响。 以解密为例:需要在request请求到达Controller之前进行拦截,获取请求body中的密文并对其进行解密,然后把解密后的

SpringBoot 整合 EasyExcel 实现自由导入导出,太强了

在实际的业务系统开发过程中,操作 Excel 实现数据的导入导出基本上是个非常常见的需求。 之前,我们有介绍一款非常好用的工具:EasyPoi,有读者提出在数据量大的情况下,EasyPoi 会占用内存大,性能不够好,严重的时候,还会出现内存异常的现象。 今天我给大家推荐一款性能更好的 Excel 导

Java Agent场景性能测试分析优化经验分享

摘要:本文将以Sermant的SpringBoot 注册插件的性能测试及优化过程为例,分享在Java Agent场景如何进行更好的性能测试优化及在Java Agent下需要着重注意的性能陷阱。 作者:栾文飞 高级软件工程师 一、背景介绍 Sermant是一个主打服务治理领域的Java Agent框架

React + Springboot + Quartz,从0实现Excel报表自动化

一、项目背景 企业日常工作中需要制作大量的报表,比如商品的销量、销售额、库存详情、员工打卡信息、保险报销、办公用品采购、差旅报销、项目进度等等,都需要制作统计图表以更直观地查阅。但是报表的制作往往需要耗费大量的时间,即使复用制作好的报表模版,一次次周期性对数据的复制粘贴操作也很耗人,同时模版在此过程

一文了解Spring Boot启动类SpringApplication

只有了解 Spring Boot 在启动时都做了些什么,我们才能在后续的实践的过程中更好地理解其运行机制,以便遇到问题能更快地定位和排查。

SpringBoot实战:Spring Boot接入Security权限认证服务

引言 Spring Security 是一个功能强大且高度可定制的身份验证和访问控制的框架,提供了完善的认证机制和方法级的授权功能,是一个非常优秀的权限管理框架。其核心是一组过滤器链,不同的功能经由不同的过滤器。本文将通过一个案例将 Spring Security 整合到 SpringBoot中,要

CAS前后端分离解决方案

CAS前后端分离解决方案 关于CSS服务器的搭建和整合SpringBoot参考:CAS5.3服务器搭建与客户端整合SpringBoot以及踩坑笔记 环境与需求 后端:springboot 前端: vue + element UI 在登录后之后登录状态在系统中自主控制。 问题 当接口在CAS过滤器中时

springboot升级过程中踩坑定位分析记录 | 京东云技术团队

因所负责的系统使用的spring框架版本5.1.5.RELEASE在线上出过一个偶发的小事故,最后定位为spring-context中的一个bug导致的。

SpringBoot内置tomcat启动过程及原理

作者:李岩科 1 背景 SpringBoot 是一个框架,一种全新的编程规范,他的产生简化了框架的使用,同时也提供了很多便捷的功能,比如内置 tomcat 就是其中一项,他让我们省去了搭建 tomcat 容器,生成 war,部署,启动 tomcat。因为内置了启动容器,应用程序可以直接通过 Mave

SpringBoot+mail 轻松实现各类邮件自动推送

一、简介 在实际的项目开发过程中,经常需要用到邮件通知功能。例如,通过邮箱注册,邮箱找回密码,邮箱推送报表等等,实际的应用场景非常的多。 早期的时候,为了能实现邮件的自动发送功能,通常会使用 JavaMail 相关的 api 来完成。后来 Spring 推出的 JavaMailSender 工具,进