2026-02-16 15:57:19

本来想按照惯例,整点那种“排比句+由浅入深”的拜年吉祥话,最好再配一张红彤彤的配图作为开头。但手放在键盘上那一刻,我反悔了。那太不“秋风于渭水”了,而且大家肯定已经在微信群里被这种“排比信息轰炸”给搞麻了吧?(其实主要是我懒得去搜词了)。
在这个短视频满天飞、大家注意力只有 3 秒、恨不得把所有内容都浓缩进 15 秒 BGM 里的时代,屏幕前的大家居然还愿意静下心来,看我写的一堆枯燥教程,听我为了几百兆内存碎碎念,为了解决个隐藏登录页絮絮叨叨写小几千字,甚至忍受我这个“随缘更新”(说好听点叫佛系,难听点就是拖延症晚期)的更新频率。
你们是我继续在这个互联网角落里“瞎折腾”的最大动力。
过去这一年,博客的数据起起伏伏。有时候精心打磨的干货反响平平,有时候随便吐槽两句“大厂负优化”反而引起了共鸣。但这都不重要,重要的是,后台每一个陌生或熟悉的 ID,每一条“博主帮大忙了”的留言,都让我觉得每次“浪费”掉的这一天半天的摸鱼时间,很值。
新的一年,我就不祝大家财源广进这种虚的了。
作为技术圈子的人,祝大家:
感谢大家听我絮絮叨叨写这么多。
小盆友一蹦一跳的过来通知我去包饺砸了。咱们明年(下次更新时)见。
除夕快乐🎇🧨
The post 祝大家除夕快乐|辞暮尔尔 烟火年年 appeared first on 秋风于渭水.
2026-02-11 10:57:02
今天(2月7号)在 RSS 订阅器里看到宗宗酱的文章《WordPress后台经常被扫》,讲了他感觉自己经常被扫描、被暴力破解,于是更改后台登录地址的事情。我本来看个标题和(RSS阅读器AI写的)摘要,想去评论区“同病相怜”一下的。
结果点进文章链接里一看人家的防火墙截图:本月暴力破解合计 274 次。
我再看看我的防火墙摘要:在我写下这段文字的时间是下午 14 点,当天暴力破解计数已经是 3355 次了,从周统计数据看,本周平均每天都有超过 5000 次的暴力破解。

