防红接口实现全解析:UA识别与跳转网关核心逻辑
2026/9/22 5:44:11 网站建设 项目流程

简介:一份面向网站运营者与推广人员的防红接口源码,基于PHP开发,支持对已红域名在微信/QQ内直接打开,并集成直连、跳转、短链接生成及防红界面。系统自带访问记录,可查看访问者IP和访问方式;前台支持用户注册、升级权限、购买套餐与生成独立KEY,后台可管理套餐并对接易支付,普通用户或会员可自定义接口域名。包体共369个文件,约4.1MB,以86个PHP业务逻辑文件、82个JS交互脚本、36个CSS样式为主,另有HTML模板、SQL数据库文件及少量字体图片资源,结构清晰便于二次开发。当前已有208人学习下载。资源为v2.2版本,包含完整源码及更新记录,重点处理了注册邮件发送、支付回调、工单回复、域名重复生成等问题,并加入动态次数效果、用户独立生成统计、续费自动更换域名和彩虹代码,方便快速部署一套带会员体系的独立防红系统。

1. 我理解的“防红接口”要解决什么问题

一条链接放进微信或 QQ 的聊天窗口后,平台会先派爬虫抓一次页面。如果域名、路径或关键词触发规则,真实用户再打开就是“已停止访问该网页”。所谓防红接口,就是在爬虫抓取时给一套干净页面,在真实点击时再放行到目标地址。它本质是双分支跳转网关:同一个接口地址,按访问方身份返回不同内容。

这套系统真正难的不是跳转,而是访问源识别。常见的做法是用 UA 和 Referer 区分微信、QQ、普通浏览器,再分别走中间提示页和直连跳转。下文直接拆解表结构、跳转接口、易支付对接和上线调参,目标是让你在服务器上搭出最小可用版本。

2. 防红系统的任务表与最小表结构

2.1 三种访问源,三套业务分支

防红接口收到请求后,先不急着跳转,而是先回答三个问题:这个请求是从哪个 App 的内置浏览器发出的?这个请求对应的原始链接是什么?我要不要给这个来源直连权限?

针对这三个问题,系统一般会分出三个分支。微信内置浏览器,UA 里带MicroMessenger,走中间提示页,让用户复制链接到外部浏览器;QQ 内置浏览器,UA 里带Mobile QQQQBrowser,走独立中间页或直接跳转;普通浏览器,比如 Chrome、Safari,直接返回公开提示页。

为什么三个分支不能返回同样内容?因为平台爬虫在抓取时使用的 UA 大概率是普通浏览器 UA。如果你把普通浏览器的行为也设置成直接跳转,爬虫就会跟着到目标站,目标域名和页面内容一旦被抓取识别,问题就在源头被定位。所以普通浏览器分支返回公开页面或 404,是整个系统最重要的安全边界。

2.2 字段设计:一个跳转任务需要哪些字段

防红系统的核心表是一张跳转任务表,每条记录代表一个对外可访问的短链。我一般会这样设计字段:

字段类型说明
idint unsigned主键
task_keyvarchar(32)对外短链码,例如 aB3xY
target_urlvarchar(255)真实落地地址,仅对放行来源开放
check_uatinyint(1)是否启用 UA 识别
direct_uavarchar(255)允许直连的 UA 关键词,多个用逗号分隔
allow_sharetinyint(1)是否允许被转发
expire_timedatetime链接过期时间
statustinyint(1)0 停用,1 启用
create_timedatetime创建时间

关键字段是direct_ua。它存的是可以放行的 UA 关键词集合,而不是完整 UA。完整 UA 会带版本号,今天允许的版本明天就被平台换掉,用关键词匹配比完整匹配稳定得多。比如MicroMessenger这个字符在微信 6.x 到 8.x 的 UA 里一直稳定存在。

2.3 用 SQL 初始化一张可用的防红任务表

CREATE TABLE `anti_red_task` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `task_key` varchar(32) NOT NULL COMMENT '对外短链码', `target_url` varchar(255) NOT NULL COMMENT '真实落地地址', `check_ua` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用UA识别', `direct_ua` varchar(255) NOT NULL DEFAULT 'MicroMessenger,QQ' COMMENT '允许直连的UA关键词', `allow_share` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否允许被转发', `expire_time` datetime DEFAULT NULL COMMENT '过期时间', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0停用 1启用', `create_time` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_task_key` (`task_key`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='防红跳转任务表';

task_key要建唯一索引,因为对外短链码需要在接口里被快速查询;status建普通索引,用于后台管理页筛选启停记录。expire_time可以先不建索引,等数据量大到需要按期清理时再补。

