当前位置: 首页 > news >正文

日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现

在这里插入图片描述

文章目录

  • 日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现
    • 一、概述
    • 二、通过 Filter 的方式实现
      • 2.1、加解密工具类
      • 2.2、请求包装类
      • 2.3、响应包装类
      • 2.4、实现加解密
      • 2.5、效果展示
    • 三、总结

日志输出-第四章-接口级(单体应用)前后端数据加解密 Filter 实现

前置内容

  1. 日志输出指南
  2. 日志输出-第二章-接口级出入参的实现
  3. 日志输出-第三章-接口级出入参输出完整数据的实现

一、概述

上一章内容为如何输出完整数据,但是一般情况下还是会采用第二章的实现方式(因为输出 body 会影响性能),上一章的处理方式实际上更多是用于处理前后端数据的加解密。

本章的内容实际上并不属于 日志输出的范围 而是对上一章的内容进行了衍生(因为日志输出是需要在流量的出入口做处理,前后端数据加解密也是在流量的出入口做处理,并且都是对 body 数据处理),是 SpringBoot 项目如何处理前后端数据加解密问题(SpringCloud 版本的加解密会在后续日志写到相应版本后再更新)。

一般情况下的做法分为两种:

  1. 通过 Filter 的方式,也就是和我们上一章的日志输出一样,只是将输出日志改为对数据进行加解密就可以了。
  2. 通过 SpringRequestBodyAdviceResponseBodyAdvice 来实现。

两种的实现难度都差不多,唯一的区别在于 通过 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**/@Overridepublic 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**/@Overridepublic void write(byte[] b) throws IOException {bos.write(b, 0, b.length);response.getOutputStream().write(b, 0, b.length);}@Overridepublic boolean isReady() {return false;}@Overridepublic 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<String, String[]> 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();// 获取Bodybody = RequestResponseUtil.getRequestBody(request);}/*** @Param parameterMap* @Return void* @Description 替换整个参数 Map* @Author lizelin* @Date 2023/11/3 14:59**/public void setParameterMap(Map<String, String[]> 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<String, String[]>) this.parameterMap).setLocked(false);}this.parameterMap.put(key, value);}/*** @Param* @Return java.util.Enumeration<java.lang.String>* @Description 获取所有参数名* @Author lizelin* @Date 2023/11/3 14:59**/@Overridepublic Enumeration<String> getParameterNames() {return CollUtil.asEnumeration(parameterMap.keySet().iterator());}/*** @Param name* @Return java.lang.String* @Description 获取指定参数名的值,如果有重复的参数名,则返回第一个的值 接收一般变量 ,如 text 类型* @Author lizelin* @Date 2023/11/3 14:59**/@Overridepublic String getParameter(String name) {ArrayList<String> 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**/@Overridepublic String[] getParameterValues(String name) {return parameterMap.get(name);}/*** @Param* @Return java.util.Map<java.lang.String, java.lang.String [ ]>* @Description 获取值列表* @Author lizelin* @Date 2023/11/3 14:58**/@Overridepublic Map<String, String[]> getParameterMap() {return parameterMap;}/*** @Param* @Return java.lang.String* @Description 获取 queryString* @Author lizelin* @Date 2023/11/3 14:57**/@Overridepublic String getQueryString() {return super.getQueryString();}@Overridepublic 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**/@Overridepublic ServletInputStream getInputStream() throws IOException {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener listener) {//不需要重写}@Overridepublic 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**/@Overridepublic ServletOutputStream getOutputStream() throws IOException {return servletOutputStream;}/*** @Param* @Return java.io.PrintWriter* @Description 获取 Writer* @Author lizelin* @Date 2023/11/3 16:46**/@Overridepublic PrintWriter getWriter() throws UnsupportedEncodingException {return printWriter;}/*** @Param* @Return void* @Description 获取 flushBuffer* @Author lizelin* @Date 2023/11/3 16:46**/@Overridepublic 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**/@Overridepublic 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**/@Overridepublic void write(int b) throws IOException {bos.write(b);}/*** @Param b* @Return void* @Description 将写暂存一份方便日志* @Author lizelin* @Date 2023/11/3 17:32**/@Overridepublic void write(byte[] b) throws IOException {bos.write(b, 0, b.length);}@Overridepublic boolean isReady() {return false;}@Overridepublic 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 {@Overridepublic 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();}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}
}