别人每天被暴力破解十几次就感觉“经常”被暴力破解了,我这都每天几千次了还那么淡定,过分了过分了啊,过于佛系了
虽然因为我的密码长度很长,且登录页允许密码错误尝试次数仅为2次,加上还有 2FA(两步验证)的存在,想要靠暴力破解密码来登录后台几乎是不可能的事情。但这个性能损失实在是太大了。
每一次恶意的登录尝试,WordPress 都要启动 PHP 进程、查询数据库、验证哈希,这都是实打实的服务器资源消耗。这就像虽然家里装了防盗门(2FA)还有个保安(安全插件)只要2次尝试打不开门就直接撵走,但每天有 5000 个人来敲门试锁也不行啊,虽然他们都进不来,但吵也被吵死了。
于是,我决定给登录页做一个“物理隐身”:只有对上了暗号的人,才能看到登录框,否则直接在服务器层面掐断连接。
我要实现的效果很简单:
wp-login.php 访问 -> 直接拦截(最好连 404 页面都不给,直接断开连接,省流量)。wp-login.php?sky -> 正常显示登录页。But there is no redirection via the plugin, the default URL of WordPress(但是没有通过插件重定向 WordPress 的一些默认URL)这是个很多人没注意到的这个插件的缺陷。以及他毕竟是基于 PHP 的还是太重了点。直接搜一下“不用插件隐藏WordPress登录页”,满屏都是下边这个示例代码。
add_action('login_enqueue_scripts', 'tb_wp_login_protection');
function tb_wp_login_protection(){
if( !isset($_GET['sky']) ){
header( 'Location: ' . home_url() );
exit;
}
}
我定睛一看,这不就纯纯自欺欺人嘛,这代码仅检测 $_GET['sky'],对 POST 请求完全无效。本质上他只是让登录页/wp-login.php不带参数时,无法在浏览器内打开,问题是你只是隐藏个登录页的打开有毛线用,谁家暴力破解登录是打开登录页手动的尝试登录的。自动化脚本都是直接对 /wp-login.php 提交POST请求的啊!
这段破代码不知道毒害了多少人了,写教程的人真的自己用过这段代码吗?真的懂 WordPress 吗?嗯?Look My Eyes!
查了一下资料,WordPress登录是这样的过程
WordPress 登录流程:
├── 0. login_init
├── 1. login_head
├── 2. login_enqueue_scripts
├── 3. 输出HTML
├── 4. 用户提交表单
└── 5. 验证登录
示例代码是在login_enqueue_scripts阶段插入的,这个时候其实已经加载了一部分资源了,所以应该用 login_init ,在加载登录脚本前就进行验证,这样就可以节约大量的性能。
add_action('login_init', 'tb_wp_login_protection');
function tb_wp_login_protection() {
$secret_key = 'sky'; // 自定义密钥
// 同时验证 GET 和 POST 请求
if( !isset($_GET[$secret_key]) && !isset($_POST[$secret_key]) ) {
header('Location: ' . home_url());
exit;
}
}
部署上去,效果立竿见影。
我用隐身窗口访问 wp-login.php,直接报错。带上参数访问,登录页出来了。
稳!
然而打脸来得太快,刚才还在嘲笑别人垃圾代码。现在当我美滋滋地输入账号密码,点击“登录”按钮的一瞬间,页面它白屏了(403)。
我再仔细一看提交时的网络请求。擦,WordPress 的登录表单默认是提交给 POST /wp-login.php 的。点登录按钮后提交的地址里 没!有!带!上!暗!号!!PHP 接到 POST 请求时发现没参数,这不就直接就拦截了吗?。
于是我去拷问了一下 Gemini 怎么改按钮的链接,结果这时候,我发现自己犯了两个错误。
$_GET 获取的是URL查询字符串参数,并不关心HTTP请求方法是 GET 还是 POST 亦或其他。$_POST获取的是请求体(body)参数,不包含URL参数。所以最开始那个示例代码其实能拦截到直接POST /wp-login.php的。
但既然他能拦截到POST /wp-login.php,但是点击登录按钮提交的 URL 还是 WordPress 默认不带参数的那个,自己把自己拦住了。所以那个示例代码仍然是实际不可用的,垃圾教程污染中文互联网
简单,修改登录表单的 action 地址就行嘛,不过刚才 Gemini 给了我两个建议
/wp-login.php?word=sky的形式,由参数名和参数两部分组成,看起来更加规范一点。add_query_arg 函数构造链接,而不是自己拼接,万一原 URL 已经有参数了呢?add_action('login_init', function() {
// 如果 URL 里没有 ?word=sky,直接 403
if ( !isset($_GET['word']) || $_GET['word'] !== 'sky' ) {
die('403 Forbidden');
}
});
// 给登录表单的提交地址也加上暗号
add_filter('login_form_action', function($url) {
return add_query_arg('word', 'sky', $url);
});
于是我继续开个隐身模式测试,这次登页出来了,登录按钮点击也能生效了,结果因为是隐私模式触发了 WordPress 的陌生设备登录安全机制,发了一封“验证链接”的邮件给我。我点击邮件里的链接,结果……您猜怎么着,诶,它又双叒叕是 403 Forbidden!
WordPress 发出的系统邮件(包括找回密码、重置密码、邮箱验证、退出登录)里的链接,都是官方生成的标准链接,里面就不可能包含我的 word=sky 暗号。
既然不能改邮件里的链接,那就得在拦截逻辑里开“白名单”。让合法的系统动作(通过 GET 里的 action 参数判断),比如 lostpassword(找回密码); confirm_admin_email(验证邮箱)之类的动作,即使没有暗号也予以放行。
于是现在隐藏 WordPress 登录页的 PHP 逻辑变成了现在这样
add_action('login_init', function() {
// 1. 如果 URL 里没有 ?word=sky
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) return;
// 2. 白名单动作
// 这些是Gemini告诉我的 WordPress 系统邮件功能会用到的 action 参数
$allowed_actions = array(
'logout',
'lostpassword',
'retrievepassword',
'resetpass',
'rp',
'confirm_admin_email',
'postpass'
);
// 3. 如果当前请求的 action 在白名单里,也予以放行
if ( isset($_GET['action']) && in_array($_GET['action'], $allowed_actions) ) {
return;
}
// 既没暗号,也不是白名单动作?拦截!
die('403 Forbidden');
});
// 给登录表单的提交地址也加上暗号
add_filter('login_form_action', function($url) {
return add_query_arg('word', 'sky', $url);
});
本以为这下总该完美了吧?结果我又遇到了“最终 Boss”——WordFence 插件。
因为我开启了 Wordfence 的 2FA(两步验证),输入完密码会跳转到输入 2FA 验证码的界面,只要一点登录,哎,我是丝毫没有意外的被立马拦截了。
被坑了这么多次都已经有经验了, Wordfence 的 2FA 界面实际上是一个独立的流程,它在生成跳转链接和验证表单时,应该是直接调用了 site_url('wp-login.php')。而且,它的验证动作 action=wordfence_2fa_login 也不在我的白名单里。
wordfence_2fa_login 加入白名单。login_url 和 site_url 过滤器,全局劫持 WordPress 生成的所有登录相关链接,强行把暗号“焊死”在链接上。// A.拦截不带暗号的请求
add_action('login_init', 'protect_login_protection_pro');
function protect_login_protection_pro() {
// 1. 有暗号?放行
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) {
return;
}
// 2. 定义白名单
$allowed_actions = array(
'logout', 'lostpassword', 'retrievepassword', 'resetpass',
'rp', 'confirm_admin_email', 'postpass', 'wordfence_2fa_login'
);
// 3. 检查白名单与但做 POST 限制
if ( isset($_GET['action']) && in_array($_GET['action'], $allowed_actions) ) {
// 放过WordFence的2FA
if ( $_SERVER['REQUEST_METHOD'] === 'POST' && $_GET['action'] !== 'wordfence_2fa_login' ) {
http_response_code(403);
die('403 Forbidden: POST not allowed for whitelist.');
}
return;
}
// 4. 拦截
http_response_code(403);
die('403 Forbidden.');
}
// 全局劫持链接
// B.全局劫持所有登录链接
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
// C.全局劫持表单提交地址
add_filter('login_form_action', 'protect_add_secret_smart');
function jiestyle_add_secret_smart($url) {
return add_query_arg('word', 'sky', $url);
}
function protect_force_secret_smart($url, $path, $scheme) {
if (strpos($path, 'wp-login.php') !== false) {
$url = add_query_arg('word', 'sky', $url);
}
return $url;
}
部署新版代码,隐私模式测试,完美,没有任何问题!搞定收工!……了吗?
等我进入后台时想起了一个巨大的问题,全局劫持所有登录链接固然解决了,POST地址不带参数时导致的问题,但刚才第三回合中提到了一个事情,却被我遗忘了:“按照目前的设计,如果攻击者访问白名单动作(比如找回密码),系统生成一个按钮返回。因为有全局注入代码,这个按钮的链接里就会包含暗号!攻击者查看返回的源码就能拿到暗号!”
这就陷入了一个两难境地:
curl -I默认登录页,状态不是 200 OK 就跳过站点了)
不注入吧:自己登录时,表单提交地址里没暗号,点登录就报错。
那就继续打补丁呗,如果当前页面是白名单页面(没带暗号),就不要在生成的链接里加暗号;当前页面已经是正确登录页(带了暗号)时,才继续劫持链接补上暗号。把前边代码 全局劫持链接 部分全删了,替换成下边这个。找回密码之类的时候,麻烦就麻烦点吧。
// 1. 只有当“当前请求”本身就带有暗号时,才激活注入逻辑!
if ( isset($_GET['word']) && $_GET['word'] === 'sky' ) {
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('login_form_action', 'protect_add_secret_smart');
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
}
// 2. 特殊情况:如果是 2FA,允许注入
else if ( isset($_GET['action']) && $_GET['action'] === 'wordfence_2fa_login' ) {
add_filter('login_url', 'protect_add_secret_smart', 10, 3);
add_filter('login_form_action', 'protect_add_secret_smart');
add_filter('site_url', 'protect_force_secret_smart', 10, 3);
}
// 注入
function jiestyle_add_secret_smart($url) {
return add_query_arg('word', 'sky', $url);
}
function jiestyle_force_secret_smart($url, $path, $scheme) {
if (strpos($path, 'wp-login.php') !== false) {
$url = add_query_arg('word', 'sky', $url);
}
return $url;
}
虽然 PHP 代码已经完美工作了,但我看着后台日志陷入了沉思。
每天 5000 次爆破,虽然都被 PHP 拦截了,但每一次请求,Nginx 都要把请求转发给 PHP-FPM,PHP 都要启动进程、加载环境、执行代码、输出 403。这依然是在消耗服务器资源嘛,虽然 WordPress 核心并没有完整启动,性能消耗并不算很大
但如果我能在 Nginx 层面就直接拦截,让这些请求连 PHP 的面都见不着,岂不是更省资源?
给你们看一组实测的直观对比:
Nginx 方案:Nginx 是基于异步事件驱动的,处理这样一个带参数判断的请求,内存开销不到 2MB,而 CPU 占用几乎是 0.1% 甚至更低。
于是,我把上面的 PHP 逻辑“翻译”成了 Nginx 配置,利用 Nginx 的 return 444(该状态码会直接关闭 TCP 连接,不返回任何数据),让攻击者的扫描器直接超时或连接重置,真正实现登录页的“物理隐身”。
高效的 Nginx 防御配置,专门针对 WordPress wp-login.php 进行保护(放在 server 块中):
# ⚡️ Nginx 登录页隐身术
# 默认开启拦截 (1)
set $block_login 0;
# 1. 只有访问 wp-login.php 时才开启检测
if ($uri ~* "^/wp-login\.php") {
set $block_login 1;
}
# 2. 【规则一】如果携带了正确的暗号 word=sky,放行
if ($arg_word = "sky") {
set $block_login 0;
}
# 3. 【规则二】白名单动作 (对应 PHP 里的白名单)
# 必须包含 wordfence_2fa_login 否则 2FA 会挂
if ($arg_action ~* "(logout|lostpassword|retrievepassword|resetpass|rp|confirm_admin_email|postpass|wordfence_2fa_login)") {
set $block_login 0;
}
# 4. 执行拦截:直接返回 444 (关闭连接,连 HTTP 头都不给)
if ($block_login = 1) {
return 444;
}
现在的架构是:

