Cloudflare 页面的密码保护
Cloudflare Pages是一项非常棒的静态网站托管服务:设置极其简单,每次向 GitHub 或 GitLab 存储库提交代码时都会自动部署您的网站,而且其免费计划非常慷慨,提供无限的用户、网站、请求和带宽。
在部署和预览静态网站方面,Pages 与 Vercel 或 Netlify 等产品非常相似。然而,与主要竞争对手相比,它缺少的一项功能是使用简单的纯密码授权来保护环境。
您可以通过与 Cloudflare 的Access产品(最多 50 个用户免费)集成来限制对 Pages 环境的访问,如果您正在寻找功能齐全的身份验证机制,那么您绝对应该考虑一下它。
但如果您需要的只是基本的保护层,让您的网站不会立即对公众开放,那么像 Netlify 和 Vercel 提供的这种简单的仅密码身份验证功能可能正是您所需要的。
在这篇文章中,我将讨论如何通过构建一个由 Cloudflare Workers(Cloudflare 的无服务器平台)支持的小型身份验证服务器来为您的 Cloudflare Pages 站点设置密码保护。
您可以在这里查看最终结果的演示:https://cloudflare-pages-auth.pages.dev/(密码password:)。
TLDR
如果您想为自己的 Cloudflare Pages 网站添加密码保护,只需前往存储库并按照那里的说明操作即可。
你基本上需要做两件事:
- 将仓库目录中的内容复制
functions到您自己的项目中。 - 在 Cloudflare Pages 控制面板中添加一个
CFP_PASSWORD环境变量,用于设置您想要使用的密码。
搞定!下次部署时,您的网站就会受到密码保护啦🎉
⚠️ 注意:您可能还需要将 Cloudflare 项目设置更新为“失败关闭”。否则,如果您达到每日函数请求上限,您的网站将失去保护。感谢 Thomas 提醒我这一点!
如果您想了解更多相关信息,请继续阅读!
页面、工作线程和函数
Cloudflare Pages 主要是一个用于托管静态网站的服务,这意味着要运行我们的小型身份验证应用程序,我们需要一个可以执行服务器端功能的后端环境。
这时Cloudflare Workers就派上了用场,它是一个无服务器执行环境(类似于 AWS Lambda 或 Vercel Edge Functions),我们可以使用它在 Cloudflare 速度惊人的边缘网络上运行我们的身份验证应用程序。
Pages 和 Workers 是两个独立的产品,虽然它们可以很好地集成在一起,但如果您想构建一个同时使用它们的应用程序,通常需要创建两个独立的项目,并分别管理和部署它们。幸运的是,我们可以使用 Cloudflare Functions 这项功能来简化操作。
函数是 Cloudflare Pages 的一项功能,它充当 Pages 站点和 Workers 环境之间的桥梁。使用函数的优势在于,我们可以将其作为 Pages 项目的一部分进行管理和部署,而无需创建单独的 Workers 应用程序。
要创建一个函数,我们只需functions在项目根目录下创建一个文件夹,并在其中添加 JavaScript 或 TypeScript 文件来处理函数的逻辑。这还会根据该文件夹的文件结构生成路由表。例如,如果我们创建以下脚本functions/api/hello-world.js:
// functions/api/hello-world.js
export async function onRequest(context) {
return new Response("Hello, world!");
}
当我们部署网站时,此功能将可通过以下 URL 访问:https://your-site.pages.dev/api/hello-world。
如果您想了解有关函数和工作进程的更多信息,请查看Cloudflare Docs网站上的各种资源。
中间件
我们的小型身份验证应用程序需要一种方法来拦截所有发送到我们页面项目的请求,以便验证用户是否拥有网站访问权限,如果没有,则将其重定向到登录页面。我们可以使用中间件来实现这一点,中间件是一种特殊的函数,它位于用户请求和路由处理程序之间。
要为网站上的所有页面创建中间件,我们需要_middleware.js向文件夹中添加一个文件functions。以下是一个中间件示例,如果您尝试访问该/admin路由,它会返回不同的响应。
export async function onRequest(context) {
const { request, next } = context;
const { pathname } = new URL(request.url);
if (pathname === '/admin') {
return new Response('You need to log in!')
}
return await next();
}
一个简单的密码保护服务器
现在我们已经了解了函数、工作线程和中间件的工作原理,可以开始设计应用程序,使其能够在任何Pages 网站上运行。我们将保持应用程序的简洁性:
- 我们将使用中间件拦截所有对网站的请求,如果用户未通过身份验证,则将其重定向到登录页面。
- 我们将创建一个路由来处理登录表单的提交,并验证用户是否提供了正确的密码(密码存储在环境变量中)。
- 如果他们提供了正确的密码,我们将设置一个包含哈希值的 cookie,后续请求将使用该哈希值来验证他们是否已通过身份验证。
整体设计图如下:
您可以在示例仓库functions的文件夹中查看支持此密码保护服务器的完整实现。该文件夹包含 5 个文件(用 TypeScript 编写,但如果您更熟悉纯 JavaScript,可以删除类型定义并重命名):.js
_middleware.ts-> 拦截我们 Pages 网站所有请求的中间件。cfp_login.ts-> 处理向路由发送 POST 请求的函数/cfp_login。constants.ts-> 您可以根据自己的喜好使用一些常量来定制服务。template.ts-> 登录页面的 HTML 模板。utils.ts-> 一些用于加密密码和处理 cookie 的实用函数。
constants.ts和文件中没有什么特别有趣的事情发生template.ts,utils.ts所以我将重点放在另外两个文件中:
_middleware.ts
// functions/_middleware.ts
import { CFP_ALLOWED_PATHS } from './constants';
import { getCookieKeyValue } from './utils';
import { getTemplate } from './template';
export async function onRequest(context: {
request: Request;
next: () => Promise<Response>;
env: { CFP_PASSWORD?: string };
}): Promise<Response> {
const { request, next, env } = context;
const { pathname, searchParams } = new URL(request.url);
const { error } = Object.fromEntries(searchParams);
const cookie = request.headers.get('cookie') || '';
const cookieKeyValue = await getCookieKeyValue(env.CFP_PASSWORD);
if (
cookie.includes(cookieKeyValue) ||
CFP_ALLOWED_PATHS.includes(pathname) ||
!env.CFP_PASSWORD
) {
// Correct hash in cookie, allowed path, or no password set.
// Continue to next middleware.
return await next();
} else {
// No cookie or incorrect hash in cookie. Redirect to login.
return new Response(getTemplate({ withError: error === '1' }), {
headers: {
'content-type': 'text/html'
}
});
}
}
正如我们之前讨论过的,这个函数会拦截所有对我们 Pages 网站的请求。如果你查看函数体,它只不过是一个大型的 if/else 语句:
- 如果请求包含具有正确身份验证哈希的 cookie,或者路径在允许的路径列表中(您不想用密码保护的路径),或者
CFP_PASSWORD环境变量未设置,则继续执行下一个中间件,在我们的例子中,这意味着用我们拦截的路由进行响应。 - 否则,返回函数的内容
getTemplate(),即登录页面的 HTML 模板。
cfp_login.ts
该应用程序的另一个有趣组成部分是cfp_login.ts函数,它又是一个庞大的 if/else 语句块:
// functions/cfp_login.ts
import { CFP_COOKIE_MAX_AGE } from './constants';
import { sha256, getCookieKeyValue } from './utils';
export async function onRequestPost(context: {
request: Request;
env: { CFP_PASSWORD?: string };
}): Promise<Response> {
const { request, env } = context;
const body = await request.formData();
const { password } = Object.fromEntries(body);
const hashedPassword = await sha256(password.toString());
const hashedCfpPassword = await sha256(env.CFP_PASSWORD);
if (hashedPassword === hashedCfpPassword) {
// Valid password. Redirect to home page and set cookie with auth hash.
const cookieKeyValue = await getCookieKeyValue(env.CFP_PASSWORD);
return new Response('', {
status: 302,
headers: {
'Set-Cookie': `${cookieKeyValue}; Max-Age=${CFP_COOKIE_MAX_AGE}; Path=/; HttpOnly; Secure`,
'Cache-Control': 'no-cache',
Location: '/'
}
});
} else {
// Invalid password. Redirect to login page with error.
return new Response('', {
status: 302,
headers: {
'Cache-Control': 'no-cache',
Location: '/?error=1'
}
});
}
}
请注意,我们导出的是一个名为 `out` 的函数,onRequestPost而不是onRequest前一个文件中的函数。这是因为我们希望此路由响应发送到该/cfp_login路径的 POST 请求。
该函数的主体会将用户通过登录表单提供的密码哈希值与CFP_PASSWORD环境变量中存储的密码哈希值进行比较。如果两者匹配,则表示用户输入的密码正确,此时我们会将用户重定向到首页,同时设置一个 cookie,其值即为该密码的哈希值。
否则,我们将重定向到主页并?error=1设置查询参数,在我们的模板中,我们使用该参数来显示错误消息。
我们设置的 cookie 默认有效期为一周(可在配置constants.ts文件中自定义)。该 cookie 会包含在后续每次访问我们网站的请求中,只要其值正确,就能满足函数的条件_middleware.ts,从而直接返回请求页面,而无需再次输入密码。
设置密码
最后一步是创建CFP_PASSWORD用于保护网站的密码环境变量。您可以在页面网站控制面板的“设置”->“环境变量”下进行此操作。如果需要,您可以为生产环境和预览环境设置不同的密码。
更改密码
我们的简易身份验证服务器没有实际的“会话”,因此如果您决定使用CFP_PASSWORD不同的密码更改环境变量,也不会使任何内容失效。
更改密码会导致 cookie 中的哈希值与服务器上的哈希值不再匹配,从而在用户下次尝试访问页面时提示用户输入新密码。
本地运行
要在本地运行函数并在您自己的计算机上测试密码保护,您可以使用wrangler CLI,命令如下npx:
npx wrangler pages dev build -b CFP_PASSWORD=password
请注意,运行 CLI 命令时需要传递CFP_PASSWORD环境变量。如果不传递,网站虽然可以访问,但不会设置密码保护。
这就是我全部的资料了!
希望这篇文章和示例项目对您有所帮助。如果您在自己的 Pages 网站上尝试了一下,请在评论区告诉我结果如何!
感谢阅读~ <3
文章来源:https://dev.to/charca/password-protection-for-cloudflare-pages-8ma