表里direct_ua默认值写了MicroMessenger,QQ,这是针对微信和 QQ 内置浏览器的两个最常用关键词。实际生产不要只依赖默认值,平台对 UA 的调整策略经常变化,第 5 章会专门讲这个参数怎么调。

3. 直连防红接口的核心代码:识别来源与下发目标地址

3.1 UA 识别:微信、QQ、普通浏览器怎么区分

先明确一点:UA 不是可信的,防红系统不会把 UA 当成唯一信任来源,它是第一层筛选。

在 PHP 里,$_SERVER['HTTP_USER_AGENT']能拿到当前请求的 UA 字符串。微信内置浏览器的 UA 里几乎都有MicroMessenger,QQ 内置浏览器常见的标识是Mobile QQQQBrowser;另外经典特征是AndroidiPhone这类操作系统标识出现在靠前的位置。

把判断逻辑封装成函数:

<?php function checkSourceEnv(string $ua): string { $ua = strtolower($ua); if (strpos($ua, 'micromessenger') !== false) { return 'wechat'; } if (strpos($ua, 'mobile qq') !== false || strpos($ua, 'qqbrowser') !== false) { return 'qq'; } return 'normal'; }

逻辑很简洁:UA 转小写后分别查找micromessengermobile qqqqbrowser,返回值对应微信、QQ 和普通浏览器。注意不要写成if (strpos(...)),因为目标关键词出现在 UA 开头时strpos返回0,而0是假值,会漏判。必须用!== false判断。

3.2 直连跳转接口的完整实现

拿到来源环境后,下一步决定怎么处理这次请求。下面这段代码是一个最小可用的直连防红跳转接口,放在jump.php

<?php // 防红直连跳转接口,入口参数:?k=task_key $taskKey = $_GET['k'] ?? ''; if ($taskKey === '') { http_response_code(400); exit('invalid request'); } // 从数据库读取任务记录 $pdo = new PDO('mysql:host=127.0.0.1;dbname=anti_red', 'root', 'password'); $stmt = $pdo->prepare('SELECT * FROM anti_red_task WHERE task_key = ? AND status = 1 LIMIT 1'); $stmt->execute([$taskKey]); $task = $stmt->fetch(PDO::FETCH_ASSOC); if (!$task) { http_response_code(404); exit('task not found'); } if ($task['expire_time'] && strtotime($task['expire_time']) < time()) { http_response_code(410); exit('task expired'); } $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $sourceEnv = checkSourceEnv($ua); // 普通浏览器:返回干净的提示页,不要直接 302 if ($sourceEnv === 'normal') { header('Content-Type: text/html; charset=utf-8'); exit('<html><head><title>提示</title></head><body><p>请在微信或QQ中打开</p></body></html>'); } // 允许直连的 UA 关键词校验 $directUaList = array_filter(array_map('trim', explode(',', $task['direct_ua']))); $allowed = false; foreach ($directUaList as $keyword) { if (stripos($ua, $keyword) !== false) { $allowed = true; break; } } if (!$allowed) { file_put_contents( __DIR__ . '/logs/block.log', date('Y-m-d H:i:s') . ' ' . $ua . PHP_EOL, FILE_APPEND ); http_response_code(403); exit('forbidden'); } // 放行:302 跳转到真实落地地址 header('Location: ' . $task['target_url'], true, 302); exit();

代码分四步:校验参数、查询任务并检查过期、判断来源环境、按来源分支决定提示或跳转。直连的意义在这里体现:判定为普通浏览器的请求一律不走目标地址,只返回静态提示页;只有 UA 命中direct_ua关键词的来源才走 302。302 对用户无感知,但后续链路仍会被目标站日志记录,所以安全边界不在跳转本身,而在跳转之前的判断。

3.3 白名单校验与日志记录

UA 伪造成本很低,所以需要在跳转前加第二道校验:Referer 白名单。

<?php $referer = $_SERVER['HTTP_REFERER'] ?? ''; $allowedReferers = ['qzone.qq.com', 'wx.qq.com', 'mp.weixin.qq.com']; $refHost = strtolower(parse_url($referer, PHP_URL_HOST) ?? ''); if (!in_array($refHost, $allowedReferers, true)) { file_put_contents( __DIR__ . '/logs/referer_block.log', date('Y-m-d H:i:s') . ' ' . $referer . PHP_EOL, FILE_APPEND ); http_response_code(403); exit('referer forbidden'); }

这段代码校验来路域名,只放行三个白名单域名。注意parse_url对没有传Referer的请求返回null,所以要先做空值合并,用?? ''兜底。微信内通过聊天窗口打开时,Referer大概率是https://wx.qq.com/https://mp.weixin.qq.com/;但从扫码或小程序进入时,部分场景Referer为空。所以 Referer 校验建议做成可配置项,默认开启,允许按任务维度关闭,否则会误伤扫码流量。