这就是我如何把每天 5000+ 次的报警日志变成 0 的全过程。
PS:最近我换了博客的出口机(最近新加坡到国内非优化线路三网全都绕美),从新加坡普通线路换到香港三网优化线路,大家访问本站的速度有没有感觉变快一点。
The post WordPress后台每天被爆破5000次?用 Nginx + PHP 让 WordPress 登录页“物理隐身”! appeared first on 秋风于渭水.
2026-01-27 16:49:53
时光倒流回好几年前,那时我的 PHP 和 JS 水平还停留在相当小白的阶段(虽然现在也还是业余),用现在流行的话说,就是一名标准的 “CV 程序员”(Ctrl+C / Ctrl+V)。
当时看到一些 SEO 教程说,不要直接引用外链,不然网站权重会丢失,不利于SEO优化云云,虽然那时候博客一个月的访问量加起来都没现在一天多,我还是决定搞一个外链跳转页,实现外链SEO优化。那时候的我,写代码基本靠搜。我当时在网上东拼西凑,这里复制一段 PHP 代码,那里又找来一段 JS 脚本,缝缝补补一通 Ctrl+C / Ctrl+V 算是把功能跑通了。

那时候的我觉得自己做得挺周全:
eval、base64 这种敏感词,防止有人注入代码。看着这套“组合拳”,我心想:“这下稳了,既防注入又防盗链,妥妥的。” 这一用,就是好几年。
部署后的头几年,一切风平浪静,看起来跳转页在忠实的工作着。直到大前年开始,因为《本地部署AI文生图工具 SD-webui 生成NSFW图》部署教程被广泛引用,博客的流量和热度突然上去了。那小半年的时间里,每日新访客(仅仅是新访客就)能稳定在 4位数,高质量反链好几个。在 Google 和 Bing 眼中,我的博客权重逐渐变高。
于是乎,树大招风,我的外链跳转页被盯上了。
最开始的端倪,是 Google Search Console 发来的“提示”。网域出现了大量“未编入索引”的提示,我点进去一看,全是 /goto/?url=… 后面跟一长串乱七八糟的垃圾网站链接(赌博、色情、灰产,应有尽有)。

紧接着,防火墙(WAF)开始频繁报警。日志显示,有大量的请求携带着奇怪的参数试图经过我的跳转页做XSS或者SQL注入。这是典型的 Open Redirect(开放重定向)漏洞利用尝试。
面对这些攻击,当时的我虽然觉得烦,但并没有意识到问题的严重性。毕竟博客前台看着没啥异常,服务器也没崩,搜索引擎也没收录这些奇奇怪怪的跳转。
于是,一向 “佛系” 的我采用了最机械的应对方式:

