SpringBoot基于Filter实现参数的修改及添加(含交互通用加密方案)

  |   0 评论   |   0 浏览

问题的背景

为了数据安全,对业务数据的报文采用了加密的方式,具体的场景如下

  • 数据

    // 原始数据
    {
      "a": "1",
      "b": "2",
      "c": {
        "c1": "c1",
        "c2": 2
      }
    }
    // 其中c为业务数据,为了安全,我们将其解析加密,比如加密成了aabbccdd11223344
    // 那么得到的最终请求数据如下:
    {
      "a": "1",
      "b": "2",
      "c": "aabbccdd11223344"
    }
    
  • 后台接受对象

    // 接受对象
    @Data
    public class ReqObj
    {
        private String a;
    
        private String b;
    
        private BusiObj c;
    }
    
    @Data
    public class BusiObj
    {
        private String c1;
    
        private Integer c2;
    }
    
  • Controller入口

    @RestController
    public class TestController
    {
    
      @PostMapping("test1")
      public String test(@RequestBody ReqObj data)
      {
          return JSON.toJSONString(data);
      }
    
      @GetMapping("test2")
      public String test(String data, String name)
      {
          return data + "///name:" + name;
      }
    }
    
  • 问题:解密放在哪里?

    through reference chain: com.lupf.aaa.controller.ReqObj["c"])
    

    直接按上面的方式运行,@RequestBody将json文本转换成对象的时候,由于前面传递的是一个String,但是现在用一个Object接受,发现没法转换。那我们要把解密的放在哪里呢?一开始想使用Aop去解决,后来发现并不行,@RequestBody的执行是在AOP之前的,还有就是,由于Controler接受的就是一个ReqObj对象,AOP代理拿到的也就是那个对象,特殊业务场景下,json文本和之后转成的对象是没办法对数据做互通处理的;因此考虑通过 Filter拿到前端的请求参数,将密文的数据解密之后,再塞回去,那样Controller拿到就是一个解密之后的文本,@RequestBody也就可以正常做转换了;同时属于一劳永逸的方式,对于controller来说,不会收到加密的任何影响;

  • 思考!响应数据如何加密?
    上面的Filter很适合做请求数据的处理,但是对于响应数据的处理就比较别手了,结合上面的方案,对于响应数据,就非常适合使用 AOP;定义一个 @Around(环绕)的切点,当收到Controller的响应之后,拿到数据,加密之后再返回给客户端即可。

