← 路由设计 · 稳定入口与可变目标 / 分享地址统一入口 · 接住各路分享 → 统计 → 跳目标 待审核 5 / 23

分享地址统一入口 · 接住各路分享 → 统计 → 跳目标

真实场景:同一个活动要分发到微信、短信、海报二维码、站内、广告投放,每个渠道一条分享链接。与其让每条链接各自硬编码最终落地页(改个目标要重发一切、还统计不到来源),不如让所有分享链接都先打到一个统一路由入口 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:别让目标来自不可信输入

上面的目标来自服务端短码表(你自己控制),很安全。但有一种更便捷的写法是把目标直接放进 queryvega.link/r?u=https://...,然后服务端不加校验地 302u。这就开了一个 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 必须指向一个前端认识的路径;最后一条 * 把其余一切 rewriteindex.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 · 相关页