“反正也没造成什么实质性的破坏,能拦就拦,拦不住貌似也没啥。” 就这样,我拖着这个隐患,得过且过地又混了两年多。
直到最近,那种久违的“折腾之魂”突然死灰复燃。趁着手里有干劲,我决定把这个陈年老页面彻底重构一下。
当我打开那个尘封已久的 index.php,仔细审视当年的代码时,冷汗下来了。不看不知道,一看吓一跳——当年我所谓的“安全措施”,简直就是“千疮百孔”,甚至是在对黑灰产说着“欢迎光临”,代码幼稚的简直想穿回去抽自己的嘴巴子。
当年写下(复制来)的防止非本站使用跳转页的代码是这样的
{
//禁止其他网站使用我们的跳转页面
// 第一步:获取我们自己的域名
var MyHOST = new RegExp("<?php echo $_SERVER['HTTP_HOST']; ?>");
// 第二步:判断来源
if (!MyHOST.test(document.referrer)) {
// 第三步:如果来源不对,准备跳转回首页
location.href="https://" + MyHOST;
}
// 第四步:正常执行跳转
location.href="<?php echo $url;?>";
}
看出问题来没?我感觉稍微有点开发经验的朋友都看出来了,因为跳转逻辑被覆盖了!!
我当年知道 JavaScript 是按顺序向下执行的,所以我想当然的认为当代码执行了location.href="https://" + MyHOST;之后,非法访问就会被跳转到我的首页了,后面的location.href="<?php echo $url;?>";不会被执行。
但实际上,修改(赋值) location.href 后代码其实会继续执行下去的,实际执行时是下边这样的过程