4. 对接易支付:签名、下单与回调的完整链路

4.1 易支付接口的参数约定

易支付是一套第三方聚合支付协议,市面上很多支付发包都按同一个参数规范实现。一个标准的易支付下单接口需要提交这些参数:

参数名是否必填说明
pid商户ID
type支付方式,如 alipay、wxpay
out_trade_no商户侧订单号
notify_url异步回调地址
return_url同步跳转地址
name商品名称
money金额,单位元,保留两位小数
sign签名
sign_typeMD5

参数多但核心只有两个:签名算法和回调验签。易支付的标准签名规则是:把所有参数按参数名 ASCII 从小到大排序,拼接成key1=value1&key2=value2,末尾拼接商户密钥,做 MD5,得到 32 位字符串,作为sign字段值。

4.2 生成签名并发起下单请求

下面代码展示如何生成签名并请求支付下单接口:

<?php function sign(array $params, string $key): string { ksort($params); $str = ''; foreach ($params as $k => $v) { if ($v === '' || $k === 'sign' || $k === 'sign_type') { continue; } $str .= $k . '=' . $v . '&'; } $str = trim($str, '&') . $key; return md5($str); } $params = [ 'pid' => '1000', 'type' => 'alipay', 'out_trade_no' => '202501010001', 'notify_url' => 'https://jump.example.com/pay/notify.php', 'return_url' => 'https://jump.example.com/pay/return.php', 'name' => '会员开通', 'money' => '9.90', 'sign_type' => 'MD5', ]; $key = 'your_mch_key'; $params['sign'] = sign($params, $key); $ch = curl_init('https://pay.example.com/submit.php'); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $resp = curl_exec($ch); curl_close($ch); echo $resp;

签名的关键在于ksort。排序后把参与签名的参数用&拼接,末尾拼商户密钥,再做 MD5。需要注意三点:空值参数不参与签名;signsign_type本身不参与签名;http_build_query会做 URL 编码,但签名拼接使用原始值,所以参数值不要传包含特殊字符的内容,否则签名对不上。

4.3 支付回调验签与防重处理

支付完成后,平台向notify_url异步发送通知。回调里比下单参数多了trade_notrade_status两个字段。验签逻辑和下单一样:把收到的参数重新签名,与sign对比,通过后再判断trade_status

