Vercel域名加密策略:彻底禁用默认域名

背景

自从2020年Cloudflare Pages上线[1]以来,其已经成为大多数用例下比Vercel更理想的静态网站托管平台。相较于Vercel,Cloudflare Pages主要优势有:

  1. 无限流量
    Cloudflare Pages对于免费计划托管的静态网站未做流量限制。
    Vercel免费计划仅允许300GiB,现降为100GiB。
  2. 允许商用
    Cloudflare Pages允许免费计划托管商业网站。
    Vercel禁止免费计划托管商业网站。
  3. 更好的网络分发(相对)
    Cloudflare Pages国内可直连访问。
    Vercel DNS曾被和谐[2]*.vercel.app已被和谐,相对于Cloudflare更不稳定。

但Cloudflare Pages免费计划仅允许每月500次的部署数量上限[3],这对于在线写作、频繁预览的场景并不太够,特别是使用基于Git工作流、采用零本地环境、实现全在线编辑的Decap-CMS作为在线编辑器,更易产生频繁的git commit,频繁触发预览构建,500次确实不太够。

相对的,Vercel提供每日100次的部署次数上限,以及每月100小时的构建时间上限[4],专用于预览部署更加合适。

但Vercel和Cloudflare Pages均存在一个问题:无法禁用默认域名。Vercel会在部署时自动创建以下域名[5]

1
2
3
<project-name>-<unique-hash>-<username>.vercel.app
<project-name>-git-<branch-name>-<username>.vercel.app
<project-name>-<username>.vercel.app

且不提供任何禁用默认域名的开关。因此,即使为Vercel绑定了自定义域名,仍有可能被通过*.vercel.app访问生产部署或预览部署,甚至会被搜索引擎索引[6],以用户不期望的方式泄露并影响SEO。

本文给出彻底禁用Vercel默认域名的访问策略,且提供更加灵活的高级访问策略配置,包括:

  1. 屏蔽指定域名
  2. 屏蔽指定路径
  3. 屏蔽过期部署
  4. 双向鉴权
  5. (可自行实现)地理位置屏蔽、IP屏蔽……

访问控制策略可分为禁止访问(返回404等错误码)和跳转(302跳转至指定域名),实现原理类似。
本文使用Vercel以提供预览URL为主,因此下文仅提供禁止访问策略。如有需要,可自行微调代码实现302跳转。

访问控制策略

以下策略基于Vercel Middleware功能[7]实现。

Vercel Middleware在Vercel部署的每个访问请求之前被调用,可用于实现各种访问控制策略。

屏蔽指定域名

需求场景

  • 需要屏蔽全部来自Vercel默认域名的访问
  • 需要仅放行来自特定域名的访问

代码

在项目根目录中添加middleware.ts

/middleware.ts
1
2
3
4
5
6
export default function middleware(request: Request) {
// 屏蔽除了mydomain.com以外的全部域名的访问
if (url.hostname === 'mydomain.com') {
return new Response(null, {status: 404});
}
}

屏蔽指定路径

需求场景

  • 仅需要屏蔽部分敏感目录的访问(如预览环境中的/admin*)

代码

/middleware.ts
1
2
3
4
5
6
export default function middleware(request: Request) {
// 屏蔽/admin*的访问
if (url.pathname.startsWith('/admin')) {
return new Response(null, {status: 404});
}
}

屏蔽过期部署

需求场景

  • 需要部署(如预览部署)在指定时间后失效无法访问,避免预览环境被意外泄漏/抓取

代码

实现思路
在Middleware中添加构建时间戳,基于请求访问时刻进行访问控制;
构建时自动触发脚本,向Middleware注入硬编码时间戳。

/middleware.ts
1
2
3
4
5
6
7
8
9
export default function middleware(request: Request) {
// 时间,过期失效
const buildTime = new Date("2077-01-01T00:00:00Z");
const now = new Date();
const oneDay = 24 * 60 * 60 * 1000; // 一天
if (now.getTime() - buildTime.getTime() > oneDay) {
return new Response(null, {status: 404});
}
}
/replace.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

const path = './middleware.ts'
// 以同步方式读取文件内容
let content = fs.readFileSync(path, {encoding: 'utf8'});

// 获取当前时间并格式化为ISO字符串
const now = new Date().toISOString();

// 替换占位符
content = content.replace('2077-01-01T00:00:00Z', now);

// 将新的文件内容写回文件
fs.writeFileSync(path, content);

console.log(`Build time of middleware.ts is set to ${now}.`);

在正式构建前启动replace.js:修改package.json