当年的我犯了初学者最常见的认知错觉,是把 JavaScript 的 location.href 当成了 PHP 里的 header('Location: ...');了,殊不知在浏览器眼里,这只是一次变量赋值。(PS:其实PHP里这样写也是错的,后面需要写exit;,不然可能用户浏览器已经跳走了,但服务器还在空跑)
浏览器是单线程执行 JS 的。当它读到我的第一次赋值时,它在心里记下:“哦,待会儿脚本跑完了我要去首页”。但是!脚本还没跑完呢,它必须继续往下跑。 紧接着它读到了第二次赋值:“哦,不对,他改主意了,待会儿脚本跑完了,让我去外链”。 后面的赋值覆盖了前面的赋值。
就像我告诉网约车司机‘去机场’,结果还没等司机踩油门,我又补了一句‘去火车站’。那司机肯定听最后一句啊!缺少一个 else,让我的防御代码变成了一句废话。
也就是说,这里正确的写法应该写成
var MyHOST = new RegExp("<?php echo $_SERVER['HTTP_HOST']; ?>");
if (!MyHOST.test(document.referrer)) {
location.href = "https://" + MyHOST;
} else {
location.href = "<?php echo $url;?>";
}
亦或者封装成一个函数用return打断函数继续执行也可以
var MyHOST = new RegExp("<?php echo $_SERVER['HTTP_HOST']; ?>");
function CheckHOST() {
if (!MyHOST.test(document.referrer)) {
location.href = "https://" + MyHOST;
return; // <--- 让函数立即停止
}
location.href = "<?php echo $url;?>";
}
顺带一提,这个错误的代码至今仍在谷歌搜索结果的前五位,而且被多个外链跳转页所使用。😂
filter_var 重构安全跳转页痛定思痛,我彻底抛弃了原来的代码,基于 PHP 服务端重写了整个逻辑。
原因很简单,正确的 JS 代码当然可以在跳转被恶意利用时拉回用户,但这无法阻拦黑产的自动化漏洞扫描。
扫描漏洞用的爬虫、脚本(Python、Curl 、Go 等)根本就不执行 JS 的!它们只看 HTTP 响应头和 HTML 里的链接。 在扫描器眼里,我的旧代码压根没有那个 if 判断,他直接看到了最后的跳转链接。扫描器只会给我的各种路径去发?url=http://www.baidu.com之类的命令遍历尝试,看会不会触发跳转,只要触发跳转到百度的首页,就标记为“存在 Open Redirect 漏洞”,自动存入“可用资产库”。于是就会被拿来做跳转了,至于实际环境访问时能不能完成跳转,不讲究的黑产并不会去核实。只是因为有 JS 的存在,实际用户访问时会被拦截罢了。
现在来源检查在服务器端完成,不依赖客户端。直接在 PHP 顶部加入了核心校验:
// 获取来源
$referer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
$host = parse_url($referer, PHP_URL_HOST);
// 只有从我自己域名点出来的链接,才放行
if (!in_array($host, ['tjsky.net', 'www.tjsky.net'])) {
header('HTTP/1.1 403 Forbidden');
die('非法请求:禁止直接访问或盗链。');
}
当年真是的小白菜的常见状态,又菜又感觉自己强,当年的 URL 合法性检查是自己手搓的
strpos($_SERVER['REQUEST_URI'], "eval(") ||
strpos($_SERVER['REQUEST_URI'], "base64")||
strpos($_SERVER['REQUEST_URI'], "127.0.0.1")||
……
//省略其他过滤语句
$t_url = preg_replace('/^url=(.*)$/i','$1',$_SERVER["QUERY_STRING"]);
//判断非空
if(!empty($t_url)) {
//判断取值是否是base64
if ($t_url == base64_encode(base64_decode($t_url))) {
$t_url = base64_decode($t_url);
}
我当时防御漏洞的逻辑很直观:黑客可能想干嘛,我就拦什么。黑客想传 eval,我就在代码里搜 eval;黑客想传 base64,我就搜 base64。这在安全领域叫“黑名单防御”,但其实:
这次重构,因为使用了PHP跳转而不是前端跳转,所以改用 PHP 内置的filter_var来检查跳转 URL 的合法性,并且先解码,再检查。再配合FILTER_FLAG_NO_PRIV_RANGE之类的参数去防止对内网和私有 IP 做跳转。
//base64解码部分代码就不写了,只看filter_var部分的。
$url_host = parse_url($final_url, PHP_URL_HOST);
//过滤本地主机
if (strtolower($url_host) === 'localhost') {
die('非法目标:你访问本地主机干啥。');
}
if (filter_var($url_host, FILTER_VALIDATE_IP)) {
//_PRIV_RANGE: 过滤 192.168.x.x, 10.x.x.x, 172.16.x.x 之类的大内网
//_RES_RANGE: 过滤 0.0.0.0, 127.0.0.1 等保留地址
if (!filter_var($url_host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
header('HTTP/1.1 403 Forbidden');
die('非法目标:你访问内网地址干啥。');
}
}
//确保链接格式符合 RFC 标准
if (!filter_var($final_url, FILTER_VALIDATE_URL)) die('目标链接格式错误。');
?url=javascript:alert(1)的话。在老代码里,这会被当作合法 URL 运行。而 filter_var 会识别出这不符合 Web 协议规范,直接在入口处将其掐死。filter_var的眼里都是不合法的它只认符合 RFC 标准的纯净 URL。当然filter_var本质上是一个“格式校验器”,挡不住顶尖高手的定向渗透,比如传入一个格式完全标准,但指向内网某个邻居的数据库的域名。filter_var 会因为它符合 URL 规范而放行。这就是所谓的 SSRF(服务端请求伪造) 风险,这类深层次的逻辑漏洞,单靠一个函数是无法完全杜绝的。但面对互联网上每天成千上万次的自动化扫描和脚本攻击,它表现得比我那些个漏洞百出的正则要好太多了。放弃对“手搓代码”的执念,承认现成工具套件,尤其是安全、密码方向的现成轮子专业能力。现在的外链跳转页代码已经是一份真正的PHP安全跳转代码了。

htmlspecialchars 防止 XSS 攻击因为现在的跳转页有一个跳转URL“预览框”。如果万一黑产绕过了前面的检查,把一段代码伪装成 URL 传了进来,直接打印在 HTML 里是非常危险的,搞不好就引发 XSS 攻击了。 htmlspecialchars会把所有的 <、>、"等特殊字符全部转义成 HTML 实体,让前端可以安全展示URL。
$display_url = htmlspecialchars($final_url, ENT_QUOTES, 'UTF-8');
额外增加 X-Robots-Tag:直接在 HTTP 标头中加入 noindex, nofollow,直接告诉搜索引擎爬虫:“这个页面通通都别收录,权重别传递”。这比在 <head> 里写 <meta name="robots" content="noindex, nofollow" /> 更优先,爬虫不需要解析 HTML 就能看到,从而更有效保护博客的 SEO 资产。(当然meta也要加,毕竟某搜索引擎的爬虫不认这个响应头)
<?php
header('X-Robots-Tag: noindex, nofollow');
本来这次对外链跳转页的重构也就止步于上一步了,有些熟悉本站的人,估计已经发现新版跳转页已经上线运行了一段时间了。
我最近在爬取一个图片资源站时,发现对方有个很有意思的设计,这个图片站为了防止遍历本地路径实现快速抓取图片资源,他把站内的图片链接都AES加密了。我只能看到他图片都是类似主域名/img/?url=OVA2Q……HBIdz09-d这样的地址。如果不知道加密密钥,就无法生成正确的资源地址。
我一琢磨,虽然我的跳转页有个来源域名检测,防止非本站访问。但 HTTP Referer 伪造起来难度也不高。要是真有人拿我的跳转页去搞伪装钓鱼的话,他自然能伪造请求头的。
于是我决定做个“二次进化”:使用 AES 加密跳转地址。
不过这个图片站的 AES 加密方案还是不太完善,因为很容易就能发现,他所有图片的开头和结尾字符串都是固定的。很明显是个使用 ECB 模式(固定使用同一个密钥)做对称加密的结果。
我打算更进一步使用 CBC 模式 (随机初始化向量)做加密,逻辑是:[ 随机生成的 16 字节 IV ] + [ AES 加密后的实际网址 ] 打包在一起再 Base64 编码。
这样服务器只要收到密文后,只需要先解码base64,截取前16个字节作为IV,用密钥和刚才拿到的IV,去解密后面的密文,就可以得到实际跳转地址。(从防止变成开放重定向跳板这个角度, ECB 模式已经能挡住黑产了,用 CBC 纯属为了“既然做了就做到极致”)
$final_url = '';
// 尝试 AES 解密
$binary_data = base64_decode($input_code);
if ($binary_data && strlen($binary_data) > 16) {
$iv = substr($binary_data, 0, 16);
$ciphertext = substr($binary_data, 16);
// 使用预设的密钥解密
$decrypted = openssl_decrypt($ciphertext, AES_METHOD, AES_KEY, OPENSSL_RAW_DATA, $iv);
if ($decrypted && strpos($decrypted, '://') !== false) {
$final_url = $decrypted;
}
}
这样带来的好处是巨大的:
1. 动态密文:因为 IV 是随机的,同一个网址每次加密出来的字符串都不一样,无法通过简单的特征分析来破解。
2. 杜绝搬运:那些喜欢“采集”我文章的爬虫(说的就是你,CSDN),搬运过去后跳转链接会直接失效,倒逼他们必须手动处理。
3. 无数据库:不需要像短链外链方案那样需要数据库来存链接白名单,我只需要保存好密钥就可以,只要解密失败,就说明这不是我自己生成的链接。
4. 防伪造:取消明文跳转和基础base64链接跳转后,可以彻底堵死跳转页可被第三方利用的可能性。只要解密失败直接不跳了。
其实从防止变成开放重定向跳板这个角度, ECB 模式已经能挡住黑产了,用 CBC 模式纯属“既然做了就做到极致”罢了。而且其实有了这个加密机制后,前边的什么来源验证啥的都可以不要了,毕竟未来只支持使用加密后的密文做跳转,只要没有密钥,第三方不可能构造出实际可用的跳转密文,也就无法利用我的跳转页。以 AES 加密的强度来看,想要根据已知「明文,密文」来反推密钥需要的算力资源过于庞大了。
咋说呢,当年作为“CV 工程师”省下的脑子,最后都变成了日志里的红字报警。
现在的跳转页,虽然功能没变,但它不再是一个过于容易被攻破的漏洞,而是一个安全、可控的流量出口。如果你也在维护类似的跳转功能,不妨也检查一下:你的跳转页的“安全检查”,是真的在检查,还是在自欺欺人?
跳转页的代码已经在Github开源啦(含详细注释,适用于 WordPress / Typecho / Z-Blog 等一切 PHP 站点):
👉 点击查看:安全的外链跳转页源码
前边这个外链的跳转就是使用AES加密后的跳转链接,只有到执行跳转时才会显示出真正的跳转目标。
The post 少写一个 else,我的外链跳转页成了黑产眼中的“香饽饽”?重构一个安全的外链跳转页(附 PHP 源码) appeared first on 秋风于渭水.
2026-01-11 00:01:06
你以为买个罗技键盘鼠标,插上就能用?太天真了。罗技的驱动历史就是一部“分裂史”。如果你像我一样,手里既有老款经典,又有新款旗舰,那你的电脑托盘区简直就是罗技图标的“连连看”。
罗技最老的管理软件叫 SetPoint、然后有了 Logitech Options,然后又变出来个 Logitech Options+ ??厂商更新软属于正常操作,但问题是:罗技的新软件它不!兼!容!旧!硬!件! 诶,这就逆天了好不。
SetPoint 下才能良好工作。在 Logitech Options 下虽能发现设备,但大部分高阶功能的配置项全不在。
Logitech Options 在才能良好工作,在 Logitech Options+ 下会缺少配置项了。
Logitech Options+ 不然看都看不到鼠标。
LGS (Logitech Gaming Software),因为新的 G Hub 不能正确支持它,依然是老问题:缺配置项。
G HUB。
以前的驱动 LGS、SetPoint 都只有几十兆,清爽无比。现在的 G HUB 和 Options+,动不动就几百兆,启动慢得像是在加载 3A 大作。

看看这界面截图,里面塞了多少不好用又很臃肿的功能。
吐槽完罗技的糟心驱动,那我们要怎么解决这个问题呢?以下是我尝试过的 3 种不同层级的解决方案。
如果你想彻底抛弃官方驱动,以下神器是你的首选。
Mac 上有个牛逼的软件叫 「SteerMouse」,也就是大家口中的“万能鼠标驱动”。它的平滑滚动算法比罗技官方还要丝滑,甚至能调整滚轮加速度曲线,让鼠标拥有匹配 Macbook 触摸板的体验。绝对值得付费购买的一款软件。
Linux 社区的反向工程做得最好。「LogiOps」 是一个运行在命令行的非官方驱动,可以使用cfg文件完美配置键盘与鼠标的所有参数。
Windows 下没有单一的完美替代品,但可以通过组合软件实现官方驱动的部分功能:
调节灯光效果:如果你只是想调你的光污染键盘,可以用 “OpenRGB”。这玩意比 G Hub 好用太多,如果只为灯效,强烈建议卸载 G Hub 只装这个。
也许是罗技仅存的良心,目前还有一些官方工具可以帮你减少垃圾驱动的影响。
如果你的需求只是配对键盘鼠标,其他什么都不需要:
* Unifying 接收器(优联):使用 「Logitech Unifying Software」,只有几兆,配对完即可卸载或关闭。
* Bolt 接收器:使用网页版工具 「logi web connect 」,连软件都不用装;如果你前边的web版不能用,可以使用 「Logi Bolt App」 独立安装包,同样配对完即可卸载或关闭。
这是罗技官方极少宣传的神器 —— Onboard Memory Manager。
对于有板载储存的键鼠,OMM 能直接读写板载内存。你可以用它设置 DPI、回报率、按键映射、灯光开关。设置完成后,即便卸载软件,配置依然保存在鼠标里。
对于像 MX Master 3/3S 这种号称“没有板载内存”的设备,其实 SmartShift 阈值、DPI 等参数会写入鼠标的临时寄存器。
* 操作方法:先安装 Logi Options+,把参数调好,然后禁止 Options+ 自启。只要不彻底断电(关掉底部开关)或者把鼠标键盘彻底用没电,大概率还能保持配置。
如果你必须使用 Options+ 的某些特定功能,但又讨厌它的臃肿,这里有一个终极解决方案。
罗技为了方便商用部署,Options+ 安装程序支持命令行禁用功能。我们可以利用这一点,只安装纯净的驱动,剔除 AI、分析、广告等垃圾组件。看下图,和文章前边那个“完整版”的Options+对比,是不是很清爽的界面。


原版项目主要针对Mac用户,而且脚本使用起来对国内用户不友好,所以我 Fork 并优化了项目 「tjsky/logi-options-plus-mini」,加入了中文界面和权限自动获取。

Windows 用户安装步骤:
1. 下载脚本压缩包并解压。
2. 双击运行 Run_Install.bat。
3. 脚本会自动检测网络环境并设置界面语言(支持从罗技中国服务器下载)。
4. 功能选择:脚本会询问你需要安装哪些组件。
* 建议输入:0 5 6 (仅基础驱动和键鼠固件更新功能)。
* 最多加个:10。(如果你需要简单的按键宏的话)
5. 输入 y 确认,脚本将自动备份配置、卸载旧驱动、并安装全新的精简版 Logi Options+ 再恢复你的配置。
6. PS“如果你运行脚本时看到的是英文界面,说明你开了全局代理让脚本误判你的地区了
无论是选择 SteerMouse 这样的第三方神器,还是使用 罗技板载内存管理器 (OMM) 实现“配置后即焚”,亦或是通过脚本 精简安装 Logi Options+,我们都能在享受罗技优秀硬件手感的同时,摆脱其糟糕的软件体验。至于那个微动计划报废问题嘛,我的建议是:除非你动手能力极强并且有足够的焊电路板的经验或者你会先练练焊电路板,否则咸鱼和淘宝有很多寄修服务或同城维修服务,找个靠谱的比自己折腾省心省力的多。
最后,希望这篇 罗技驱动优化指南 能帮你的电脑“减负”!如果你有其他更好的罗技优化技巧,欢迎在评论区分享。
The post 告别 Logitech Options+ 臃肿!罗技驱动精简瘦身与替代全攻略 appeared first on 秋风于渭水.
2026-01-07 12:11:08
熟悉我的朋友都知道,我一向是比较“佛系”的。
其实早在云服务器刚部署好,装好各种东西后,我就发现了这个奇怪的现象:明明我买的是 4GB 的规格,系统里怎么看都只有 3.2GB 左右。
当时看到这个数字,我心里的第一个念头是:“我去,服务商该不会是在给我搞‘缩水机’吧?” 难道这所谓的 4G 实例其实是 3.2G 的残次品?但紧接着,作为一名折腾多年的业余 DIY 玩家,这“3.2G”的诡异数字让我脑海里瞬间闪过一丝不祥的预感——这数值也太像 32 位系统的寻址上限了!
那一瞬间我甚至有点自我怀疑:“难道我手滑,给这台机器装了个 32 位的 Ubuntu?如果不小心把古董级的 32 位系统装在了现代 VPS 上,那可真是运维生涯的黑历史了……”
我赶紧敲了一行 uname -a 压压惊。看到屏幕上明明白白显示着 x86_64,我这才松了一口气:还好,系统没装错,脑子也还正常。
既然排除了“因为蠢装错系统”和“服务商虚假发货”的可能性(凉心云不至于的,况且一般哪有 3.5G 的机器),因为当时老机器上的原有服务已经全部完成迁移到这台新机器并上线运行了有几天了,我想着反正跑个 WordPress 加上几个 Docker 小容器,3GB 也绰绰有余,再加上那时候还是个 linux 萌新,面对折腾系统总有种恐惧感,也就懒得去深究那消失的 800MB 到底去了哪。这一拖,就是4年多过去了(这到底算佛系还是算拖延症呢)。
直到前几天,我遭遇了一次恶意的刷流攻击。虽然通过各种手段挡住了攻击,但那当时看着服务器内存水位线一直处于报警阈值边缘,我的心态终于崩了。
资源寸土寸金,在关键时刻,这“莫名失踪”的 内存可能就是压死骆驼的最后一根稻草。于是,我决定不再佛系,彻底查清这笔“内存烂账”。
虽然早就排除了 32 位系统的嫌疑,但我还是得确认一下底层的硬件规格。使用 dmidecode 命令查看底层硬件信息:
sudo dmidecode -t memory
输出结果非常明确,物理插槽上确实是实打实的 4096 MB:
Physical Memory Array
Location: Other
Use: System Memory
Maximum Capacity: 4 GB
...
Memory Device
Size: 4096 MB
Type: RAM
...
然而,当我切换回系统视角,使用 top 命令或者在面板一看,画风就变了:
MiB Mem : 3419.1 total
4096 – 3419 ≈ 677 MB。 既然物理内存没少,系统也是 64 位的,那只能说明一件事:这部分内存被操作系统内核在启动阶段就已经“私吞”了,导致用操作系统实际可支配的内存减少。
系统可用内存比机器规格小不少,在虚拟化环境(云服务器,VPS)中是非常常见的。一般无非是3个地方
现在云服务器,都有一些魔改虚拟化机制,可能会在客户机底层埋一些小玩意,这些“小玩意”会占用一点点客户机内存作为通信缓冲区,不过一般都非常克制,不至于直接砍下去 800 MB 吧
毕竟虚拟的主板 BIOS 和硬件设备也需要占用地址空间嘛,比如我还开了VNC,几十MB的虚拟显存占用总要有。但是这玩意一般最多也就吃个200M,不应该啊。
我不放心执行一下命令,看了下内存映射日志。
dmesg | grep "Memory:"
输出: Memory: 3319136K/4194304K available (14336K kernel code, ...),嗯……这,被吃的内存可真多。
kdump-config show
显示 current state: ready to kdump,说明它正在占用内存。
cat /proc/cmdline
返回的配置最后赫然写着:crashkernel=2G-16G:512M,16G-:768M
破案了!元凶就是它——Kdump。
什么是 Kdump?
Ubuntu 默认会启用 Kdump 。目的是在为了实现当系统内核崩溃时记录错误日志的功能。为了保证系统内存被用户打满再崩溃时,系统还有地方可以存放崩溃现场的数据。所以系统会在启动时预先强行划走一部分内存。这部分内存对普通应用是不可用也不可见的,因此 top 、php、Nginx 统统都统计不到。
不得不说,这也太狠了吧,2G-16G:512M,合着,物理内存在 2G 到 16G 之间,强制保留 512MB???我4G砍512MB还不太明显,2G的机器,你给人家砍 512MB,还让不让人用了?
Kdump 对于像咱们这样的个人站长,服务器主要运行 Web 服务的人,是完全没有用的,咱们并不需要在系统崩溃后查看 Kdump 保留内存转储存文件,来分析为什么系统会崩溃,面对系统崩溃我们能做到只有重启,快照,重装三板斧和报告软件作者。
所以,这 512MB 的“昂贵的买路钱”,我不交了。
sudo vim /etc/default/grub
# 当然,你要是习惯用 nano 的话 用sudo nano /etc/default/grub
找到 GRUB_CMDLINE_LINUX 这一行。
# 原配置:
GRUB_CMDLINE_LINUX="... crashkernel=2G-16G:512M,16G-:768M ..."
# 修改为(直接禁用)
GRUB_CMDLINE_LINUX="... crashkernel=0M ..."
注意:
1. 保留该行内原有的其他参数,只修改 crashkernel 部分
2. 如果你的参数是写在GRUB_CMDLINE_LINUX_DEFAULT里的话,那就改这里的参数。
这一步至关重要,你不更新引导文件,改了配置也没用。
注意:执行后,服务器(系统)会被重启
sudo update-grub
sudo reboot
重启服务器后,第一时间打开终端输入 free -m.可用总内存从 3.2GB 瞬间跃升到了 3.9G。舒坦,这么多年拖延的问题终于解决了。虽然平时看着不起眼,但在流量高峰期或者 Docker 容器跑得比较多的时候,它就是从卡顿到流畅运行之间的那道分界线。如果你也发现你的 VPS 内存“缩水”严重,不妨检查一下 /etc/default/grub,说不定也能找回这笔“私房钱”。
3419Mib/1024/1024X1000X1000=3.26G内存可用,然后面板还隐藏了百分位,变成了3.2G,然后我(4G-3.2G)X1000=800MB,得出了“我擦,怎么少了快800MB”的结论。正确算法应该是 4096-3419=677MB 不过琢磨了下,我决定还是按照当时的实际心路历程去如实写吧。The post VPS内存缩水排查:消失的 800MB 内存去哪了? appeared first on 秋风于渭水.
2026-01-04 16:53:40

大概从(2025年的)年初开始,我的电脑出现了一个非常诡异的“玄学”故障。绝对算得上是我遇到过最离谱的 Windows 点击链接无反应 故障
起初是在 Telegram 上,朋友发来的消息如果带有 google.com链接(比如搜索结果分享 www.google.com/search?q=...),我点击链接后什么都不会发生。
注意,是真正的“什么都没发生”:浏览器没有弹出,没有报错,仿佛我的点击被黑洞吞噬了一样。
但奇怪的是,点击其他任何链接(比如 github.com,baidu.com,youtube.com)都能正常唤起浏览器。
但极其偶尔的时候(也就2次),如果我关机关一半又取消了,这时候是可以点击 Telegram 中的 google 链接,自动调用浏览器打开的,只不过这时候因为魔法工具没启动,得到的只是一个无法连接的网页。
当时这个问题对我的影响不算大,毕竟只是 Google 的链接嘛,大不了我手动复制链接扔进浏览器里打开。我甚至一度以为是 Telegram 的 Bug (毕竟用的不是官方客户端)也就没当回事,就这样“带病生存”了大半年。
直到前几天,我的 Antigravity 登录掉了。
当我点击登录界面的「Sign in with Google」按钮时,熟悉的死寂再次降临 —— 按钮按下去,浏览器没有弹出,没有任何反应。我没法登录了。
这问题就严重了。我开始意识到,这不是 Telegram 单个软件的问题。经过一番简单的测试,我发现事情比我想象的要严重得多:
https://google.com,回车,无反应。start https://google.com,回车,无反应,也不报错,只有鼠标光标会变成繁忙状态1秒左右,然后恢复原样。这就不仅是“玄学”了,这是系统级的 URL Scheme 劫持。作为一个爱折腾的人,我决定彻底揪出这个“幽灵”。
既然是 Google 打不开,我首先想到的自然是网络或浏览器问题,毕竟国内嘛。
https://google.com.jp,秒开。.com是Windows 可执行文件后缀。我试着输入 https://google.com/(加了尾部斜杠强制识别为 URL),依然打不开。那就可以排除系统将其误判为本地文件的可能。https://ww.google.com、https://myaccount.google.com,秒开。输入 https://www.google.com、https://google.com、https://accounts.google.com,故障出现。ww.google.com和myaccount.google.com的。google.com、accounts.google.com等个别域名,非常精确。这就非常有意思了。既然浏览器没问题,网络没问题,那问题一定出在 “我发出打开URL指令” 到 “浏览器收到指令” 的中间环节。
既然 CMD 运行 start https://google.com 没有任何报错信息,并且有进入繁忙状态,说明那个拦截请求的进程要么是在“沉默中灭亡崩溃”了,要么“处理了不告诉我偷偷的”。我打开了 Windows 事件查看器 (Event Viewer),先试图寻找系统崩溃的蛛丝马迹。
果不其然,在 Windows 日志 -> 应用程序 中,我抓住了一个刚刚发生的“应用程序错误”:

任务类型:应用程序奔溃事件
出错应用程序名称: WsaClient.exe
出错模块名称: ucrtbase.dll
异常代码: 0xc0000409 (堆栈缓冲区溢出)
将日志清空后再次执行打开 google 链接的操作,可以再次复现。
WsaClient.exe?这踏马不是 Windows Subsystem for Android (WSA) 吗?
看到 WSA 我反应过来了
我安装的是 GitHub 上修改过的 MagiskOnWSA 版本 WSA,里面集成了 GApps(谷歌全家桶)。
虽然这个版本的 WSA 很香,但安卓系统有一种机制叫 App Links,应用可以向系统注册它支持的域名。显然,WSA 里的 Google App 极其霸道地向 Windows 宣示了主权:“所有 google.com 等域名都归我管,别给浏览器。”

这个图已经是我修复后的了,之前 Google App 足足关联了 17 种链接。
故障流程还原:
google.com。而 Office 里的链接之所以能打开,估计是微软自家的 Office 内部有更高级的超链接处理逻辑,绕过了系统的这一层默认关联,链接直接发给了浏览器。
既然找到了真凶,卸载 WSA 当然能解决,但我还想留着它用。我需要的是切断关联。
但这比我想象的难得多:
pm disable-user 把安卓端的 Google App 冻结了。结果 Windows 居然还是固执地调用 WSA,然后继续崩溃,甚至我都把 WSA 给关闭了,Windows 还是锲而不舍的去呼叫 WSA ,就像对着一具尸体喊话。这就很绝望了。说明 Windows 系统内部有一份独立的、不受 UI 控制的注册表清单,记录着这个错误的关联。
没办法,只能动用注册表编辑器 (regedit) 进行物理切除。
经过反复按 F3 地毯式搜索 google.com ,我终于在深层的 AppModel 路径下找到了这几个“僵尸”条目:

计算机\HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\SystemAppData\MicrosoftCorporationII.WindowsSubsystemForAndroid_8wekyb3d8bbwe\AppUriHandlers\www.google.com
这里明明白白地写着 AppUriHandlers\www.google.com。就是这几行注册表项,像幽灵一样死死抓着我的google.com链接不放,导致我无法在电脑中点击链接打开谷歌的网页。
解决方法:
https://google.com
https://google.com

困扰我一年的“玄学”故障,竟是因我在 WSA 里装了个谷歌全家桶的需求。
这个故事告诉我们:
gmail.app.goo.gl、m.instagram.com
如果你也遇到点击某个特定网站毫无反应 Windows 链接跳转失效的情况,不妨查查是不是有什么奇怪的 应用 产生了 URL Scheme劫持 。希望这篇排错记录能帮到有同样困扰的朋友。
The post 记一次“闹鬼”的故障:Windows 点击 Google 链接毫无反应,竟是安卓子系统 (WSA) 惹的祸? appeared first on 秋风于渭水.