<?php function verifyNotify(array $data, string $key): array { if (empty($data['sign'])) { return ['code' => 0, 'msg' => 'no sign']; } $params = $data; $sign = $params['sign']; unset($params['sign'], $params['sign_type']); $calSign = sign($params, $key); if ($calSign !== $sign) { return ['code' => 0, 'msg' => 'sign mismatch']; } return ['code' => 1, 'msg' => 'ok']; } $data = $_POST; $result = verifyNotify($data, $key); if ($result['code'] !== 1) { exit('fail'); } $orderId = $data['out_trade_no'] ?? ''; $tradeStatus = $data['trade_status'] ?? ''; if ($tradeStatus === 'TRADE_SUCCESS') { // 防重:只有 status=0 的未处理订单才会被更新 $stmt = $pdo->prepare('UPDATE pay_orders SET status = 1 WHERE out_trade_no = ? AND status = 0'); $stmt->execute([$orderId]); } // 必须输出纯文本 success,否则平台会持续重推 exit('success');

回调处理有两个必须注意的细节。一是防重入:UPDATE ... WHERE status = 0保证同一订单只有第一次回调能成功更新,重复回调不会产生副作用。二是返回内容:易支付要求回调接口输出纯文本success,不能输出 JSON、HTML 或其他字符,否则平台认为回调失败并继续重试。

5. 直连防红系统的缓存与排错:三个必调参数

5.1 为什么要在防红接口前加 Redis 缓存

防红接口的高频路径是查询任务表,数据库按task_key做索引查找。当一个短链被分发到几百个群后,同一秒可能涌进上千个请求,数据库压力会直线上升。常见做法是用 Redis 缓存任务记录,以anti_red:task:{key}为键,存 JSON 序列化数据。

<?php $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $cacheKey = 'anti_red:task:' . $taskKey; $taskJson = $redis->get($cacheKey); if ($taskJson !== false) { $task = json_decode($taskJson, true); } else { $stmt = $pdo->prepare('SELECT * FROM anti_red_task WHERE task_key = ? AND status = 1 LIMIT 1'); $stmt->execute([$taskKey]); $task = $stmt->fetch(PDO::FETCH_ASSOC); if ($task) { $redis->setex($cacheKey, 300, json_encode($task, JSON_UNESCAPED_UNICODE)); } }

缓存超时 300 秒。太短起不到保护作用,太长会导致后台改了状态前台不生效。更稳妥的做法是后台更新任务时主动DEL anti_red:task:{key},让缓存立刻失效。另外注意:如果任务被停用,SELECT查不到新记录,旧缓存仍然会被持续读取,直到超时,所以停用操作的即时性要依赖主动删缓存,而不是缓存自然过期。

5.2 上线后必须调整的三个参数

防红系统上线后,我遇到最多需要调整的地方集中在这三个参数:

参数默认值调整场景
direct_ua关键词集合MicroMessenger,QQ平台更新 UA 规则时,旧关键词可能不再出现
Redis 缓存超时300 秒活动运营期间调短,平时调长
Referer 白名单wx.qq.com 等扫码进入时 Referer 为空,需要增加放行规则

direct_ua是防红效果的直接决定项。微信和 QQ 更新内核后,UA 字符串可能保留旧关键词也可能完全替换,所以要定期从真实用户日志里提取新出现的特征,更新到这个集合。不要只看网上别人分享的列表,自己服务器日志里的 UA 才是第一手数据。

5.3 两个日志点:拦截日志与回调日志

固定观察两个日志文件:block.log记录被拦截的普通浏览器访问,pay_callback.log记录支付回调原始数据和结果。

拦截日志常见问题是“该跳的没跳”,比如用户明明用微信打开却命中普通浏览器分支。这通常不是 UA 识别写错,而是反向代理改写了HTTP_USER_AGENT。排查时先看 Nginx 配置里有没有proxy_set_header User-Agent;如果用了框架,再看中间件是否统一清掉了 UA。

回调日志常见问题是“验签断言失败”。看到这类报错,直接打印$_POST原始数组,与支付平台文档对照。很多私有化部署的易支付网关会在回调字段前加自定义前缀,或者字段名多一层嵌套。在回调接口第一行记录原始日志,能让这类问题在几分钟内定位:

<?php file_put_contents( __DIR__ . '/logs/pay_callback.log', date('Y-m-d H:i:s') . ' ' . json_encode($_POST) . PHP_EOL, FILE_APPEND );

记录日志时不要记录银行卡号、支付凭证等敏感字段;日志文件权限建议设置为 640,并定期做切割。

6. 部署后用命令行模拟来源请求,做防红直连自检

6.1 用 curl 模拟微信、普通浏览器和扫码场景

防红接口写完不能只靠真机点来验证,真机实测效率低。我习惯把验证脚本化:在命令行伪造不同 UA 和 Referer,观察响应状态码与Location字段。

# 模拟微信内置浏览器访问 curl -I -X GET 'https://jump.example.com/jump.php?k=aB3xY' \ -H 'User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49' \ -H 'Referer: https://wx.qq.com/' # 模拟普通浏览器访问 curl -I -X GET 'https://jump.example.com/jump.php?k=aB3xY' \ -H 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/125.0 Safari/537.36' # 模拟不带 Referer 的扫码场景访问 curl -I -X GET 'https://jump.example.com/jump.php?k=aB3xY' \ -H 'User-Agent: Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 MicroMessenger/7.0.20'

-I只取响应头。判断点有两个:一看是否返回 302 且Location是预期的真实地址;二看普通浏览器分支是否返回 200 提示页或 403,而不是 302。如果普通浏览器也 302,说明来源判断有漏洞,优先回看checkSourceEnv的关键词顺序,微信和 QQ 特征词不要互相包含,否则会命中错误分支。

6.2 把签名自检写进部署脚本

支付侧的自检目标是验证签名算法是否和支付平台一致。下面这段命令可以直接跑:

php -r " require 'sign.php'; \$params = ['pid'=>'1000','type'=>'alipay','out_trade_no'=>'TEST001','notify_url'=>'https://j.example.com/n','return_url'=>'https://j.example.com/r','name'=>'测试','money'=>'1.00','sign_type'=>'MD5']; echo sign(\$params, 'test_key'), PHP_EOL; "

把这个输出值和支付平台上同样参数计算出的签名比对。不一致时先查ksort排序是否覆盖了所有参与签名的参数,再查 URL 编码方式是否在签名前后发生了变化。这里有个高频坑:直接拿http_build_query的结果参与签名字符串拼接,导致特殊字符被编码后两边算出的签名永远不一致。签名用的字符串必须来自原始参数数组,顺序固定、不做任何编码。

最后还有一个高频问题:接口返回“请在微信或QQ中打开”,但用户确实是从微信进来的。这种情况下直接在接口里输出var_dump($_SERVER['HTTP_USER_AGENT']),和手机上的真实微信 UA 逐字符对比,通常能在同一分钟内发现问题出在关键词缺失还是代理改写。修完关键词集合再跑一遍 6.1 的 curl 回归,三个分支状态码全部符合预期后,这个直连防红网关才算真正上线。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询