基于 Gin 的 HTTP 中间人代理 Demo

前面实现的代理对于 HTTPS 流量是进行盲转的,也就是说直接在 TCP 连接上传输 TLS 流量,但是我们无法查看或者修改它的内容。当然了,通常来说这也是不必要的。不过对于某些场景下还是有必要的,例如使用 Fiddler 进行抓包或者监控其它电脑的流量传输等,这就需要用到中间人代理了。所以说,学习一下这一块的内容,对于更好的使用 Fiddler Charles 或者 mitmProxy 这类软件是很有帮助的。遥想起来,好几年前我还在大学的时候,学会使用 Fiddler 去抓包,当时就觉得很神奇。我当时特别喜欢使用它来学习 HTTP 协议的结构,或者开安卓模拟器来抓 APP 的包,不过我当时对于代理的概念几乎是一无所知。现在,也很少再玩这些东西了,反而对代理的了解更进一步了。

在继续阅读之前,最好还是对一下概念有一个简单的了解:

  • 非对称加密和对称加密
  • 公钥和私钥
  • 签名
  • 证书

一、中间人代理的概念

一图胜千言,我们先来看几张图片吧。
在这里插入图片描述

这是前面介绍的通过 TCP 隧道的透明代理的一个简单示意图,数据是通过 TCP 上发送的二进制 TLS 流量,代理无法解密这些数据,它只是负责对这些数据进行盲转发,所以说它是透明代理。所以即使经过了代理服务器,客户端实际上也是和服务器进行的 TLS 握手,然后通信的,这种情况下通信是安全的。

在这里插入图片描述

这是我们接下来要介绍的中间人代理了,它同样是一个代理,但是这次它会假装自己是客户端需要访问的服务器,然后客户端实际上是同中间人进行通信,代理再同服务器进行通信(甚至没有这一步!),这样就可以查看、修改客户端和服务器通信的内容了(这种情况下通信是不安全的)。

注:中间人,Man In The Middle,简称 MITM。

在这里插入图片描述

网络是分层的,这就是 HTTPS 协议的示意图。所以知乎上有一个博主说了一句话,我认为挺有道理的,大致意思是:没有 HTTPS 协议,只有 HTTP 协议。

所以对于我们接下来的内容,即实现一个中间人代理的关键是解决客户端和中间人代理之间的 TLS 握手。我们先来使用命令行看一下访问 https://www.baidu.com 的过程(大致了解一下,我也只是理解一点),首先是连接到目标主机的 443 端口,这是 TCP 连接。然后开始建立 TLS 连接,可以看到会进行协议握手(handshake)(握手就是进行协商一个双方共同接受的条件,协议版本或者密码套件之类的)。

在这里插入图片描述

使用浏览器访问百度,查看它的证书,这样比在命令行里面更方便一些。这里我们主要关注这个 CN 字段,其它的我也不了解。

在这里插入图片描述

前面这些图应该可以有一个大致的概念了。我们要做的就是代替真正的服务器和客户端进行握手(相当于是欺骗它我就是你需要访问的服务器),但是我们不可能搞到真正服务器的证书(私钥)的。不过,任何人都可以很方便的生成一个自己公钥和私钥,它和那些由CA签发的证书来说,区别只是它不是受信任的。

节选自 《HTTP权威指南》第 14 章节 安全的 HTTP
在发送已加密的 HTTP 报文之前,客户端需要和服务器进行一次 SSL 握手,在这个握手过程中,它们要完成以下工作:

  • 交换协议版本号
  • 选择一个两端都了解的密码
  • 对两端的身份进行验证
  • 生成临时的会话密钥,以便加密信道。

这里的第三点,对两端身份进行验证,其实通常是没有的。我们一般只对服务器的身份进行验证,即它确实是我们要访问的服务器(服务器一般不会要求验证客户端的身份)。当然也有要求客户端进行验证的,例如说 Fiddler 也抓不了支付宝的数据包。据说是因为它使用了双端验证,即使你可以假扮服务器,但是没有假扮客户端去和真正的服务器进行交互(我听说支付宝的客户端证书是内置的,所以中间人代理是没法抓到它的数据包的)。所以中间人代理实际上也就是钻了这个漏洞,否则大部分的 HTTPS 流量都难以解密了。