2.5、效果展示

请求数据为加密数据

image-20240529221647616

控制台数据为解密数据

image-20240529221818628

响应结果为加密数据

image-20240529221910720

对加密的响应结果进行解密,输出结果为 P2 中希望返回的数据

image-20240529222006713

三、总结

SpringBoot 的前后端加解密内容基本上就完成了,整体比较简单,基本上就是日志的思路。

只是需要注意的是示例中的内容只对请求 body 中的内容进行解密操作。

也就是我没写路径传参加解密,主要是因为如果代码比较严格的话,是不允许 POST 请求的时候带 QueryString 的,然后 GET 请求一般 url 参数也没必要加密,但是不排除奇怪的需求或者是屎山代码的情况。

判断一下请求的 Method 然后获取 QueryString 就能做了

下一章再更新 通过 SpringRequestBodyAdviceResponseBodyAdvice 的实现。

相关文章:

  • 设计模式 17 组合模式 Composite Pattern
  • 网页设计步骤总结
  • C++ Qt:QString与数字之间的相互转换
  • es和mongdb对比
  • Ai速递5.29
  • 0.25W 1.5KVDC~3KVDC 隔离超小型单输出 DC/DC 电源模块——TKE-W25系列
  • 重磅发布,2024精选《制造业商业智能BI最佳实践合集 》
  • 电量计量芯片HLW8110的前端电路设计与误差分析校正.pdf 下载
  • 一个程序员的牢狱生涯(44)询问
  • MOS管开关电路简单笔记
  • MySQL建库
  • stable Diffusion缺失模型补充地址
  • Android 版本与 API level 以及 NDK 版本对应
  • 原神抽卡点名程序教程(直接下载用)
  • 爬虫案例-亚马逊反爬分析-验证码突破(x-amz-captcha)
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • extjs4学习之配置
  • iOS 颜色设置看我就够了
  • Java多态
  • JSDuck 与 AngularJS 融合技巧
  • miaov-React 最佳入门
  • node 版本过低
  • PHP 程序员也能做的 Java 开发 30分钟使用 netty 轻松打造一个高性能 websocket 服务...
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • webpack入门学习手记(二)
  • 从PHP迁移至Golang - 基础篇
  • 搭建gitbook 和 访问权限认证
  • 缓存与缓冲
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 前端 CSS : 5# 纯 CSS 实现24小时超市
  • 深入 Nginx 之配置篇
  • 正则与JS中的正则
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​创新驱动,边缘计算领袖:亚马逊云科技海外服务器服务再进化
  • ​渐进式Web应用PWA的未来
  • #pragma data_seg 共享数据区(转)
  • #每天一道面试题# 什么是MySQL的回表查询
  • #知识分享#笔记#学习方法
  • (bean配置类的注解开发)学习Spring的第十三天
  • (Demo分享)利用原生JavaScript-随机数-实现做一个烟花案例
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (四)事件系统
  • (转)用.Net的File控件上传文件的解决方案
  • (转载)PyTorch代码规范最佳实践和样式指南
  • .net 简单实现MD5
  • .NET 使用 ILRepack 合并多个程序集(替代 ILMerge),避免引入额外的依赖
  • .net后端程序发布到nignx上,通过nginx访问
  • .net生成的类,跨工程调用显示注释
  • ::
  • @KafkaListener注解详解(一)| 常用参数详解
  • [.net]官方水晶报表的使用以演示下载
  • [Android]竖直滑动选择器WheelView的实现
  • [Bugku]密码???[writeup]
  • [BZOJ 3282] Tree 【LCT】