分享地址统一入口 · 接住各路分享 → 统计 → 跳目标
真实场景:同一个活动要分发到微信、短信、海报二维码、站内、广告投放,每个渠道一条分享链接。与其让每条链接各自硬编码最终落地页(改个目标要重发一切、还统计不到来源),不如让所有分享链接都先打到一个统一路由入口 vega.link/s/:code。命中后服务端做三件事:记一笔统计 → 查出当前目标 → 302 跳过去。
统一入口换来什么。 改 spring 的目标 → 所有印着 vega.link/s/spring 的海报 / 短信立刻全部跟随,一张都不用重发。同时,因为每次点击都带着 ?from= /
utm_* 经过这里,你能精确算出每个渠道带来多少访问——这是各渠道直接指向落地页时永远拿不到的来源数据。
1 · 这层入口还能附带做的事
- 灰度 / AB:命中后按比例把流量分到
/promo/spring-a/-b,在入口处就分流。 - 多端适配:判 User-Agent——手机跳 App deep link、桌面跳网页(见 deep link 页)。
- 风控 / 反爬:异常来源(无 referer、高频)在入口处就拦掉,不放到落地页。
- 过期 / 下线:短码可设有效期,过期统一跳「活动已结束」,而不是留死链。
2 · 安全 · open redirect:别让目标来自不可信输入
上面的目标来自服务端短码表(你自己控制),很安全。但有一种更便捷的写法是把目标直接放进 query:vega.link/r?u=https://...,然后服务端不加校验地 302 到 u。这就开了一个 open redirect 漏洞:攻击者构造
vega.link/r?u=//evil.com/phish 发给受害者——链接看起来是你的可信域名,点开却被带到钓鱼站。防御只有一条原则:跳转目标必须过 allowlist 校验。
记住:凡是跳转目标能被用户 / 外部输入影响的地方——query 里的 u / returnUrl / next / 登录后回跳——都必须校验。最稳的是只允许相对路径(根本不接受绝对 URL),次之是 host allowlist。注意
//evil.com 这种协议相对写法和 https:evil.com 都能绕过「只检查开头是不是 http」的字符串前缀校验——一定要用 new URL() 解析出真正的 host 再比对。
3 · 前后端分离 (SPA):history 模式 + 服务端只认三条路由
上面统一入口的三件事都发生在服务端。但更常见的部署是纯前后端分离:前端是一份 index.html + 静态 JS(SPA),业务页面的路由表握在前端 router 手里,服务端只剩 API。这种架构下,分享链路推荐 history 模式(干净 path,如
/promo/spring),而不是 hash(/#/promo/spring)——理由全都和「分享」直接相关:
- 服务端看得见完整 path。 渠道统计、访问日志、edge 拦截都要在服务端记录,而 hash 的 fragment 根本不会随请求发出——服务端永远只看到
/,这项统计无从记录。 - 分享卡片能按页定制。 微信 / Twitter 的爬虫不执行 JS,只有按 path 才能给每页吐不同的 OG meta(标题 / 缩略图);hash 模式所有页面在爬虫眼里是同一个 URL。
- URL 干净、渠道兼容。 能直接印海报、塞短信;而微信等渠道回跳时会在
#前插 query,hash 路由容易被搅乱。
代价只有一条:服务端要配 history fallback。整张服务端路由表收敛成三条,顺序即优先级(和匹配引擎页同一套规则):/api/* 反代给后端;/s/:code
这类分享入口只让服务端认识——前端路由表里没有它,统计与短码表本来就是服务端的职责,命中后照样「统计 → 查表 → 302」,只是 Location 必须指向一个前端认识的路径;最后一条 * 把其余一切 rewrite 到 index.html(注意 rewrite ≠
redirect:回 200 + index.html 的内容,地址栏不变),前端 router 再从 location.pathname 接手渲染。
落到具体配置,一份 nginx 静态托管 SPA 的整张服务端路由表如下(顺序即优先级):
location /api/ { proxy_pass http://api:3000; } # API 反代, 排最前
location /s/ { proxy_pass http://api:3000; } # 分享入口: 只有服务端认识, 统计 → 查表 → 302
location / { try_files $uri /index.html; } # fallback: 文件存在给文件, 否则 rewrite 到 index.html
| 维度 | history(/promo/spring) |
hash(/#/promo/spring) |
|---|---|---|
| 服务端可见性 | ✓ 完整 path,统计 / 日志 / 拦截都有记录 | ✗ fragment 不出浏览器 |
| OG 分享卡片 | ✓ 可按 path 逐页定制 | ✗ 爬虫眼里全站一个 URL |
| 渠道兼容 | ✓ 正常 | △ 回跳常被在 # 前插参搅乱 |
| 服务端配置 | 需要一条 fallback | 零配置(它唯一的优势) |
两段接力。 分享短链的一次点击 = 服务端先接(/s/:code:统计、查短码表、302)→ 前端再接(fallback 给到 index.html,router 按 pathname 渲染)。短码表、统计落库、OG
预览这三样天然留在服务端;前端要保证的只有一件事:URL 即状态——被分享的页面仅凭 URL 就能还原(参数进 path / query,不要只存在内存 store 里),否则链接发出去,对方打开只是一个初始空页。
4 · 相关页
- 本系列 · HTTP 重定向——入口命中后那一下「跳过去」用的就是 3xx;分享场景目标常变,通常用
302(临时、不缓存)。 - 本系列 · App 看不见的 route——统一入口的进阶:同一条分享链接,手机上要能直接跳进 App、没装 App 再退回网页。
- OWASP · Unvalidated Redirects——open redirect 的危害与 allowlist 防御清单。
- Vue Router · HTML5 History Mode——对应「SPA」小节:history 模式为什么需要服务端 fallback,附 nginx / Apache / Netlify 等各家配置。