二、实现细节

所以我们要做的工作也就大致清晰了,下面是一个简单的示意图,圆角矩形表示这中间的协议变化。
首先客户端通过 HTTP 协议和代理服务器建立 TCP 连接(在 Go 中,是通过 Hijack 获取底层 TCP 连接的)。之后客户端会在这个 TCP 连接上发送 TLS 的内容。所以我们把这个连接转成 TLS 连接(就是说和客户端进行握手),如果成功之后我们就会被认为是真的服务器了(实际上是假的),因为现在就是单纯的 HTTP 通信了,所以可以自由的查看和修改报文的内容了。

在这里插入图片描述

生成本地的自签名证书,这里的 CN 字段,指定为 baidu.com

在这里插入图片描述

然后我们来看一下,关于中间人代理这块的代码:

这里的变化也就是多加了这一个函数,也就是在劫持获得 TCP 连接后,把它作为参数传入这个函数。这个函数的功能主要就是加载公钥和私钥,然后把 TCP 连接变成 TLS 连接,接着就可以进行 HTTP 的请求和响应了。

func mitm(conn net.Conn) error {
	tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return err
	}
	// 再把底层连接转换成 tls 连接,然后再封装成 http request
	tlsConn := tls.Server(conn, &tls.Config{ // 这里有一个概念叫 客户端连接和服务端连接
		PreferServerCipherSuites: true,
		MaxVersion:               tls.VersionTLS13,
		Certificates:             []tls.Certificate{tlsCert}, // 这里最关键的是这个证书的配置
	})
	defer tlsConn.Close()

	// 封装成 http request
	var req *http.Request
	if req, err = http.ReadRequest(bufio.NewReader(tlsConn)); err != nil {
		return err
	}

	req.RequestURI = ""      // 这里创建一个新的请求或者把这个设置为 空,不然会报错
	req.URL.Scheme = "https" // 中途转换的 Request 丢失了一些信息,居然没有 scheme 了
	req.URL.Host = req.Host

	var resp *http.Response
	if resp, err = proxyHttpClient.Do(req); err == nil {
		err = resp.Write(tlsConn)
	}

	return err
}

我之前一直没有想明白这一块,我也看了很多其它人的实现,但是就是这一点没有想明白。然后昨天看了一个外国的博文,发现居然就是这样的,还是对这个网络这一块的了解太少了,哈哈。我刚开始想到了把它转成 TLS 连接,但是我在想那怎么再变成 HTTP 连接呢?但是,仔细一想,HTTP 是一个文本协议,实际上我可以直接对这个 TLS 连接上读取或者返回符合 HTTP 报文格式的数据,所以最后写入响应数据就是 resp.Write(tlsConn),虽然这只是简短的一行,却困惑了我好久!!!

Talk is cheap, show me your code

我就是看的这篇博客然后才了解了最后困扰我的那部分内容,这个作者写的真的不错!

Go and Proxy Servers: Part 2 - HTTPS Proxies

注:这里使用的是 tls.Server,这个包下还有一个 tls.Client 的方法。所以它这里是为了区分客户端连接和服务端连接。我的理解是这样的,如果你打算发起一个请求,你肯定需要的是客户端连接。反过来,如果你已经得到了一个请求,你打算处理它,它就是服务端连接。

三、完整代码和测试

package main

