系统设计 / 路由设计 · 稳定入口与可变目标 / 分享地址的统一入口 待审核 5 / 23

分享地址的统一入口

真实场景:同一个活动要分发到微信、短信、海报二维码、站内、广告投放,每个渠道一条分享链接。与其让每条链接各自硬编码最终落地页(改个目标要重发一切、还统计不到来源),不如让所有分享链接都先打到一个统一路由入口 vega.link/s/:code。命中后服务端做三件事:记一笔统计、查出当前目标、302 跳过去。

图 0-1 · 一次分享点击经过统一入口的三步:记统计、查目标、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 与 allowlist

上一节的目标来自服务端短码表,由站方自己控制,很安全。但有一种更便捷的写法是把目标直接放进 query,如 vega.link/r?u=https://...,服务端不加校验地 302 到 ?u= 里那个地址。这就开了一个 open redirect 漏洞:攻击者构造 vega.link/r?u=//evil.com/phish 发给受害者,链接看起来是可信域名,点开却被带到钓鱼站。

图 2-1 · 几种绕过字符串前缀校验的构造,以及 allowlist 如何拦下它们。可切换校验方式对比结果。

警示 · 凡是跳转目标能被用户或外部输入影响的地方——query 里的 ?u=?returnUrl=?next=,以及登录后回跳——都必须校验。最稳的是只允许相对路径、根本不接受绝对 URL,次之是 host allowlist。注意 //evil.com 这种协议相对写法与 https:evil.com 都能绕过「只检查开头是不是 http」的字符串前缀校验,一定要用 new URL() 解析出真正的 host 再比对。

3 · SPA 下的三条服务端路由

上面统一入口的三件事都发生在服务端。但更常见的部署是纯前后端分离:前端是一份 index.html 加静态 JS,业务页面的路由表握在前端 router 手里,服务端只剩 API。这种架构下分享链路推荐 history 模式(干净 path,如 /promo/spring)而非 hash,理由都和分享直接相关。

服务端看得见完整 path。渠道统计、访问日志与 edge 拦截都要在服务端记录,而 hash 的 fragment 根本不会随请求发出,服务端永远只看到 /

分享卡片能按页定制。社交平台的爬虫不执行 JS,只有按 path 才能给每页吐不同的 OG meta;hash 模式下所有页面在爬虫眼里是同一个 URL。

URL 干净、渠道兼容。能直接印海报、塞短信;而部分渠道回跳时会在 # 前插 query,hash 路由容易被搅乱。

代价只有一条:服务端要配 history fallback。整张服务端路由表收敛成三条,顺序即优先级,与匹配引擎同一套规则。/api/* 反代给后端;/s/:code 这类分享入口只让服务端认识,前端路由表里没有它,命中后照样统计、查表、302,只是 Location 必须指向一个前端认识的路径;最后一条 * 把其余一切 rewrite 到 index.html,前端 router 再从 location.pathname 接手渲染。注意 rewrite 不是 redirect:它回 200 与 index.html 的内容,地址栏不变。

图 3-1 · 三条服务端路由对不同请求的分派:API 反代、分享入口、以及 fallback rewrite。可改请求路径观察命中哪一条。

落到具体配置,一份 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
hash 模式唯一的优势是零服务端配置;只要涉及分享,其余三项都倒向 history。
维度 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 里,否则链接发出去,对方打开只是一个初始空页。

入口命中后那一下跳转用的就是 3xx,分享场景目标常变、通常用 302。同一条分享链接要在手机上直接跳进 App、没装再退回网页,是 deep link 的题目。