SpringBoot基于Filter实现参数的修改及添加(含交互通用加密方案)
问题的背景
为了数据安全,对业务数据的报文采用了加密的方式,具体的场景如下
-
数据
// 原始数据 { "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"}
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);
标题:SpringBoot基于Filter实现参数的修改及添加(含交互通用加密方案)
作者:码霸霸
地址:https://blog.lupf.cn/articles/2020/04/16/1587014541291.html