Filter相关代码

  • 创建一个获取body数据的帮助类

    import javax.servlet.http.HttpServletRequest;
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.nio.charset.Charset;
    
    public class HttpGetBodyInfoHelper
    {
        public static String getBodyString(HttpServletRequest request)
        {
            StringBuilder sb = new StringBuilder();
            InputStream inputStream = null;
            BufferedReader reader = null;
            try
            {
                inputStream = request.getInputStream();
                reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
                String line;
                while ((line = reader.readLine()) != null)
                {
                    sb.append(line);
                }
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            finally
            {
                if (inputStream != null)
                {
                    try
                    {
                        inputStream.close();
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
                if (reader != null)
                {
                    try
                    {
                        reader.close();
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
            }
            return sb.toString();
        }
    }
    
  • 创建解密修改数据的Wrapper(包装器)

    // 核心的请求参数操作全部在这个包装器中去实现
    
    import org.springframework.http.HttpMethod;
    
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.InputStreamReader;
    import java.nio.charset.Charset;
    import java.util.Collections;
    import java.util.HashMap;
    import java.util.Map;
    
    public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper
    {
        private String method;
        private final byte[] body;
        private String paramStr;
    
        public RequestReaderHttpServletRequestWrapper(HttpServletRequest request)
        {
            super(request);
            //获取前端传递的body的数据
            this.paramStr = HttpGetBodyInfoHelper.getBodyString(request);
            // 拿到请求方式 GET或者POST
            method = request.getMethod();
            if (HttpMethod.POST.name().equals(method))
            {
                // 如果是POST请求,且使用body的方式进行数据传递,这里就可以拿到请求参数了
                // 那么在这里就可以去做解密了
    
                // 这里就调用相关解密的方法去解密,得到一串解密之后的数据
                // 假如下面就是经过解密之后的明文
                paramStr = "{\"a\":\"1\",\"b\":\"2\",\"c\":{\"c1\":\"c1\",\"c2\":2}}";
            }
            else if (HttpMethod.GET.name().equals(method))
            {
                // 如果是get请求,比如说通过?data={....}这样的方式传递的参数
                // 那么body中的数据就没法获取
    
                // 可以通过以下的方式得到
                String data = request.getParameter("data");
    
                // 同样在这里调用解密的方法
                // 假如下面就是经过解密之后的明文
                paramStr = "{\"a\":\"1\",\"b\":\"2\",\"c\":{\"c1\":\"c1\",\"c2\":3}}";
            }
    
            //将文本数据转成数组对象
            body = paramStr.getBytes(Charset.forName("UTF-8"));
        }
    
        @Override
        public Map<String, String[]> getParameterMap()
        {
            HashMap<String, String[]> newMap = new HashMap<>();
            newMap.putAll(super.getParameterMap());
            // 也可以在这里添加GET的参数,不用赋值,这里赋值没有效果
            //newMap.put("name", new String[]{"aaa"});
            return Collections.unmodifiableMap(newMap);
        }
    
        @Override
        public String[] getParameterValues(String name)
        {
            // get请求,在这里修改参数
            // 这里如果是key为data的参数,就将参数值修改为解密后的数据
            if ("data".equals(name))
            {
                return new String[]{paramStr};
            }
            // 这里为新添加的name赋值;注意如果前端传递了name,这里也会替换掉
            // 因此这如果需要兼容前端传递的话,需要做特殊处理
            if ("name".equals(name))
            {
                return new String[]{"lupf"};
            }
            return super.getParameterValues(name);
        }
    
        //封装一个返回文本数据的对象
        public String getBody()
        {
            return paramStr;
        }
    
        @Override
        public BufferedReader getReader()
        {
            return new BufferedReader(new InputStreamReader(getInputStream()));
        }
    
        @Override
        public ServletInputStream getInputStream()
        {
            //将数据流传给下一个处理器
            //因为这里读取了数据,流数据读取一次之后就没有了,因此这里以流的形式将数据再次会给下一个处理器
            final ByteArrayInputStream bais = new ByteArrayInputStream(body);
    
            return new ServletInputStream()
            {
    
                @Override
                public int read()
                {
                    return bais.read();
                }
    
                @Override
                public boolean isFinished()
                {
                    return false;
                }
    
                @Override
                public boolean isReady()
                {
                    return false;
                }
    
                @Override
                public void setReadListener(ReadListener readListener)
                {
    
                }
            };
        }
    }
    
  • filter

    // 没啥特殊的   就是将上面的wrapper添加进去
    
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @Component
    public class HttpRequestBodyDataReadFilter implements Filter {
    
        @Override
        public void destroy() {
    
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
            RequestReaderHttpServletRequestWrapper requestWrapper = null;
            if (request instanceof HttpServletRequest) {
                //得到HTTP的request
                HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                requestWrapper = new RequestReaderHttpServletRequestWrapper(httpServletRequest);
            }
    
            // 在doFiler方法中传递新的request对象
            if (requestWrapper == null) {
                chain.doFilter(request, response);
            } else {
                chain.doFilter(requestWrapper, response);
            }
        }
    
        @Override
        public void init(FilterConfig arg0) {
        }
    }
    
  • 注册filter

    // 在SpringBoot的启动类里面添加以下代码
    @Bean
    public FilterRegistrationBean httpServletRequestReplacedRegistration()
    {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new HttpRequestBodyDataReadFilter());
        registration.addUrlPatterns("/*");
        registration.addInitParameter("paramName", "paramValue");
        registration.setName("httpRequestBodyDataReadFilter");
        registration.setOrder(1); // 执行顺序,可能会存在多个filter  特殊场景下需要指定执行的顺序
        return registration;
    }
    
  • 测试

    http://192.168.1.82:19002/test1
    http://192.168.1.82:19002/test2?data={"a":"1","b":"2","c":"aabbccdd11223344"}
    

    file 如遇图片加载失败,可尝试使用手机流量访问
    file 如遇图片加载失败,可尝试使用手机流量访问

SpringBoot手动注入工具类

如wrapper中得到数据之后,需要对数据解析解密,此时可能需要密钥,需要操作数据库或者其他服务;那么就需要将service或者其他类的示例注入进来,因此就可以通过以下工具类注入

  • 工具类

    import org.springframework.context.ApplicationContext;
    
    /**
     * spring 上下文管理工具类
     *
     * @Title: SpringContextUtil.java
     * @Description:
     * @date 2017-5-23 上午11:05:48
     *
     */
    public class SpringContextUtil
    {
      //spring上下文
      private static ApplicationContext applicationContext;
    
      /**
       * 实现ApplicationContextAware接口的回调方法,设置上下文环境
       *
       * @param applicationContext
       */
      public static void setApplicationContext(ApplicationContext applicationContext) {
        if (null == SpringContextUtil.applicationContext)
          SpringContextUtil.applicationContext = applicationContext;
      }
    
      public static ApplicationContext getApplicationContext() {
        return applicationContext;
      }
    
      /**
       * 通过name获取 Bean.
       *
       * @param name
       * @return
       */
      public static Object getBean(String name) {
        return getApplicationContext().getBean(name);
    
      }
    
      /**
       * 通过name获取 Bean.
       *
       * @param clazz
       * @return
       */
      public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
      }
    
      /**
       * 通过name,以及Clazz返回指定的Bean
       *
       * @param name
       * @param clazz
       * @return
       */
      public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
      }
    }
    
  • 设置工具类的上下文;
    在入口Application类中实现ApplicationListener接口

    public class TestApplication implements ApplicationListener<ContextRefreshedEvent>
    {
    
        public static void main(String[] args)
        {
            SpringApplication.run(TestApplication.class, args);
        }
    
        @Override
        public void onApplicationEvent(ContextRefreshedEvent event)
        {
            // 设置上下文对象
            SpringContextUtil.setApplicationContext(event.getApplicationContext());
        }
    }
    
  • 使用

    SpringContextUtil.getBean(aaa.class);
    

    file 如遇图片加载失败,可尝试使用手机流量访问



标题:SpringBoot基于Filter实现参数的修改及添加(含交互通用加密方案)
作者:码霸霸
地址:https://blog.lupf.cn/articles/2020/04/16/1587014541291.html