简介:网站运营中,域名被微信拦截会直接导致链接变红、业务流量骤降。微信的拦截机制是基于域名维度的风险检测,一旦命中,用户访问时根本不会加载页面 HTML。面对这种环境限制,一种轻量级的防红跳转方案成为刚需:通过两个静态 HTML 页面(引导页与内容页)配合双域名进行用户分流。其核心原理是利用 UA 判断用户是否在微信内置浏览器中,若在,则展示包含复制按钮和“浏览器打开”指引的引导页;若不在,则直接跳转至业务内容页。这种方案技术门槛低、部署灵活,非常适用于公众号引流、短信推广等场景。本文从域名拦截原理出发,详解这一最小防红闭环的代码实现、Nginx 部署以及域名切换的避坑实践。
1. 域名被微信拦成红色后,两个 HTML 才是防红跳转的最小闭环
在微信里打开自己发的链接,结果看到一整屏红色“已停止访问该网页”,这个场景对做公众号、短信引流和自有网站的人都算不上陌生。标题里的“防红跳转到浏览器”不是什么黑科技,它是把用户从微信内置浏览器里“请出去”的一整套静态页面方案:域名被拦之后,你还能靠一个能打开的引导页告诉用户下一步做什么,再靠一个内容页承接真正的业务。这套方案只依赖两个 HTML 文件,适合手里有自己域名(最好两三个)、业务内容合规、想尽快把损失降到最低的开发者。先把结论放在前面:微信内置浏览器没有开放任何接口允许网页强行拉起外部浏览器,所有号称“自动跳到浏览器”的做法最后都绕回到“手动打开”四个字上。
2. 防红跳转的边界和链路:为什么只能引导、不能强拉,以及两页两域名怎么配合
2.1 微信拦截域名时拦掉的到底是什么
很多刚接触防红的同学会把“拦截”理解成“页面加载后被检测然后弹窗”,于是到处找“绕过代码”。实际不是。微信客户端在 WebView 发起请求之前,会先把目标 URL 发给后端的外链安全检测服务,命中风险名单后直接渲染“已停止访问该网页”的错误页。这个错误页由微信客户端本地渲染,你的服务器日志里可能连一次 HTTP 请求都看不到,HTML 代码根本没机会执行。
所以“域名红不红”是域名维度的,不是页面维度的。同一个域名下的任意路径、任意参数,只要域名命中名单就都进不去。这也是为什么“在页面里写一段跳转代码”救不了已经红的域名——你连让用户看到这段代码的机会都没有。理解这一点后,防红跳转的正确方向就很清楚了:不在红域名上做文章,而是准备一个还没红的“引导域名”,让用户先到达引导页,再从引导页拿到真实内容地址。
域名防红、html 跳转这些词落到实现上,其实就是两件事:判断当前是不是微信内置浏览器,然后给用户一条能手动执行的路径。下面把这条路径的边界讲透。
2.2 为什么 location.href 无法把用户带出微信
不少读者第一次搜“跳转到浏览器”,期望的是类似window.location.href = "weixin://"或者intent://这类代码,点击后手机能直接弹出系统浏览器。实际在微信里这条路径走不通:iOS 微信用的是 WKWebView,Android 微信用的是基于 X5 内核的定制 WebView,两者都没有向网页开放“拉起外部浏览器”的 API;网页里申请打开intent://这类 scheme 时会被客户端直接拦截或忽略。
微信官方 JSSDK 里也没有“打开系统浏览器”的能力,能做的只有window.location.href跳到另一个 URL,而这个 URL 如果继续命中风险检测,结果还是红色拦截页。因此“跳转到浏览器”的唯一稳定实现,是引导用户点击微信右上角的“...”菜单,在弹出菜单里选择“在浏览器中打开”。这听起来很玄学,拆开看就是 UA 判断加一个操作按钮。
我的经验是,与其花时间找自动跳转的旁门左道,不如把引导步骤做清晰,让用户十秒内能完成手动跳转。这个方向上的投入产出比是最高的。
2.3 两页两域名的完整链路:从一个可访问的引导页开始
整套方案最少需要两个 HTML 文件、两个域名。第一个 HTML 是引导页,部署在一个仍然可访问的“安全域名”上;第二个 HTML 是内容页,部署在真正承载业务、可能被拦截的“业务域名”上。链路是:用户从短信、二维码、公众号菜单或聊天会话点开引导页链接 → 引导页里的 JS 读取 UA → 如果是微信内置浏览器,就展示“复制地址 + 右上角菜单在浏览器打开”的操作引导;如果不是微信环境,直接跳到内容页正常访问。
为什么不能只做一个 HTML?因为两个页面的任务完全不同。引导页面向“还没到内容页”的用户,它要解决的是“微信里点不开怎么办”;内容页面向“已经打开业务页面”的用户,它需要在自己还没被全量拦截时给用户留一条逃生通道,比如顶部提示条。把这两件事塞进同一个页面,要么用户始终看不到真实内容,要么引导入口完全失效。
域名配比上我一般建议准备两个独立域名:一个专门做引导,不放任何业务内容;另一个放业务内容。引导域名要保持干净,不参与跳转链、不挂广告、不被用户投诉,它的寿命远比业务域名长。如果条件允许再备第三个域名做轮换池,万一业务域名被红,引导页改一个配置就能切换。
这个链路里最容易做错的,是把引导页当成“文案页”,只放一段“请用浏览器打开”的文字,不给复制按钮。用户在微信里手打网址几乎必错,二维码在微信内长按又会默认用内置浏览器打开。所以复制按钮加浏览器打开指引是最低配置。
3. 直接可用的两个 HTML:引导页与内容页防红适配的完整代码
3.1 guide.html:中转引导页(部署在备用域名)
下面这个 HTML 是整套方案的核心,保存为 guide.html,放在引导域名根目录当首页。页面逻辑分三段:先判 UA,再给微信用户展示引导界面,非微信用户直接跳转内容页。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>访问引导</title> <style> body { margin: 0; font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f5f6f8; } .card { max-width: 420px; margin: 40px auto; background: #fff; border-radius: 16px; padding: 28px 24px; box-shadow: 0 4px 16px rgba(0,0,0,.06); } h1 { font-size: 20px; margin: 0 0 12px; } p { font-size: 14px; line-height: 1.7; color: #555; } .btn { display: block; width: 100%; padding: 14px 0; border: 0; border-radius: 8px; font-size: 16px; color: #fff; background: #07c160; margin-top: 16px; cursor: pointer; } .btn.secondary { background: #e8eaed; color: #333; } .tips { margin-top: 16px; font-size: 13px; color: #999; line-height: 1.6; } .tips b { color: #333; } </style> </head> <body> <div class="card"> <h1>访问提示</h1> <p>当前页面在微信内访问可能受限。为避免重复出现安全风险提示,请按下面的步骤用系统浏览器访问。</p> <button class="btn" id="copyBtn">复制访问地址</button> <button class="btn secondary" id="guideBtn">查看浏览器打开步骤</button> <div class="tips" id="tipsBox" style="display:none;"> iOS:点击右上角 <b>···</b>,选择 <b>在Safari中打开</b>。<br> 安卓:点击右上角 <b>···</b>,选择 <b>在浏览器中打开</b>。<br> 也可以把复制好的地址粘贴到任意浏览器再访问。 </div> <div class="tips" id="copiedTip" style="display:none;">已复制。请打开系统浏览器,粘贴后访问。</div> </div> <script> // 真实业务域名,替换成你的内容页地址 var FINAL_URL = 'https://official.example.com/'; // 微信 UA 识别;兼容 iOS / Android 微信 var isWeChat = /MicroMessenger/i.test(navigator.userAgent); // 非微信环境直接去内容页,带 from 参数方便内容页统计来源 if (!isWeChat) { location.replace(FINAL_URL + '?from=guide'); } var copyBtn = document.getElementById('copyBtn'); var guideBtn = document.getElementById('guideBtn'); var tipsBox = document.getElementById('tipsBox'); var copiedTip = document.getElementById('copiedTip'); function copyLink() { var input = document.createElement('textarea'); input.setAttribute('readonly', 'readonly'); input.value = FINAL_URL + '?from=guide'; input.style.position = 'fixed'; input.style.left = '-9999px'; document.body.appendChild(input); input.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (e) {} document.body.removeChild(input); if (ok) { copiedTip.style.display = 'block'; tipsBox.style.display = 'none'; } else { alert('复制失败,请长按地址手动复制:' + FINAL_URL); } } copyBtn.addEventListener('click', copyLink); guideBtn.addEventListener('click', function () { tipsBox.style.display = 'block'; copiedTip.style.display = 'none'; }); </script> </body> </html>代码逻辑说明:UA 判断放在脚本最前面,避免在微信环境里出现一帧内容页闪屏。非微信环境用location.replace而不是location.href,这样跳走后浏览器历史记录里不会留下一个空白引导页,用户按返回键不会又回到引导页。
复制函数用textarea加execCommand('copy'),这是兼容性最稳的方案。navigator.clipboard在微信内置浏览器里经常因为权限策略失败,而execCommand只要由用户点击事件同步触发,iOS 和安卓微信都能用。注意input.select()之后要立刻执行复制,中间不要包setTimeout,否则手势上下文会丢失。
注意:引导页不要加自动跳转逻辑。目标域名一旦被拦,自动跳转只会把用户送进红页。
参数说明里唯一必改的是FINAL_URL,改成你的内容页完整地址,带https://和结尾斜杠。?from=guide是渠道追踪参数,内容页统计入口用的;如果你的业务系统不认未知参数,可以去掉,或者换成你自己定义的渠道标识。微信 UA 正则目前覆盖所有主流微信版本,如果要把企业微信也当微信环境处理,把正则改成/MicroMessenger|wxwork/i。
3.2 landing.html:内容页的微信访问提示浮层
第二个文件要解决的问题是:内容页自身在微信里还能打开时,给用户一个明显的提示条,告诉他“这里在微信里不稳定,请用浏览器打开”。如果域名已经被完全拦截,这个页面本身也加载不出来,所以它是保险丝,不是救火队。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>示例内容页</title> <style> /* 业务页面自带样式省略,只给出防红提示条样式 */ body { margin: 0; } .wx-tip { position: fixed; top: 0; left: 0; right: 0; z-index: 99999; background: #ffece8; color: #333; font-size: 14px; padding: 12px 16px; display: none; line-height: 1.6; box-shadow: 0 2px 8px rgba(0,0,0,.1); } .wx-tip .tip-btn { margin-left: 8px; color: #07c160; font-weight: bold; cursor: pointer; white-space: nowrap; } .wx-tip .close { float: right; color: #999; cursor: pointer; margin-left: 12px; } .page { padding: 20px; } </style> </head> <body> <!-- 防红提示条,默认隐藏,仅微信环境显示 --> <div class="wx-tip" id="wxTip"> 微信内访问可能受限,请点击右上角 <b>···</b> 选择浏览器打开。 <span class="tip-btn" id="copyUrlBtn">复制地址</span> <span class="close" id="closeTip">✕</span> </div> <div class="page"> <h1>业务内容标题</h1> <p>这里放你的正常业务内容。微信环境访问时顶部会有提示条,系统浏览器访问时完全隐藏。</p> </div> <script> // 微信 UA 检测;命中才显示提示条 var isWeChat = /MicroMessenger/i.test(navigator.userAgent); var tip = document.getElementById('wxTip'); if (isWeChat) { tip.style.display = 'block'; } document.getElementById('closeTip').addEventListener('click', function () { tip.style.display = 'none'; }); document.getElementById('copyUrlBtn').addEventListener('click', function () { var input = document.createElement('textarea'); input.setAttribute('readonly', 'readonly'); input.value = location.href; input.style.position = 'fixed'; input.style.left = '-9999px'; document.body.appendChild(input); input.select(); try { document.execCommand('copy'); alert('地址已复制'); } catch (e) {} document.body.removeChild(input); }); </script> </body> </html>逻辑说明:这段 JS 是独立的,不依赖任何框架,直接放在业务页面<head>或<body>开头都能跑。提示条用position: fixed固定在顶部,z-index: 99999保证不被业务页面的弹层盖住。复制按钮复制的是location.href当前完整地址,这样用户在微信里复制后,到浏览器粘贴时能保留当前的来源参数。
如果业务页面是 Vue 或 React 这类 SPA,不要把这段逻辑放在某个组件的mounted里,建议放在入口 HTML 的<head>中,保证提示条在首屏渲染前就能出现。关闭按钮给用户一个出口,避免提示条长期遮挡业务内容。
3.3 两页联调时的关键参数与替换清单
下面是部署前要过一遍的替换清单,我每次换新域名时都按这个表检查:
| 配置项 | 位置 | 说明 |
|---|---|---|
| FINAL_URL | guide.html | 内容页完整地址,必须 https,结尾带斜杠 |
| 引导页域名 | 部署配置 | 保持简单,不放业务内容 |
| 业务域名 | Nginx / landing.html | 真正承载内容的域名 |
| ?from=guide | guide.html 跳转参数 | 内容页统计入口来源 |
| MicroMessenger | 两处 JS | 微信 UA 匹配规则 |
| 二维码图片 | guide.html 可选 | 如果放二维码,指向 FINAL_URL |
联调时最容易犯的替换错误,是把FINAL_URL写成引导页域名,结果用户复制到的是引导页地址,在浏览器打开后又被引导页的 JS 判断为非微信环境,再跳回内容页,相当于绕了一圈。还有一种情况是把内容页和引导页放在同一个域名的不同目录下,这也不行,微信拦截是按域名维度判定的,子目录一样进不去。
4. 部署与自测:把两个 HTML 跑成线上服务并验证微信侧行为
4.1 最省事的静态托管:两个域名 + 一个 Nginx
部署上我一般把两个文件都当纯静态页面处理,不需要后端服务。同一个服务器上跑两个 server 块就行,也可以用云厂商的对象存储加 CDN,但自建 Nginx 调试最直接。关键配置有三点:强制 HTTPS、引导页禁用缓存、两个域名分开配置。
server { listen 80; server_name guide.example.com official.example.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name guide.example.com; root /var/www/guide; index guide.html; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; add_header Cache-Control "no-store"; } server { listen 443 ssl http2; server_name official.example.com; root /var/www/official; index landing.html; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; }配置说明:第一个 server 块负责把 80 端口全部转到 HTTPS,微信对明文 HTTP 链接的容忍度更低,不要省这一步。引导页加Cache-Control: no-store,避免用户手机上缓存了旧引导页导致改了配置不生效;内容页不需要加,正常缓存有助于打开速度。
证书部分要注意,如果两个域名共用同一张证书,证书必须同时覆盖两个域名;如果分属不同后缀,就各自申请证书。Nginx worker 用户需要对两个 root 目录有读权限,否则线上会 403。改完配置先nginx -t再systemctl reload nginx。
提示:两个 server 块如果不共用证书,需要把
ssl_certificate和ssl_certificate_key分别指到各自证书文件。
4.2 线上验证的四种方式:从 curl 到真机微信
自测建议按顺序做,别一上来就用已经在微信里红了的域名测试,那样没有任何参考价值。
先模拟微信 UA 请求引导页,确认返回的是引导页 HTML:
curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.40" -s https://guide.example.com/ | head -n 20输出里能看到“访问提示”和“复制访问地址”按钮就算通过。再模拟普通浏览器 UA 请求一次:
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36" -s https://guide.example.com/ | head -n 20这里有个细节:跳转逻辑写在 JS 里,curl 不执行 JS,所以普通 UA 请求返回的仍然是引导页 HTML,这不能算失败。curl 这一步只验证页面可达、没有被微信在请求层拦截。要验证真正的跳转行为,用 Chrome 开发者工具:DevTools 右上角三个点菜单里打开 More tools 里的 Network conditions,在 User agent 里自定义追加MicroMessenger/8.0.40,刷新页面看是否显示引导 UI;去掉 UA 后缀再刷新,地址栏会自动切到内容页。
最后一步是手机真机验证。把引导页链接发到文件传输助手或一个测试群,用手机微信点开,观察有没有弹红色拦截页。测试域名尽量用新申请的、没有历史污点的域名,已经被标记过的域名测再多轮也进不去。
4.3 域名进入红色状态后的切换策略
一旦确认业务域名被微信拦截,最佳动作不是守在原地申诉,而是立即切换。常见做法是改引导页的FINAL_URL,换成另一个可用域名,然后重新发布静态文件。如果服务器托管,发布动作一般几十秒内完成。
为避免每次都要改 HTML、还要清缓存,我一般会把FINAL_URL抽到一个独立配置层。简单一点的做法是单独建一个 config.js:
var FINAL_URL = 'https://official.example.com/';引导页里的FINAL_URL改成从 config.js 读取,后续切换域名时只改这个文件,不用碰业务页面。更进一步可以把配置放到远程 JSON,让引导页启动时动态拉取当前生效域名,这个做法在第 6 章展开。
还要提醒一点:微信官方的域名状态查询一直没有公开稳定的接口,网上流传的“域名防红检测 API”大多是半黑盒,返回结果只代表某个节点、某个 UA 下的检测状态,不能当权威结论。我建议把“用户用手机微信打开是否看到红页”当作最终验收标准,人工反馈比你想象中可靠。
5. 防红跳转避坑记录:5 个高发问题的现象、原因与处理
5.1 iOS 微信里“复制链接”点了没反应
现象:安卓微信里复制正常,iOS 的微信里点“复制访问地址”没有任何反应,也没复制成功提示。
原因:document.execCommand('copy')必须在用户点击事件同步执行,iOS 对剪贴板权限校验更严格。很多人为了让复制按钮有“点击反馈”,先弹一个自定义 toast 或按钮 loading,中间隔了 100ms 异步任务,导致系统把这个操作判定为非用户手势,直接拒绝。
解决:
copyBtn.addEventListener('click', function () { var input = document.createElement('textarea'); input.value = FINAL_URL; document.body.appendChild(input); input.select(); var ok = document.execCommand('copy'); // 必须同步 document.body.removeChild(input); if (ok) { copiedTip.style.display = 'block'; } });复制执行完后不要用setTimeout包execCommand,也尽量避免在触发复制前做异步接口请求。如果业务上一定要先请求,就先请求完,等用户再点一次按钮时再复制。
5.2 自动跳转活不过三秒,页面反复弹“已停止访问”
现象:引导页里加了 3 秒后自动location.href到内容页的逻辑,结果用户在微信里点开引导页,3 秒后直接弹红色拦截页,连手动复制的时间都没有。
原因:自动跳转只是把用户从引导域名带到一个新 URL,微信对每个 URL 都会做安全检测,目标域名如果已在风险名单里,跳过去的结果就是红页。防红不只是“引导页不红”,而是“目标页在微信内根本不该直接跳”。
解决:把自动跳转删掉。引导页只提供复制按钮和手动操作步骤。我的习惯是引导页永远不自动跳,让用户手动复制一次。多一步操作看起来损失转化,实际上避免了“跳到红页再回来”的二次流失,留存反而更好。
5.3 连引导页都进不来,直接看到红色拦截页
现象:把引导页链接发到自己微信里测试,还没看到任何提示就直接被拦。
原因:引导页域名也进了风险名单。常见情况是:引导页和内容页共用了同一个域名不同子目录,或者引导域名之前挂过和业务页相似的内容被连带举报,或者域名本身之前被用于发送垃圾短链。
解决:引导页必须和内容页物理隔离,用独立域名、独立服务器目录、独立证书,页面里不放真实业务内容,也不要在引导页里写任何诱导分享文案。如果这个域名已经红透,直接换新域名;短时间内不要在同一台服务器、同一条 DNS 解析下继续挂相同内容,否则容易被再次标记。
5.4 备用域名在系统浏览器里也打不开,提示证书错误
现象:用户按引导复制了内容页地址,粘贴到 Safari 或 Chrome 后提示“您的连接不是私密连接”,页面无法打开。
原因:备用域名证书过期、证书不包含该域名、或者 Nginx 只监听了 443 没配置对应server_name。很多开发者只给主业务域名申请证书,备用域名没续期,内容页切过去时用户直接卡在证书警告页。
解决:在切换域名前先做一次完整的 HTTPS 自检:
curl -I https://official.example.com/ openssl x509 -in /etc/nginx/ssl/fullchain.pem -noout -dates第一条看返回码和响应头,第二条看证书有效期。同时确认证书的 Subject Alternative Name 里包含备用域名。Nginx 修改证书后要nginx -t && systemctl reload nginx,别漏了这一步。
5.5 公众号菜单链接被标记,防红代码绕不过去
现象:公众号自定义菜单里填的 URL 在后台提示“可能存在安全风险”,用户点击菜单直接弹风险提示,而不是进入页面。
原因:公众号菜单的外链校验发生在微信后台配置阶段,它针对的是菜单 URL 本身。如果菜单 URL 直接填业务域名或已被标记的引导域名,后台就会给出安全警告。
解决:菜单 URL 填一个干净、未标记、且不承载实际内容的引导域名;业务内容只放在引导页上的“复制地址”里,让用户复制后到浏览器打开。这样菜单本身的检测对象是安全的引导页,业务域名不直接暴露在菜单配置中。如果引导域名也被标记过,先换新域名再配置菜单,不要试图用脚本绕过微信后台的检测,那个流程不是前端能干预的。
6. 把两个 HTML 升级成域名自动切换:一个 JSON 配置和一次诚实的上报
前面讲的手动改FINAL_URL能用,但域名多了以后每次都要发布一次静态文件,比较烦。常见的进阶做法是把域名列表放到远程 JSON 里,引导页启动时先拉配置,再决定往哪跳。这样域名被红时只需要更新 JSON,引导页不用重新发布、不用清缓存。
// 引导页启动时拉取可用域名配置 fetch('https://guide.example.com/boot.json') .then(function (res) { return res.json(); }) .then(function (cfg) { if (cfg.domains && cfg.domains.length) { FINAL_URL = cfg.domains[0].url; // 当前生效域名 localStorage.setItem('finalUrl', FINAL_URL); } else { FINAL_URL = localStorage.getItem('finalUrl') || 'https://official.example.com/'; } // 微信内展示引导,非微信环境跳转 if (!/MicroMessenger/i.test(navigator.userAgent)) { location.replace(FINAL_URL + '?from=guide'); } }) .catch(function () { // 拉取失败时回退到本地配置,保证引导页可用 if (!/MicroMessenger/i.test(navigator.userAgent)) { location.replace(FINAL_URL + '?from=guide'); } });这个做法的边界要说清楚:boot.json只能告诉页面“哪个域名可用”,但前端 JS 拿不到微信安全检测的即时结果。也就是说,域名被红后系统不会自动知道,需要靠访问日志或用户反馈来更新 JSON。我的习惯是给每个渠道的下发链接都带渠道参数,发现某个渠道的短链在微信里打不开了,就去更新 JSON 里的域名顺序,让新访问者优先落到备用域名。如果引导页和 JSON 不同域,记得给 JSON 接口开 CORS。
如果你是长期稳定内容,自持两三个域名完全够用,没必要依赖第三方;如果可用率要求极高,再考虑接第三方平台。但无论用谁的方案,引导页都要保持自己能随时替换,不要把域名池的命脉押在某个黑盒接口上。
这个方向做久了之后,我最大的感受是“防红”不是让域名永远不会红,而是让业务在单个域名红掉之后还能继续跑。把引导页做得足够简单、把域名切换做得足够快,用户根本感知不到你换了域名。希望帮到你。
本文还有配套的精品资源,点击获取