/package.json
1
2
3
4
5
6
7
{
...
"scripts": {
"expirable-build": "node middleware-replace.js && hexo generate"
},
...
}

修改Vercel构建指令为expirable-build

Vercel项目 -> Settings -> Build & Development Settings -> Build Command -> npm run expirable-build (需要重新触发构建)

双向鉴权

需求场景

  • 仅允许特定来源的请求代理访问
  • 针对上述基于request.url进行访问限制判断的策略,加密避免伪造Request header绕过访问限制策略
  • 屏蔽意外的Vercel响应,例如Vercel本身的404错误页面,或等待部署完成的页面:

代码

实现思路
在Middleware中对Request header鉴权;
在反向代理方(如Cloudflare workers)对Response header鉴权。

/middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export default function middleware(request: Request) {
// 鉴权,添加自cloudflare workers
if (request.headers.get('x-request-auth') !== 'RequestPassword') {
return new Response(null, {status: 404});
}

// 返回自定义的Response Header
const headers = new Headers({
'x-response-auth': 'ResponsePassword',
//参考@vercel/edge API,委托响应给Vercel获取原始响应,并附加响应头
'x-middleware-next': '1'
});
return new Response(null, {headers})
}

在Cloudflare中创建一个Workers反向代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export default {
async fetch(request, env, ctx) {
const newHeaders = new Headers(request.headers);
newHeaders.append("x-request-auth", "RequestPassword");

// 这里定义真正需要访问的Vercel部署URL,例如*.vercel.app
const url = new URL(request.url);

const newRequest = new Request(url, {
body: request.body,
headers: newHeaders,
method: request.method,
redirect: request.redirect,
});
const response = await fetch(newRequest);

// 校验response密钥
if (response.headers.get('x-response-auth') !== 'ResponsePassword') {
return new Response(null, { status: 404 });
}

// 新建一个 Headers 对象来复制原始响应头
const newResponseHeaders = new Headers(response.headers);
newResponseHeaders.delete("x-response-auth");
// 删除所有以 x-vercel 开头的header
for (let [key] of newResponseHeaders) {
if (key.startsWith("x-vercel")) {
newResponseHeaders.delete(key);
}
}

// 创建一个新的 Response 对象,使用新的头部和原始响应的body
const newResponse = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newResponseHeaders
});

return newResponse;
},
};

现在Vercel部署仅可通过Cloudflare workers路由反向代理访问,如*.workers.dev或自定义Workers路由。

集成

同时启用上述所有访问控制策略:

/middleware.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default function middleware(request: Request) {
// 鉴权,添加自cloudflare workers
if (request.headers.get('x-request-auth') !== 'RequestPassword') {
return new Response(null, {status: 404});
}

// 时间,过期失效
const buildTime = new Date("2077-01-01T00:00:00Z");
const now = new Date();
const oneDay = 24 * 60 * 60 * 1000; // 一天
if (now.getTime() - buildTime.getTime() > oneDay) {
return new Response(null, {status: 404});
}

const url = new URL(request.url);
// 屏蔽/admin*的访问
if (url.pathname.startsWith('/admin')) {
return new Response(null, {status: 404});
}

// 返回自定义的Response Header
const headers = new Headers({
'x-response-auth': 'ResponsePassword',
//参考@vercel/edge API,委托响应给Vercel获取原始响应,并附加响应头
'x-middleware-next': '1'
});
return new Response(null, {headers})
}

注意事项

  1. 无法影响Vercel历史的部署
    如果担心泄漏,可手动删除历史部署,或批量删除(移除Vercel项目并重新添加Vercel项目);
  2. 注意修改Vercel项目设置,避免泄漏Vercel信息
    Settings -> General -> Comments -> Off
    Settings -> Domains -> 删除Vercel默认分配的随机域名
    Settings -> Deployment Protection -> Vercel Authentication -> Disabled(访问预览部署时无需登录)

Q&A

Vercel提供的域名URL是否会在证书提供商处泄漏?

(应该)不会。与Cloudflare Pages的*.pages.dev不同,Vercel对*.vercel.app域名申请了共享证书,并不会为其中的每个子域名申请证书,因此不会泄漏URL地址。


  1. 1.Introducing Cloudflare Pages: the best way to build JAMstack websites
  2. 2.Vercel - Errors Accessing From China (14/May/21)
  3. 3.Limits · Cloudflare Pages docs
  4. 4.Pricing - Vercel
  5. 5.Accessing Deployments through Generated URLs - Vercel
  6. 6.Exposed vercel preview deploys · vercel/vercel · Discussion #5711
  7. 7.Edge Middleware API Reference - Vercel