微信强制外跳浏览器方案(Nginx 实现)
一、目标与效果说明
目标
在微信里点击链接时:
- Android 微信: 自动调起系统默认浏览器,并在外部浏览器打开真实业务页面
- iOS 微信: 弹出遮罩,用户手动点击在浏览器打开
实现方式
利用微信内置浏览器对 "附件下载(Content-Disposition: attachment)" 的处理逻辑: 当返回头声明为附件下载时,Android 微信会交给系统浏览器或外部下载器处理,该请求在外部浏览器重新发起后,Nginx 返回一段包含 JavaScript 的 HTML,在客户端进行 URL 解码并跳转。
二、整体流程概览
-
用户在微信中点击形如:
https://www.jump.com/open?url=https%3A%2F%2Fwww.baidu.com说明:
?url=后面的参数就是最终要跳转到的完整业务链接(需经过 URL 编码)。 -
请求打到
www.jump.com上的 Nginx:- User-Agent 中包含
MicroMessenger,识别为微信内置浏览器。 - Nginx 返回:
Content-Type: application/pdfContent-Disposition: attachment; filename="open.pdf"- 一个伪装的 PDF 内容。
- User-Agent 中包含
-
Android 微信把这个请求交给系统浏览器/外部浏览器处理,外部浏览器再次访问同一个 URL。
-
第二次请求中不再带
MicroMessengerUser-Agent,Nginx 判断为"外部浏览器"。- 由于 URL 参数是编码过的(如
https%3A%2F%2F...),Nginx 若直接 302 跳转会导致路径错误。 - Nginx 返回一段 HTML,内含 JavaScript 脚本。
- 由于 URL 参数是编码过的(如
-
外部浏览器执行这段 JS 脚本:
- 使用
decodeURIComponent()还原真实 URL。 - 执行
window.location.replace()完成跳转,打开真实业务页面。
- 使用
三、前置条件
-
跳转域名未被微信拦截: 指
www.jump.com这个跳转专用域名能在微信内正常打开(?url=后面的目标业务域名无此要求)。 -
已有一台可以部署 Nginx 的服务器,并且:
- 可绑定跳转专用域名(如
www.jump.com)。 - 能为其配置 HTTPS 证书(Let's Encrypt / 云厂商证书均可)。
- 可绑定跳转专用域名(如
-
Nginx 版本支持
map、if、return等基础指令(常见发行版默认满足)。
四、步骤 1: 准备跳转子域名
为强制外跳单独准备一个域名,例如:
https://www.jump.com
在域名 DNS 控制台中新增一条解析:
- 记录类型: A(或 CNAME 至负载均衡)
- 主机记录:
www - 记录值: Nginx 服务器 IP
解析生效后,访问 https://www.jump.com 应能到这台 Nginx。
五、完整OpenResty配置(openresty/1.27.1.2)
# user nginx;
worker_processes auto;
worker_rlimit_nofile 65535;
worker_shutdown_timeout 10s;
error_log logs/error.log warn;
pid logs/nginx.pid;
events {
worker_connections 65535;
multi_accept on;
use epoll;
}
http {
include mime.types;
default_type application/octet-stream;
# -------- Gzip --------
gzip on;
gzip_min_length 1k;
gzip_comp_level 6;
gzip_vary on;
gzip_disable "MSIE [1-6]\\.";
gzip_types
text/plain text/css
application/javascript application/json
application/xml text/javascript;
sendfile on;
keepalive_timeout 65;
server_tokens off;
client_max_body_size 30m;
client_body_timeout 300s;
# -------- WebSocket/Keep-Alive --------
map $http_upgrade $connection_upgrade {
default upgrade;
'' "";
}
# -------- HTTP → HTTPS 强制跳转 --------
server {
listen 80;
listen [::]:80;
server_name www.jump.com;
return 301 https://$host$request_uri;
}
# ============================================================
# 微信跳转服务 (jump) - 使用 Lua 逻辑
# ============================================================
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.jump.com;
# 开启 http2
http2 on;
# 证书配置
ssl_certificate /home/nginxcert/_.jump.com.crt;
ssl_certificate_key /home/nginxcert/_.jump.com.key;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# --------------------------------------------------------
# 入口逻辑:使用 Lua 处理所有逻辑 (输出更可靠)
# --------------------------------------------------------
location = /open {
content_by_lua_block {
-- 1. 获取参数和 UA
local ua = string.lower(ngx.var.http_user_agent or "")
-- 这里的 arg_url 会自动被 Nginx 解码 (例如 %3A 变成 :)
-- 为了保证 JS 能正确处理,我们重新获取原始 Query String 可能更安全,
-- 但通常 arg_url 也是可以的。为了保险,我们下面直接拼入 JS 字符串。
local target_url = ngx.var.arg_url
-- 2. 检查参数
if not target_url or target_url == "" then
ngx.status = 400
ngx.say("Missing url parameter")
return
end
-- 3. 判断环境
local is_wechat = string.find(ua, "micromessenger")
local is_android = string.find(ua, "android")
if is_wechat then
if is_android then
-- A. [Android 微信] -> 走伪装 PDF 强制外跳
-- 注意:这里需要 exec 到另一个 location 是为了利用 Nginx 的 default_type 机制
-- 此时 target_url 不重要了,因为跳出去后浏览器会重发请求
ngx.exec("/_android_handler")
else
-- B. [iOS 微信] -> 直接输出遮罩 HTML
ngx.header.content_type = "text/html;charset=utf-8"
ngx.say('<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>请在浏览器打开</title><style>body{margin:0;padding:0;}.mask{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:9999;text-align:center;color:#fff;padding-top:20px;}.guide-img{width:80%;max-width:400px;margin:20px auto;border:1px dashed #666;padding:10px;border-radius:8px;background:rgba(255,255,255,0.1);}.text{font-size:18px;line-height:1.6;margin-top:20px;}</style></head><body><div class="mask"><div class="text"><p>由于微信限制,请按照以下步骤操作:</p><p>1. 点击右上角的 <span style="font-size:24px;font-weight:bold;">...</span></p><p>2. 选择 <span style="color:#4caf50;font-weight:bold;">在浏览器打开</span></p></div><div class="guide-img">Safari / 浏览器</div></div></body></html>')
return
end
else
-- C. [外部浏览器] -> 直接输出 JS 跳转 HTML
-- 此时已经在浏览器里了,直接用 target_url 生成页面
ngx.header.content_type = "text/html;charset=utf-8"
-- 构建 HTML 字符串,把 target_url 嵌入进去
-- 注意:encodeURIComponent 对应的解码是 decodeURIComponent
-- 如果 ngx.var.arg_url 已经被 nginx 解码过了(例如变成 https://...), JS 再 decode 一次也没事
-- 关键是防止 Lua 里的 nil 导致拼接报错(前面已检查)
local html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>正在跳转...</title></head><body>' ..
'<script>' ..
'var target = "' .. target_url .. '";' ..
'if(target.indexOf("%") > -1) { target = decodeURIComponent(target); }' ..
'window.location.replace(target);' ..
'</script></body></html>'
ngx.say(html)
return
end
}
}
# --------------------------------------------------------
# 内部处理块 (只保留 Android PDF 逻辑)
# --------------------------------------------------------
# [Android 处理] 伪装 PDF 强制外跳
location = /_android_handler {
internal;
default_type application/pdf;
add_header Content-Disposition 'attachment; filename="open.pdf"';
add_header Cache-Control 'no-store, no-cache, must-revalidate, max-age=0';
add_header Pragma 'no-cache';
return 200 "%PDF-1.1\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R/Resources<<>>>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000058 00000 n\n0000000115 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n190\n%%EOF";
}
# 默认首页
location / {
add_header Content-Type "text/html;charset=utf-8";
return 200 "Wechat Jump Service is Running.";
}
}
}