import (
	"bufio"
	"crypto/tls"
	"io"
	"log"
	"net"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

const (
	RPOXY_SERVER  = "CrazyDragonHttpProxy" // it is just a kidding, but Only HTTP!
	TUNNEL_PACKET = "HTTP/1.1 200 Connection Established\r\nProxy-agent: CrazyDragonHttpProxy\r\n\r\n"
	certFile      = "./server.crt"
	keyFile       = "./server.key"
)

var proxyHttpClient = http.DefaultClient

func main() {
	r := gin.Default()
	r.Use(recordReq)      // 使用 gin 的中间件简单打印请求的信息
	r.NoRoute(routeProxy) // NO Route is every Routes!!!
	r.Run(":8888")
}

// 这样就可以处理所有的请求了,在这里区分是 http 还是 https,https 会通过 CONNECT 来建立隧道,所以就通过它来区分。
func routeProxy(c *gin.Context) {
	req := c.Request
	if req.Method == http.MethodConnect {
		httpsProxy(c, req) // 处理 HTTPS 请求(HTTPS => TCP <-> TLS <-> HTTP )
	} else {
		httpProxy(c, req) // 处理 HTTP 请求
	}
}

func httpsProxy(c *gin.Context, req *http.Request) {
	c.Status(200) // 不加也没有问题,只是说记录的日志状态码都会变成 404.
	// 这里接管这个 http 连接,然后将它劫持为一个 TCP 连接
	hj, ok := c.Writer.(http.Hijacker)
	if !ok {
		http.Error(c.Writer, "webserver doesn't support hijacking", http.StatusInternalServerError)
		return
	}
	clientConn, _, err := hj.Hijack()
	if err != nil {
		http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
		return
	}
	// 建立隧道之后,发送一个连接建立的响应(其实,只要状态码是 200 就可以了)
	if _, err = clientConn.Write([]byte(TUNNEL_PACKET)); err != nil {
		log.Printf("Response Failed: %v", err.Error())
	} else {
		log.Println("Response Success.")
	}
	// 因为我们知道了客户端需要访问的 HOST+PORT,所以现在我们需要来模拟一个假的服务器(即中间人)来和它进行对话。
	// 所以这里的关键是这个模拟的服务需要有证书,不然无法和客户端进行握手。
	err = mitm(clientConn)
	if err != nil {
		log.Println(err)
		return
	}
}

func httpProxy(c *gin.Context, req *http.Request) {
	req.RequestURI = "" // 这里创建一个新的请求或者把这个设置为 空,不然会报错
	resp, err := proxyHttpClient.Do(req)
	if err != nil {
		log.Println(err)
		return
	}
	defer resp.Body.Close()
	c.Status(resp.StatusCode) // 修改状态码,因为默认的是 404(我是在 NoRoute 里面处理的)。
	for k, v := range resp.Header {
		c.Header(k, strings.Join(v, ",")) // 写入头部(全部写入严格来说是不对)。
	}
	c.Header("Server", RPOXY_SERVER) // haha, it just a kidding!!!
	io.Copy(c.Writer, resp.Body)     // 响应数据给客户端
}

func recordReq(ctx *gin.Context) {
	log.Printf("Method: %s, Host: %s, URL: %s, Version: %s\n", ctx.Request.Method, ctx.Request.Host, ctx.Request.URL.Path, ctx.Request.Proto)
}

func mitm(conn net.Conn) error {
	tlsCert, err := tls.LoadX509KeyPair(certFile, keyFile)
	if err != nil {
		return err
	}
	// 再把底层连接转换成 tls 连接,然后再封装成 http request
	tlsConn := tls.Server(conn, &tls.Config{ // 这里有一个概念叫 客户端连接和服务端连接
		PreferServerCipherSuites: true,
		MaxVersion:               tls.VersionTLS13,
		Certificates:             []tls.Certificate{tlsCert}, // 这里最关键的是这个证书的配置
	})
	defer tlsConn.Close()

	// 封装成 http request
	var req *http.Request
	if req, err = http.ReadRequest(bufio.NewReader(tlsConn)); err != nil {
		return err
	}

	req.RequestURI = ""      // 这里创建一个新的请求或者把这个设置为 空,不然会报错
	req.URL.Scheme = "https" // 中途转换的 Request 丢失了一些信息,居然没有 scheme 了
	req.URL.Host = req.Host

	var resp *http.Response
	if resp, err = proxyHttpClient.Do(req); err == nil {
		err = resp.Write(tlsConn)
	}

	return err
}

启动代码后,使用 curl 访问 https://www.baidu.com,这里要加 -k 参数跳过证书验证(我这个自签名证书是不受信任的,默认是关闭连接的)。

在这里插入图片描述

如果不加 -k 的话,证书验证过不去,连接会被终止。

在这里插入图片描述

启动代码后,配置好代理的配置(可以看前面的博客的配置,注意端口号这里我换了,因为 8888 被占用了),使用浏览器访问 https://www.baidu.com

在这里插入图片描述
遇到问题了,使用不受信任的证书,浏览器禁止我访问百度了,因为它采用了更严格的安全策略 HSTS。因为通常情况下,即使证书不安全,我也可以坚持访问的。不过现在这样还就是麻烦了呢。

HSTS(HTTP Strcit-Transport-Securit)即 HTTP 严格传输安全, 是一种 Web 安全策略机制,可保护网站免受协议降级攻击和 cookie 劫持、中间人攻击。它允许 Web 服务器声明浏览器(或其他符合要求的用户代理)应用使用安全的 HTTPS 连接与交互,而不是通过不安全的 HTTP 协议。

HSTS 详解,让 HTTPS 更安全

不过幸好,这个是可以关闭的。我用的是 edge 浏览器,输入下面的网址进入这个页面,在下面输入百度的域名,删除动态的 HSTS。不过它也提示了预加载的是没有用的。这个应该是临时的,最好还是开启它,因为这是涉及安全的问题!

在这里插入图片描述

好了,现在终于可以访问了 www.baidu.com 了,这里只是打开了首页截了几张图片。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

四、信任证书和证书链

证书通常是由某些很权威的机构来颁发的,一般称为 CA。这里的颁发,指的是用 CA 的私钥来对证书进行签名(这样别人就无法伪造证书了,因为没有办法签名)。验证证书就是用 CA 的公钥对证书进行解密并验证。 并且逻辑上证书是一条链的结构,首先是根证书,然后是中间证书,最后是服务端证书。

根证书 --> 中间证书 --> 服务端证书

这样是从颁发证书的角度看的,实际上验证证书的时候是反过来的,从服务端证书开始,一直到根证书,都验证通过了才是通过。

现在这个代理服务器,还是有很多问题的,首先它只能访问 https://www.baidu.com,其次它会报红色警告。第一个是因为我只是生成了一个假的 baidu.com 的证书,所以对于其它的网址直接是过不去检验的(会检查域名和CN的匹配关系,然后是校验证书的有效性)。如果域名和证书不匹配,那么直接就是错误了,如下图。

在这里插入图片描述

这个红色警告是因为这个证书是自签名证书,天然就是不被信任的,当然也有解决办法了。我们来看看 Fiddler 是怎么解决的,它是把自己的证书安装到了系统的根证书(根证书是默认信任的)中,然后用这个来给我们访问的域名签发假的证书(可以看到全部是假的证书,而且它的名字叫 不要信任 Fiddler 根证书)。这里就没有中间证书了,证书校验就是服务端证书和根证书了。

在这里插入图片描述

在这里插入图片描述

我这里只是用了一个自签名证书,因为我只演示了百度这一个域名,所以就不需要再弄一个根证书了。而且,我也不想安装把这个安装到系统的根证书,因为这样很危险。所以,如果想要访问各种不同的网站,需要自己来为每一个访问的域名签发证书,这就需要一个 CA 证书来操作了,这样只需要把一个证书加入系统根证书就好了。如果想要扩展的话,应该会用到下面这个方法来动态的生成证书,不过这个主题的探索对于我来说已经差不多要结束了,因为关于证书这一块还是挺多不熟悉的。如果你也阅读到这里,相信你对于 Fiddler、Charles 和 MitmProxy 的也会有更多的理解,对于我们使用这些工具是很有帮助的。

在这里插入图片描述

这一块的内容还是挺多的,我也只是了解一点,如果想要看更多的内容的话,可以看下面的这两个技术博客,写得很好。

证书链简介

HTTPS 精读之 TLS 证书校验

五、总结

这篇博客和上一篇博客之间已经隔了好久了。因为理解这个中间人代理的过程遇到了困难,再加上时间不是很充足,也就没有继续写这个主题的内容。最近刚好又有了时间了,所以就集中时间看了很多内容,测试代码(因为 TSL 发生了错误基本上看不懂什么意思,感觉自己掌握的知识和工具还是太少了,很多错误只能束手无策了),也算是对这个东西有了一个新的理解。我其实还是更喜欢盲转发的代理,因为那样实现起来更简单,可以做一些上网行为统计的小工具玩一玩。对于这种中间人代理,因为已经有了很成熟的工具了,所以就当做是了解怎么样更好的使用这些工具了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/232632.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Git 五分钟教程速度入门

Git 五分钟教程速度入门 分类 编程技术 许多人认为 Git 太混乱&#xff0c;或认为它是一种复杂的版本控制系统&#xff0c;其实不然&#xff0c;这篇文章有助于大家快速上手使用 Git。 入门 使用Git前&#xff0c;需要先建立一个仓库(repository)。您可以使用一个已经存在的…

HLS实现图像膨胀和腐蚀运算--xf_dilation和xf_erosion

一、图像膨胀和图像腐蚀概念 我们先定义&#xff0c;需要处理的图片为二值化图像A。图片的背景色为黑色&#xff0c;即像素值为0。图片的目标色为白色&#xff0c;即像素值为1。 再定义一个结构元S&#xff0c;结构元范围内所有的像素为白色&#xff0c;像素值为1。 1、图像的…

自下而上-存储全栈(TiDB/RockDB/SPDK/fuse/ceph/NVMe/ext4)存储技术专家成长路线

数字化时代的到来带来了大规模数据的产生&#xff0c;各行各业都面临着数据爆炸的挑战。 随着云计算、物联网、人工智能等新兴技术的发展&#xff0c;对存储技术的需求也越来越多样化。不同应用场景对存储的容量、性能、可靠性和成本等方面都有不同的要求。具备存储技术知识和技…

HarmonyOS应用开发-闪屏启动页

这是鸿蒙开发者网站的一个应用《溪村小镇》示例代码&#xff0c;把闪屏启动页单拿出来&#xff0c;分析一下代码。 一、先上效果图 这是应用打开时的一个启动页&#xff0c;启动页会根据三个时间段&#xff08;白天、傍晚、晚上&#xff09;来分别展示溪村小镇不同的景色。 二…

RocketMQ-RocketMQ高性能核心原理与源码剖析(下)

融汇贯通阶段 ​ 开始梳理一些比较完整&#xff0c;比较复杂的完整业务线。 8、消息持久化设计 1、RocketMQ的持久化文件结构 ​ 消息持久化也就是将内存中的消息写入到本地磁盘的过程。而磁盘IO操作通常是一个很耗性能&#xff0c;很慢的操作&#xff0c;所以&#xff0c;…

MyBatis--07--启动过程分析、SqlSession安全问题、拦截器

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 谈谈MyBatis的启动过程具体的操作过程如下&#xff1a;实现测试类,并测试SqlSessionFactorySqlSession SqlSession有数据安全问题?在MyBatis中&#xff0c;SqlSess…

App备案、ios备案Bundle ID查询、公钥信息、SHA-1值

App备案、ios备案Bundle ID查询、公钥信息、SHA-1值 Bundle ID这个就不说了&#xff0c;都知道是啥&#xff0c;主要说公钥信息和SHA-1值的获取 打开钥匙串访问&#xff0c;找到当前需要备案App的dis证书&#xff0c;如下&#xff1a; #####右键点击显示简介 #####可以看…

ThinkPHP生活用品商城系统

有需要请加文章底部Q哦 可远程调试 ThinkPHP生活用品商城系统 一 介绍 此生活用品商城系统基于ThinkPHP框架开发&#xff0c;数据库mysql&#xff0c;前端bootstrap。系统分为用户和管理员。(附带配套设计文档) 技术栈&#xff1a;ThinkPHPmysqlbootstrapphpstudyvscode 二 …

小米手机锁屏时间设置为永不休眠_手机不息屏_保持亮屏

环境&#xff1a;打开手机自带的锁屏时间设置发现没有 永不息屏的选项 原因&#xff1a;采用了三星OLED屏幕&#xff0c;所以根据OLED屏幕特性&#xff0c;这个是为了防止烧屏而特意设计的。非OLED机型支持设置“永不” 解决方案1&#xff1a;原生系统是支持永不锁屏的&#…

【自定义Source、Sink】Flink自定义Source、Sink对redis进行读写操作

使用ParameterTool读取配置文件 Flink读取参数的对象 Commons-cli&#xff1a; Apache提供的&#xff0c;需要引入依赖ParameterTool&#xff1a;Flink内置 ParameterTool 比 Commons-cli 使用上简便&#xff1b; ParameterTool能避免Jar包的依赖冲突 建议使用第二种 使用Par…

STL(七)(map篇)

### 这里重点学习map ### 在实际做题过程中,multimap几乎用不到### unordered_map拥有极好的平均时间复杂度和极差的最坏时间复杂度,所以他的时间复杂度是不稳定的,unordered_map一般用不到,要做一个了解 1.map map是一种关联容器,用于存储一组键值对(key-value pairs),其中每…

鸿蒙开发组件之Slider

一、Slider控件是鸿蒙开发中的滑动条组建&#xff0c;初始化方式 Slider({min:0, //最小值max:100,//最大值value:30,//默认值step:10,//步长&#xff0c;每次滑动的差值style:SliderStyle.OutSet, //滑块的样式&#xff0c;默认outsetdirection:Axis.Horizontal, //水平方式的…

Transformer 简介

Transformer 是 Google 在 2017 年底发表的论文 Attention Is All You Need 中所提出的 seq2seq 模型。Transformer 模型的核心是 Self-Attention 机制&#xff0c;能够处理输入序列中的每个元素&#xff0c;并能计算其与序列中其他元素的交互关系的方法&#xff0c;从而能够更…

【自定义Source、Sink】Flink自定义Source、Sink对ClickHouse进行读和批量写操作

ClickHouse官网文档 Flink 读取 ClickHouse 数据两种驱动 ClickHouse 官方提供Clickhouse JDBC.【建议使用】第3方提供的Clickhouse JDBC. ru.yandex.clickhouse.ClickHouseDriver ru.yandex.clickhouse.ClickHouseDriver.现在是没有维护 ClickHouse 官方提供Clickhouse JDBC…

【小沐学Python】Python实现语音识别(SpeechRecognition)

文章目录 1、简介2、安装和测试2.1 安装python2.2 安装SpeechRecognition2.3 安装pyaudio2.4 安装pocketsphinx&#xff08;offline&#xff09;2.5 安装Vosk &#xff08;offline&#xff09;2.6 安装Whisper&#xff08;offline&#xff09; 3 测试3.1 命令3.2 fastapi3.3 go…

【数据结构】——排序篇(上)

前言&#xff1a;前面我们已经学过了许许多多的排序方法&#xff0c;如冒泡排序&#xff0c;选择排序&#xff0c;堆排序等等&#xff0c;那么我们就来将排序的方法总结一下。 我们的排序方法包括以下几种&#xff0c;而快速排序和归并排序我们后面进行详细的讲解。 直接插入…

C#注册表技术及操作

目录 一、注册表基础 1.Registry和RegistryKey类 &#xff08;1&#xff09;Registry类 &#xff08;2&#xff09;RegistryKey类 二、在C#中操作注册表 1.读取注册表中的信息 &#xff08;1&#xff09;OpenSubKey()方法 &#xff08;2&#xff09;GetSubKeyNames()…

2-Spring

2-Spring 文章目录 2-Spring项目源码地址Spring概述Spring特点&#xff08;优点&#xff09;Spring相关学习网站基于Maven的Spring框架导入Spring的组成及拓展 Spring-IOC--原型理解IOC-原型--示例开发示例-常规开发示例-Set函数&#xff08;IOC原型&#xff09;开发示例-对比思…

Python-pdf工具自制(合并、拆分、删除)

pdf工具&#xff0c;之前写的合并工具有点麻烦&#xff0c;使用PyQt5库重写合并拆分和删除指定页面的程序 实现如图&#xff1a; 代码&#xff1a; import sysimport osfrom PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget, QFileDia…

新版Android Studio 正则表达式匹配代码注释,删除注释,删除全部注释,IntelliJ IDEA 正则表达式匹配代码注释

正则表达式匹配代码注释 完整表达式拼接Android Studio 搜索匹配【IntelliJ IDEA 也是一样的】 完整表达式拼接 (/*{1,2}[\s\S]?*/)|(//[\x{4e00}-\x{9fa5}].)|(<!-[\s\S]?–>)|(^\s\n)|(System.out.println.*) 表达式拆解&#xff0c;可以根据自己需求自由组合&#x…