文章目录
- 日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现
- 一、概述
- 二、通过 Filter 的方式实现
- 2.1、加解密工具类
- 2.2、请求包装类
- 2.3、响应包装类
- 2.4、实现加解密
- 2.5、效果展示
- 三、总结
日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现
前置内容
- 日志输出指南
- 日志输出-第二章-接口级出入参的实现
- 日志输出-第三章-接口级出入参输出完整数据的实现
一、概述
上一章内容为如何输出完整数据,但是一般情况下还是会采用第二章的实现方式(因为输出 body 会影响性能),上一章的处理方式实际上更多是用于处理前后端数据的加解密。
本章的内容实际上并不属于 日志输出的范围 而是对上一章的内容进行了衍生(因为日志输出是需要在流量的出入口做处理,前后端数据加解密也是在流量的出入口做处理,并且都是对 body 数据处理),是 SpringBoot 项目如何处理前后端数据加解密问题(SpringCloud 版本的加解密会在后续日志写到相应版本后再更新)。
一般情况下的做法分为两种:
- 通过 Filter 的方式,也就是和我们上一章的日志输出一样,只是将输出日志改为对数据进行加解密就可以了。
- 通过 Spring 的 RequestBodyAdvice 和 ResponseBodyAdvice 来实现。
两种的实现难度都差不多,唯一的区别在于 通过 Spring 的这种方式好像是只能实现 POST 这类请求的拦截(我只是大概试了一下)。
二、通过 Filter 的方式实现
这一部分除了输出之外,最大的区别在于 Response 的包装类,因为上一章的内容在于如何输出日志,所以我们实际上只需要在响应中写数据的时候写两份就可以做到了。
/** * @Param * @Return * @Description 内部类,对 ServletOutputStream 进行包装,方便日志记录 * @Author lizelin * @Date 2023/11/3 16:43 **/ private class ResponseWrapperOutputStream extends ServletOutputStream { private ByteArrayOutputStream bos = null; private HttpServletResponse response = null; public ResponseWrapperOutputStream(ByteArrayOutputStream stream, HttpServletResponse response) { bos = stream; this.response = response; } /** * @Param b * @Return void * @Description 将写暂存一份方便日志 * @Author lizelin * @Date 2023/11/3 17:31 **/ @Override public void write(int b) throws IOException { bos.write(b); response.getOutputStream().write(b); } /** * @Param b * @Return void * @Description 将写暂存一份方便日志 * @Author lizelin * @Date 2023/11/3 17:32 **/ @Override public void write(byte[] b) throws IOException { bos.write(b, 0, b.length); response.getOutputStream().write(b, 0, b.length); } @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { //不需要重新 } }
也就是上面代码中的 write 部分,ByteArrayOutputStream 的作用在于我们自己输出日志使用,但是向客户端响应的部分实际上还是走的 response.getOutputStream().write(b); 也就是说实际上我们上一章的内容只是在原有的响应逻辑上做了一个旁路逻辑用于输出日志。
2.1、加解密工具类
一共有两组密钥,后端响应加密、前端响应解密、后端请求解密、前端请求加密
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.RSA; import cn.hutool.crypto.asymmetric.SM2; /** * @ClassName ParametersSecureUtil * @Author lizelin * @Description 参数加密 util * @Date 2024/5/29 14:50 * @Version 1.0 */ public class ParametersSecureUtil { //后端持有私钥解密 private static String REQ_REAR_PRIVATE_KEY = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALLu5fiTx0RbbCMVFaiAx9LSiddTQT58ab07I6LtlIem8J2Q8C+1P6m+8w81T3kPz8WfpQM+7npdv3FhQmrRggj37Lm5+8Q7OMMCXG3va6kpRr9xGd4TjyuJBi8AGX58MnojruiQCwhxjngyJggmHZYlAB61A3OMq1Bi2ExBbkGnAgMBAAECgYAGJKGMmTY8KI9b3PtzX4h8unG1DMyuooLW1lLw4ws4ZQjZwAIfATAAWefqW8AwvdQ6SrLVm7GATfumntoy5KJ8MaF86pfTcGsuqpYZXwcjAHJ4sikKPZYUPn+BaEDcMIBBx8QkVSd0okV2m0bwou6nbVoorjkLCzdQrlXSpmeeUQJBAN8BL8QFYed+BLqyYMiZicGjuRRKGe4QUeMjpLDys0WC4HXCQjozbk1t9LL63GzC7BkMrH/BIReqIv9S7i6ff/UCQQDNaGeXNwGWj2JfsrQMmBe3HReVuwNV7bBlD2EmKT8csZ3F05t+JMR/XaBP44ApZGiCjfPfDAUPBNR0TYEXVAWrAkEAx5SLSDa8+W36E5CjN8TZ2fiKIpNzA3GNp+f1c/ux38sS0bFKjkYLOLbooeoLrjcBECYcl7WjxUcaTUHOMuHCpQJAYocOCY6tCFdGzLiffNsHpSIjSgMmmnUlA5TY+MEYMN9R2q6iC2P/jUiPuUJbG3+6UcVdkUPmuUmLzy3OGi6HeQJBAJOZmEtgHMCfqirahDbrDIbRojw7qNDSX2bbIPEmiFs8iEN+JR8ULUwDBRvmhj6a2IddfPGLOEK5xepiuco2BGs="; //前端持有公钥加密 这个实际上是给前端用的,后端不需要使用 private static String REQ_FRONT_PUBLICE_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCy7uX4k8dEW2wjFRWogMfS0onXU0E+fGm9OyOi7ZSHpvCdkPAvtT+pvvMPNU95D8/Fn6UDPu56Xb9xYUJq0YII9+y5ufvEOzjDAlxt72upKUa/cRneE48riQYvABl+fDJ6I67okAsIcY54MiYIJh2WJQAetQNzjKtQYthMQW5BpwIDAQAB"; //后端持有公钥加密 private static String RES_REAR_PUBLICE_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9AeIxSwTTLmtgCOMsCpytG+SdX5PjC0jOjuIbY4wd61rVNemjqJNldBrrJ6ldF+t+5GXB/O0IevAL47At5WltTcWrOGEpSJssHDaVmya5E/yyDDP+3PPlvH6KR1SdgH8fppipjWRFYU5/ke+EQLTmrNxFqvqniUlEPl/63TyuqQIDAQAB"; //前端持有私钥解密 private static String RES_FRONT_PRIVATE_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAL0B4jFLBNMua2AI4ywKnK0b5J1fk+MLSM6O4htjjB3rWtU16aOok2V0GusnqV0X637kZcH87Qh68AvjsC3laW1Nxas4YSlImywcNpWbJrkT/LIMM/7c8+W8fopHVJ2Afx+mmKmNZEVhTn+R74RAtOas3EWq+qeJSUQ+X/rdPK6pAgMBAAECgYBr+qR/5szl3TIo1kr6gUGLQFE2e0Egx/SbVVPls9R7z1bAUiGdhxRWNKOgTrNaZOz8PH3J+rZsTtfO4xBm2BaHCTmCShOVl6RG/qeNC1A1S2nmpkckvS4XfH7DVs0IILaEVnHIYLSUd8oiP/nJ+Hppn6Sj7cUGSwb2itYx1YtpUQJBAN6dRipcvKHpQkvodFfkT1m6XkwgrMuiIvTmqWYvKTXUwZ7uvyYIjB42O7DziCKoBFUualED/g69ft5fhGDepFUCQQDZWlovo0RSjfUyzuh1VcI9mQu3gLSgfUbJKgeOD+435jHFhIXiKKIdaN1L1R1MLggCbaZGyq0rqS6D9GykRpUFAkEA1DJKXbsEO7ni7gRoUhdY5AjYNey3iWvFsnfkZXjy6VMiNOMS5agkF/BOOcAJti894gxaX1tU4qwSsNmPj97p+QJAC7vW9o9n1tUXEaEd54ezrsOeYE+wcKGSurVsJv0xLQ9eTH11BNqQtem9WKSuqjgp8oec3GGAq8S8YB9H5i5xSQJALd09O7Hv0fZRn8yI09qQ2KCB0CpIHrXHjGI1I/TR72k/DlTJOOIKe6LnkecXF21xiMOq0aqhs0Ol5U2FIXkkzw=="; /** * 请求 */ private static RSA reqRsa = SecureUtil.rsa(REQ_REAR_PRIVATE_KEY, REQ_FRONT_PUBLICE_KEY); /** * 响应 */ private static RSA resRsa = SecureUtil.rsa(RES_FRONT_PRIVATE_KEY, RES_REAR_PUBLICE_KEY); /** * @Param encryptedData * @Return java.lang.String * @Description 请求解密 * @Author lizelin * @Date 2024/5/29 15:43 **/ public static String requestDecrypt(String encryptedData) { return reqRsa.decryptStr(encryptedData, KeyType.PrivateKey); } /** * @Param encryptedData * @Return java.lang.String * @Description 请求内容加密,todo 测试用 * @Author lizelin * @Date 2024/5/29 15:52 **/ public static String requestEncrypt(String encryptedData) { return reqRsa.encryptHex(encryptedData, KeyType.PublicKey); } /** * @Param encryptedData * @Return java.lang.String * @Description 响应加密 * @Author lizelin * @Date 2024/5/29 15:45 **/ public static String responseEncrypt(String encryptedData) { return resRsa.encryptHex(encryptedData, KeyType.PublicKey); } /** * @Param encryptedData * @Return java.lang.String * @Description 响应内容解密 todo 测试用 * @Author lizelin * @Date 2024/5/29 15:52 **/ public static String responseDecrypt(String encryptedData) { return resRsa.decryptStr(encryptedData, KeyType.PrivateKey); } public static void main(String[] args) { // String str = "{\n" + // " \"createBy\":\"lzl\",\n" + // " \"content\":\"大厦春,你要干什么\"\n" + // "}"; String str = "95b06517952572ccd3cb645991658bcfee0cc71a465b454fa2db6cd814c2ff72e69130c334105d4303fc6378f2c0720a7e24f1c1d19f366840dc75bfa858833df7860373070b8586b42127cd489b419ac0093da7936d984c65a4b8d2b8dc1697eb3d239b7446258d4eaabaf5341e92ab2d4cb25f8da3c571c165c35e635fa1db"; System.out.println(responseDecrypt(str)); } }
2.2、请求包装类
这个实际上与上一章的代码一致
import cn.hutool.core.collection.CollUtil; import org.apache.catalina.util.ParameterMap; 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.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Enumeration; import java.util.Map; /** * @ClassName LogHttpServletRequestWrapper * @Author lizelin * @Description 日志 Http Servlet 请求包装器 * @Date 2024/5/27 18:26 * @Version 1.0 */ public class LogHttpServletRequestWrapper extends HttpServletRequestWrapper { /** * 所有参数的 Map 集合 */ private Map
parameterMap; /** * 存储 body 数据的容器(这里存储为解析流后的JSON) */ private String body; /** * @Param * @Return java.lang.String * @Description 获取Body * @Author lizelin * @Date 2023/11/3 16:09 **/ public String getBody() { return this.body; } /** * @Param body * @Return void * @Description 修改 body * @Author lizelin * @Date 2023/11/3 16:09 **/ public void setBody(String body) { this.body = body; } public LogHttpServletRequestWrapper(HttpServletRequest request) { super(request); // 给参数集合赋值 parameterMap = request.getParameterMap(); // 获取Body body = RequestResponseUtil.getRequestBody(request); } /** * @Param parameterMap * @Return void * @Description 替换整个参数 Map * @Author lizelin * @Date 2023/11/3 14:59 **/ public void setParameterMap(Map parameterMap) { this.parameterMap = parameterMap; } /** * @Param key * @Param value * @Return void * @Description 向参数集合中添加参数 * @Author lizelin * @Date 2023/11/3 14:59 **/ public void putParameterMap(String key, String[] value) { if (this.parameterMap instanceof ParameterMap) { ((ParameterMap ) this.parameterMap).setLocked(false); } this.parameterMap.put(key, value); } /** * @Param * @Return java.util.Enumeration * @Description 获取所有参数名 * @Author lizelin * @Date 2023/11/3 14:59 **/ @Override public Enumeration getParameterNames() { return CollUtil.asEnumeration(parameterMap.keySet().iterator()); } /** * @Param name * @Return java.lang.String * @Description 获取指定参数名的值,如果有重复的参数名,则返回第一个的值 接收一般变量 ,如 text 类型 * @Author lizelin * @Date 2023/11/3 14:59 **/ @Override public String getParameter(String name) { ArrayList values = CollUtil.toList(parameterMap.get(name)); if (CollUtil.isNotEmpty(values)) { return values.get(0); } else { return null; } } /** * @Param name * @Return java.lang.String[] * @Description 获取单个的某个 key 的 value * @Author lizelin * @Date 2023/11/3 14:58 **/ @Override public String[] getParameterValues(String name) { return parameterMap.get(name); } /** * @Param * @Return java.util.Map * @Description 获取值列表 * @Author lizelin * @Date 2023/11/3 14:58 **/ @Override public Map getParameterMap() { return parameterMap; } /** * @Param * @Return java.lang.String * @Description 获取 queryString * @Author lizelin * @Date 2023/11/3 14:57 **/ @Override public String getQueryString() { return super.getQueryString(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } /** * @Param * @Return javax.servlet.ServletInputStream * @Description 重写获取输入流,因为在输出日志的时候会读取输入流,而流只能读取一次,所以在向后传递的时候就需要做特殊处理 * @Author lizelin * @Date 2023/11/3 14:55 **/ @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener listener) { //不需要重写 } @Override public int read() { //重写读 return byteArrayInputStream.read(); } }; } } 2.3、响应包装类
这里主要的修改点在于去掉了 response.getOutputStream().write(b, 0, b.length) 这些内容。也就是响应包装类其实主要用途在于读取数据,写数据的部分在 Filter 中实现
import lombok.extern.slf4j.Slf4j; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; /** * @ClassName LogHttpServletResponseWrapper * @Author lizelin * @Description 日志 Http Servlet 响应 包装器 * @Date 2024/5/27 18:25 * @Version 1.0 */ @Slf4j public class LogHttpServletResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream byteArrayOutputStream = null; private ResponseWrapperOutputStream servletOutputStream = null; private PrintWriter printWriter = null; public LogHttpServletResponseWrapper(HttpServletResponse response) throws IOException { super(response); byteArrayOutputStream = new ByteArrayOutputStream(); servletOutputStream = new ResponseWrapperOutputStream(byteArrayOutputStream); printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream, this.getCharacterEncoding())); } /** * @Param * @Return javax.servlet.ServletOutputStream * @Description 获取 OutputStream * @Author lizelin * @Date 2023/11/3 16:47 **/ @Override public ServletOutputStream getOutputStream() throws IOException { return servletOutputStream; } /** * @Param * @Return java.io.PrintWriter * @Description 获取 Writer * @Author lizelin * @Date 2023/11/3 16:46 **/ @Override public PrintWriter getWriter() throws UnsupportedEncodingException { return printWriter; } /** * @Param * @Return void * @Description 获取 flushBuffer * @Author lizelin * @Date 2023/11/3 16:46 **/ @Override public void flushBuffer() throws IOException { if (servletOutputStream != null) { servletOutputStream.flush(); } if (printWriter != null) { printWriter.flush(); } } /** * @Param * @Return void * @Description 重置流 * @Author lizelin * @Date 2023/11/3 16:46 **/ @Override public void reset() { byteArrayOutputStream.reset(); } /** * @Param * @Return String * @Description 读取流中的数据 * @Author lizelin * @Date 2023/11/3 16:43 **/ public String getBody() throws IOException { //刷新缓冲区 flushBuffer(); //读取流中的数据 return new String(byteArrayOutputStream.toByteArray(), StandardCharsets.UTF_8); } public byte[] getBodyBytes() throws IOException { //刷新缓冲区 flushBuffer(); //读取流中的数据 return byteArrayOutputStream.toByteArray(); } /** * @Param str * @Return void * @Description 写入数据 * @Author lizelin * @Date 2024/5/29 17:33 **/ public void setBody(String str) throws IOException { flushBuffer(); byteArrayOutputStream.reset(); for (byte item : str.getBytes()) { servletOutputStream.write(item); } } /** * @Param * @Return * @Description 内部类,对 ServletOutputStream 进行包装,方便日志记录 * @Author lizelin * @Date 2023/11/3 16:43 **/ private class ResponseWrapperOutputStream extends ServletOutputStream { private ByteArrayOutputStream bos = null; public ResponseWrapperOutputStream(ByteArrayOutputStream stream) { bos = stream; } /** * @Param b * @Return void * @Description 将写暂存一份方便日志 * @Author lizelin * @Date 2023/11/3 17:31 **/ @Override public void write(int b) throws IOException { bos.write(b); } /** * @Param b * @Return void * @Description 将写暂存一份方便日志 * @Author lizelin * @Date 2023/11/3 17:32 **/ @Override public void write(byte[] b) throws IOException { bos.write(b, 0, b.length); } @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { //不需要重新 } } }
2.4、实现加解密
这部分的步骤实际上和日志输出的思路基本上一样
import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletOutputStream; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @ClassName LogHandlerInterceptor * @Author lizelin * @Description 日志请求入参过滤器 * @Date 2023/11/3 12:15 * @Version 1.0 */ @Slf4j @Component @AllArgsConstructor public class LogRecordRequestFilter implements Filter, Ordered { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; //这里主要是由于流不可重复读取,采用包装类的方式 LogHttpServletRequestWrapper requestWrapper = new LogHttpServletRequestWrapper(httpServletRequest); String requestWrapperBody = requestWrapper.getBody(); //数据解密 requestWrapper.setBody(ParametersSecureUtil.requestDecrypt((String)requestWrapperBody)); HttpServletResponse httpServletResponse = (HttpServletResponse) response; LogHttpServletResponseWrapper responseWrapper = new LogHttpServletResponseWrapper(httpServletResponse); chain.doFilter(requestWrapper, responseWrapper); //未加密数据 String responseWrapperBody = responseWrapper.getBody(); //数据加密 responseWrapper.setBody(ParametersSecureUtil.responseEncrypt((String)responseWrapperBody)); byte[] bodyBytes = responseWrapper.getBodyBytes(); response.setContentLength(bodyBytes.length); //输出加密数据 ServletOutputStream outputStream = response.getOutputStream(); outputStream.write(bodyBytes); outputStream.flush(); } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
2.5、效果展示
请求数据为加密数据
控制台数据为解密数据
响应结果为加密数据
对加密的响应结果进行解密,输出结果为 P2 中希望返回的数据
三、总结
SpringBoot 的前后端加解密内容基本上就完成了,整体比较简单,基本上就是日志的思路。
只是需要注意的是示例中的内容只对请求 body 中的内容进行解密操作。
也就是我没写路径传参加解密,主要是因为如果代码比较严格的话,是不允许 POST 请求的时候带 QueryString 的,然后 GET 请求一般 url 参数也没必要加密,但是不排除奇怪的需求或者是屎山代码的情况。
判断一下请求的 Method 然后获取 QueryString 就能做了
下一章再更新 通过 Spring 的 RequestBodyAdvice 和 ResponseBodyAdvice 的实现。
还没有评论,来说两句吧...