在不断发展的 Web 开发领域,无服务器应用程序正在稳步留下自己的印记。 它们无可否认的优势,例如无与伦比的可扩展性和强大的性能,使它们脱颖而出。 无服务器的美妙之处在于它的承诺:提供强大的解决方案,不仅可以轻松扩展,而且预算友好。
您是否曾经梦想过推出一个商业创意,但却被创建用户注册和登录系统的技术细节所阻碍? 虽然 Netlify 等平台和各种基于云的数据库确实存在,但它们无法与 Cloudflare 提供的功能相比。 许多替代方案在扩展时可能会增加成本,但对于 Cloudflare Pages,情况有所不同。
在过去的几个月里,我一直断断续续地致力于这个项目和演示,同时兼顾我的其他承诺。 我对等待表示歉意,尤其是对那些热切期待 这个系统的人。
释放 Cloudflare 页面的力量
- 无缝可扩展性:轻松管理无限的用户注册。
- 成本效益:告别意外的管理费用; 享受一致的定价。
- 极速:体验前所未有的性能。
- 可靠的安全性:您的用户数据仍然受到保护且安全。
我们将使用什么
- Cloudflare 页面
- Cloudflare 页面功能
- Cloudflare Workers KV
什么是 Cloudflare 页面?
Cloudflare Pages 是一个现代、用户友好的平台,供开发人员构建、部署和托管其网站。 它提供与 GitHub 的无缝集成,这意味着您只需将代码推送到 GitHub,Cloudflare Pages 将处理其余的工作 - 构建、部署,甚至更新。
它的工作原理如下:
- 集成工作流程:Cloudflare Pages 围绕 git 工作流程构建。 将 GitHub 存储库连接到 Cloudflare Pages 后,每次您推送到所选分支时,它都会开始构建和部署您的站点。
- JAMstack 优化:Cloudflare Pages 支持 JAMstack 原则,这意味着您可以使用您喜欢的静态站点生成器或 JavaScript 框架(包括但不限于 Jekyll、Hugo、Next.js 和 React)构建站点。
- 快速、安全的交付:Pages 由全球分布的 Cloudflare 网络提供支持,确保您的网站可用且快速,无论您的受众身在何处。 此外,Cloudflare 固有的安全功能可保护您的网站免受威胁。
- 持续部署:每次您在 GitHub 存储库上进行更新时,Cloudflare Pages 都会自动构建和部署您的网站。 这使您可以快速迭代并使部署过程变得轻而易举。
- 自定义域和 HTTPS:使用 Pages,您可以将自定义域连接到您的站点,并且它在所有站点上提供免费、自动的 HTTPS,以确保连接始终安全。
- 预览部署:每当您在链接的 GitHub 存储库中创建新的拉取请求时,Cloudflare Pages 都会自动生成一个唯一的预览 URL,以便您在上线之前查看更改。
无论您是独立开发者还是大型团队的一员,Cloudflare Pages 都提供了一种简单、快速且安全的方式让您的网站上线。
鉴于上述情况,对于这个用户注册系统,我选择了纯粹且直接的 HTML 页面,避开任何额外的框架或构建工具。 这种方法确保了无与伦比的简单性,并提供了实现任何期望结果的灵活性。
什么是 Cloudflare Workers?
Cloudflare Workers 是一个创新的无服务器计算平台,让开发人员可以将代码直接部署到 Cloudflare 的广泛网络,该网络遍布全球 200 多个城市。 从本质上讲,它使应用程序能够尽可能靠近最终用户运行,从而减少延迟并增强用户体验。
以下是其功能和优点的概述:
- 无服务器执行环境:Cloudflare Workers 在无服务器环境中运行,这意味着开发人员无需管理或维护任何服务器。 相反,他们可以专注于编写代码,而平台则负责从分发到扩展的其余部分。
- 边缘计算:与应用程序在单个服务器或数据中心上运行的传统模型不同,Cloudflare Workers 将您的代码带到 Cloudflare 网络的边缘。 这可确保您的应用程序运行时更贴近用户,从而提高性能和速度。
- 语言灵活性:Workers 使用 V8 JavaScript 引擎,与 Chrome 使用的运行时相同,允许开发人员使用 JavaScript 编写代码。 此外,由于 WebAssembly 支持,还可以使用 Rust、C 和 C++ 等其他语言。
- 安全性:通过利用 Cloudflare 网络固有的安全性,Workers 有助于保护应用程序免受 DDoS 攻击等各种威胁。
Cloudflare Workers 为希望增强应用程序性能、可靠性和安全性的开发人员提供了创新且高度可扩展的解决方案。
在 Cloudflare Pages 中,Workers 位于名为functions
的目录中。 我已将所有 JavaScript/TypeScript 代码放置在这个空间中,利用 Workers 提供的全面功能。
什么是 Cloudflare Workers KV?
Cloudflare Workers KV (Key-Value) 是一个全球分布式、最终一致的键值存储系统,允许您 从 Cloudflare Workers 脚本中的任何位置存储和访问数据。 它旨在帮助您扩展和简化无服务器环境中的状态管理。
以下是其主要特性和优点:
- 全球分布:Cloudflare Workers KV 构建在 Cloudflare 网络之上,该网络覆盖全球 300 多个城市。 这可确保您的数据在用户附近存储和访问,从而减少延迟并提高应用程序的整体性能。
- 快速读写:Workers KV 提供适合各种应用的低延迟数据访问。 虽然写入需要更长的时间才能在全局范围内传播(通常在几秒钟内),但读取操作通常速度很快,因此非常适合读取密集型工作负载。
- 大规模:您可以在单个 Workers KV 命名空间中存储数十亿个密钥,每个密钥可以容纳高达 25MB 的值。
- 命名空间:KV 命名空间是键值对的容器。 它们允许您在 Workers KV 存储中隔离不同的数据集,这在管理多个应用程序或环境(例如登台和生产)时特别有用。
- 最终一致性:Workers KV 使用最终一致性。 这意味着数据更新(写入)将在全球范围内传播并随着时间的推移变得一致,这通常只需几秒钟。
Cloudflare Workers KV 提供了一种独特的解决方案,用于在无服务器环境中管理状态,为开发人员提供可靠、快速且全球分布式的数据存储系统。
在开发这个用户注册系统时,我战略性地设计了以下 Workers KV 命名空间:
- USERS:这是所有用户的主存储。 它旨在处理本质上无限数量的记录。
- USERS_LOGIN_HISTORY:记录登录活动的专用空间,使用户能够定期评估其帐户的安全足迹。
- USERS_SESSIONS:此命名空间捕获有关当前登录用户的详细信息,包括唯一 ID、设备、位置等。
- USERS_SESSIONS_MAPPING:由于 Workers KV 的最终一致性模型,写入“USERS_SESSIONS”和检查它之间可能存在延迟。 如果操作发生在不同的边缘位置,则这种情况尤其可能发生。 为了避免这种情况,在验证后,我直接将新的会话 UID 添加到 USERS_SESSIONS_MAPPING,确保在写入“USERS_SESSIONS”之前就将其包含在内。
- USERS_TEMP:我使用此命名空间作为临时(临时)链接和其他具有预定到期时间的内容的存储库。
我们创建了具有无限容量、自动扩展和高可用性的数据库——这些功能通常存在于昂贵的数据库中。
设计项目
我的目标是在不依赖第三方库的情况下制作一些简单有效的东西,并且我成功了。 整个项目结构是这样展开的:
1+---framework
2| +---models
3| | password.ts
4| | user.ts
5| +---templates
6| | \---emails
7| | password-reset.ts
8| | password-updated.ts
9| \---utils
10| encryptor.ts
11| index.ts
12| mailer.ts
13| validators.ts
14+---functions
15| | forgot-password.ts
16| | login.ts
17| | logout.ts
18| | password-reset.ts
19| | register.ts
20| | _middleware.ts
21| \---user
22| dashboard.ts
23| _middleware.ts
24\---public
25 | forgot-password.html
26 | index.html
27 | login.html
28 | logout.html
29 | password-reset.html
30 | register.html
31 \---user
32 dashboard.html
为了让您清楚地了解上面的架构,让我们深入研究一下详细的细分:
- framework:该目录包含我们的基础 TypeScript 代码。 从数据模型到电子邮件模板的所有内容都位于此处,确保整个系统采用一致的方法。
- functions:在这里,您将找到专为 Cloudflare Pages 函数定制的 TypeScript 代码,从而简化了站点的后端操作。
- public:我们所有可公开访问的静态 HTML 文件都驻留在此文件夹中,形成用户可见的界面。
简而言之,当您导航到login.html
页面时,Cloudflare Pages 就会启动,执行相应的login.ts
代码。 这种动态相互作用在所有页面及其相关功能中持续存在。
通过这种设置,我们可以无缝地处理一系列任务。 无论是内容重写、数据处理还是通过 Cloudflare Workers KV 获取数据,一切都得到有效管理。
构建用户注册系统
在开始我们的旅程之前,我们将首先建立一个用户注册系统。 这是核心功能。
- 我们的第一步是设计一个简单的 HTML 表单并将其放置在
register.html
中,然后编写负责处理所提供数据的函数:
1<form class="row g-3 needs-validation" id="register" method="POST" novalidate>
2 <div class="col-md-6 mb-3">
3 <label for="firstName" class="form-label">First Name</label>
4 <input type="text" class="form-control" name="firstName" id="firstName" autocomplete="given-name" placeholder="Required" required>
5 <div class="invalid-feedback">You must enter your First Name.</div>
6 </div>
7 <div class="col-md-6 mb-3">
8 <label for="lastName" class="form-label">Last Name</label>
9 <input type="text" class="form-control" name="lastName" id="lastName" autocomplete="family-name" placeholder="Required" required>
10 <div class="invalid-feedback">You must enter your Last Name.</div>
11 </div>
12 <div class="col-md-12 mb-3">
13 <label for="email" class="form-label">Email</label>
14 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
15 <div class="invalid-feedback">You must enter your Email.</div>
16 </div>
17 <div class="col-md-12 mb-3">
18 <label for="company" class="form-label">Company</label>
19 <input type="text" class="form-control" name="company" id="company" autocomplete="organization" placeholder="Optional">
20 <div class="invalid-feedback">You must enter your Company.</div>
21 </div>
22 <div class="col-md-6 mb-3">
23 <label for="password" class="form-label">Password</label>
24 <input type="password" class="form-control" name="password" id="password" autocomplete="new-password" placeholder="Required" required>
25 <div class="invalid-feedback">You must enter a Password.</div>
26 </div>
27 <div class="col-md-6 mb-3">
28 <label for="confirm_password" class="form-label">Confirm Password</label>
29 <input type="password" class="form-control" name="confirm_password" id="confirm_password" autocomplete="new-password" placeholder="Required" required>
30 <div class="invalid-feedback">Your Password does not match.</div>
31 </div>
32 <div class="col-md-12 mb-3">
33 <div class="form-check">
34 <input class="form-check-input" type="checkbox" name="terms" id="terms" required>
35 <label class="form-check-label" for="terms">
36 I have read and agree to the <a href="#" class="link-primary">Terms of Service</a>, <a href="#" class="link-primary">Privacy Policy</a> and <a href="#" class="link-primary">Cookies Policy</a>.
37 </label>
38 <div class="invalid-feedback">You must agree before registering.</div>
39 </div>
40 </div>
41 <div class="col-md-6 mx-auto mb-3">
42 <div class="d-grid gap-2">
43 <button class="btn btn-primary" type="submit">Create Account</button>
44 </div>
45 </div>
46</form>
设置好表单后,下一步是使用 JavaScript 增强数据提交。 虽然传统的表单提交完全可行,但我在示例中合并了 Bootstrap 5 验证,因此倾向于 AJAX 来发布数据。
这是我的演示中的一个工作示例:
1(function () {
2 'use strict'
3
4 // Check if passwords match
5 document.querySelectorAll("#password, #confirm_password").forEach(function (input) {
6 input.addEventListener("keyup", function () {
7 const password = document.getElementById("password");
8 const confirmPassword = document.getElementById("confirm_password");
9 if (
10 password.value !== "" &&
11 confirmPassword.value !== "" &&
12 confirmPassword.value === password.value
13 ) {
14 // Do something when passwords match
15 }
16 });
17 });
18
19 var form = document.querySelector("#register");
20
21 form.addEventListener('submit', function (event)
22 {
23 const _alert = document.querySelector("div.alert.alert-danger");
24
25 if(_alert)
26 _alert.remove();
27
28 if (!form.checkValidity())
29 {
30 // Prevent default form submission
31 event.preventDefault();
32 event.stopPropagation();
33 form.classList.add('was-validated');
34 }
35 else
36 {
37 // Mark inputs as validated
38 form.classList.add('was-validated');
39
40 // Prevent default form submission again
41 event.preventDefault();
42 event.stopPropagation();
43
44 // Helper to get all inputs we want
45 const getAllFormElements = element => Array.from(element.elements).filter(tag => ["input"].includes(tag.tagName.toLowerCase()));
46
47 // Grab desired inputs
48 const registerFormElements = getAllFormElements(event.currentTarget);
49
50 // Loop over them and disable
51 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
52 element.setAttribute("readonly", true);
53 element.style = 'background-color: #e9ecef; opacity: 1;';
54 });
55
56 // Disable button and show loading spinner
57 let _button = event.currentTarget.querySelector(".btn");
58 _button.setAttribute("disabled", true);
59 _button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Please wait...';
60
61 // A modern replacement for XMLHttpRequest.
62 // https://caniuse.com/fetch
63 if (window.fetch)
64 {
65 (async() => {
66 await fetch(window.location.href, {
67 method: 'POST',
68 // NOTE: FormData will only grab inputs with name attribute
69 body: JSON.stringify(Object.fromEntries(new FormData(event.target))),
70 headers: {
71 'Accept': 'application/json',
72 'Content-Type': 'application/json'
73 }
74 }).then((response) => {
75 // Pass both the parsed JSON and the status code to the next .then()
76 return response.json().then((data) => ({status: response.status, body: data}));
77 }).then(({status, body}) => {
78
79 if (body.success === true && status === 201) {
80 Array.prototype.slice.call(document.querySelectorAll('form *')).forEach(function (element) {
81 element.style.display = "none";
82 });
83
84 let _alert = document.createElement("div");
85 _alert.classList.add('alert');
86 _alert.classList.add('alert-success');
87 _alert.setAttribute("role", 'alert');
88 _alert.innerHTML = '<p>Thank you for joining! A confirmation email has been sent out. You need to verify your email before trying to log in.</p>';
89
90 form.prepend(_alert);
91 } else {
92 let _alert = document.createElement("div");
93 _alert.classList.add('alert');
94 _alert.classList.add('alert-danger');
95 _alert.setAttribute("role", 'alert');
96 _alert.innerText = `Error #${data.error.code}: ${data.error.message}`;
97
98 form.prepend(_alert);
99
100 form.classList.remove('was-validated');
101
102 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
103 element.removeAttribute("style");
104 element.removeAttribute("readonly");
105 });
106
107 _button.removeAttribute("disabled");
108 _button.innerHTML = 'Create Account';
109 }
110 });
111 })();
112 }
113 else
114 {
115 alert("Your browser is too old!\nIt does not have the most recent features.\nPlease update your browser or use a different one.");
116 }
117 }
118 }, false);
119})();
单击“创建帐户”后,数据将通过 AJAX 发布到/register
,从而触发functions/register.ts
代码。 该机制允许 GET 和 POST 数据处理。 虽然管理 OPTIONS 是可能的,但我们将在本次讨论中将其放在一边。
- 对于标准页面访问或 GET 执行,有可能引入大量高级功能,例如 CSRF 保护。 让我们看一下下面的例子:
1/**
2 * GET /register
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // Fetch the original page content
7 const response = await next();
8
9 // Prepare our CSRF data
10 const IP = request.headers.get('CF-Connecting-IP');
11 const Country = request.headers.get('CF-IPCountry') || '';
12 const UserAgent = request.headers.get('User-Agent');
13 const expiry = Date.now() + 60000;
14 const CSRF_TOKEN = JSON.stringify({i: IP, c: Country, u: UserAgent, e: expiry});
15 const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
16 const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
17
18 // Rewrite the content and stream it back to the user (async)
19 return new HTMLRewriter()
20 .on("form", {
21 element(form) {
22 // The CSRF input
23 form.append(
24 `<input type="hidden" name="csrf" value="${hex}" />`,
25 { html: true }
26 );
27 },
28 })
29 .transform(response);
30};
在此过程中,我们使用 Cloudflare Workers HTMLRewriter 功能为每个页面请求生成唯一的 CSRF 令牌并将其嵌入到表单中。
我将在后续部分中更深入地研究我的 CSRF 策略。 暂时观察我如何动态附加唯一的随机 CSRF 代码来增强安全性。
- 现在,让我们转向 POST 执行阶段。 在这里,我们仔细验证输入数据,安全存储,然后向用户发送电子邮件以进行确认或进一步说明。 为了向您提供概念性概述,我起草了一个伪代码表示:
1/**
2 * POST /register
3 */
4export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) =>
5{
6 // 验证内容类型
7 // 在做任何事情之前验证我们的 CSRF
8 // 检查现有用户电子邮件
9 // 生成新的盐并对原始密码进行哈希处理
10 // 存储用户一些元数据
11 // ETC
12}
在我的配置中,成功注册后,用户的详细信息将存储在 Workers KV 中,如下所示:
1{
2 "uid": "888e8801-3b35-4dd3-9ae2-7ff0f564bb3b",
3 "email": "[email protected]",
4 "firstname": "John",
5 "lastname": "Smith",
6 "company": "",
7 "role": "User",
8 "password": "....",
9 "salt": "...",
10 "access_token": null,
11 "created_at": 123456789,
12 "confirmed_at": null,
13 "last_updated": null,
14 "last_sign_in_at": 123456789,
15 "recovery_sent_at": null,
16 "password_changed_at": null
17}
您可以调整此格式,根据需要添加或删除字段。 我必须强调,我不会提供完整的、可直接复制的代码。 投资于理解和实践探索至关重要。 只有这样,一个人才能真正成长为一名开发人员。
构建用户登录系统
过渡到 Cloudflare Pages 企业的登录机制非常简单。 我们将采用与注册过程类似的方法。
- 正如我们制作注册表单一样,为登录过程制作一个简洁的表单并将其放置在
login.html
中:
1<form class="row g-3 needs-validation" id="login" method="POST" novalidate>
2 <div class="col-sm-12">
3 <label for="email" class="form-label">Email</label>
4 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
5 <div class="invalid-feedback">You must enter your Email.</div>
6 </div>
7 <div class="col-sm-12">
8 <label for="password" class="form-label">Password</label>
9 <input type="password" class="form-control" name="password" id="password" autocomplete="password" placeholder="Required" required>
10 <div class="invalid-feedback">You must enter a Password.</div>
11 </div>
12 <div class="d-grid gap-2 col-sm-6 mx-auto">
13 <button class="btn btn-primary" type="submit">Login</button>
14 </div>
15</form>
- 设置表单后,就可以处理其渲染了。 后续步骤涉及创建必要的代码。 这将从
login.ts
自动触发:
1/**
2 * GET /login
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // 获取原始页面内容
7 // 准备我们的CSRF数据
8 // 重写内容并将其流式传输回用户(异步)
9};
- 最后一步是制定代码来管理 POST 执行,位于
login.ts
中:
1/**
2 * POST /login
3 */
4export const onRequestPost: PagesFunction<{
5 USERS: KVNamespace;
6 USERS_SESSIONS: KVNamespace;
7 USERS_SESSIONS_MAPPING: KVNamespace;
8 USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) =>
9{
10 // 验证内容类型
11 // 在做任何事情之前验证我们的 CSRF
12 // 检查现有用户电子邮件
13 // 生成新的盐并对原始密码进行哈希处理
14 // 比较密码
15 // 保存会议
16 // 更新历史记录
17 // 检索该用户的当前映射(如果存在)
18 // 将新的会话 UID 添加到映射中
19 // 存储更新后的映射
20 // ETC
21};
通过这 3 个简化的步骤,我们无缝地构建了登录系统。 不可否认,登录机制是最复杂的部分,需要执行注释中的多项操作。 然而,不要将此视为一个复杂的问题,而是一个需要克服的令人振奋的挑战!
构建用户密码重置系统
- 与我们之前的组件相似,我们首先设计用于启动密码重置的专用表单:
1<form class="row g-3 needs-validation" id="forgot" novalidate>
2 <div class="col-sm-12">
3 <label for="email" class="form-label">Email</label>
4 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required />
5 <div class="invalid-feedback">You must enter your Email.</div>
6 </div>
7
8 <div class="d-grid gap-2 col-sm-6 mx-auto">
9 <button class="btn btn-primary" type="submit">Request</button>
10 </div>
11</form>
基本概念很简单:输入用户的电子邮件后,如果我们的系统能够识别该电子邮件,则会生成并发送一个单一的密码重置链接。 为了增强安全性,请确保此链接在短时间内保持活动状态,最好不超过 2 小时。
- 开始编写专门用于管理 GET 执行的代码,您需要将其嵌入到
forgot-password.ts
中:
1/**
2 * GET /forgot-password
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // 获取原始页面内容
7 // 准备我们的CSRF数据
8 // 重写内容并将其流式传输回用户(异步)
9};
- 继续制定负责 POST 执行的代码,确保其位于同一个
forgot-password.ts
文件中:
1/**
2 * POST /forgot-password
3 */
4export const onRequestPost: PagesFunction<{
5 USERS: KVNamespace;
6 USERS_TEMP: KVNamespace;
7 }> = async ({ request, env }) =>
8{
9 // 验证内容类型
10 // 在做任何事情之前验证我们的 CSRF
11 // 检查现有用户电子邮件
12 // 生成一个唯一的字符串进行哈希
13 // 散列字符串
14 // 保存到KV,过期2小时
15 // 发送邮件给用户
16 // ETC
17};
现在由您来完成编码难题。 通过遵循我的评论中的指导,您应该会发现该过程非常易于管理。 完成后,您将配备一个完全可操作的密码重置系统。
注意事项:不要显示诸如不存在此类电子邮件
之类的消息。 这些指标对于黑客来说是金矿,为潜在的暴力攻击铺平了道路。 相反,采用一种更加模糊但用户友好的方法:如果电子邮件在我们的系统中注册,您将收到一个重置链接
。
用户注册演示
探索我建立的用户注册流程的现场演示:https://members.mecanik.dev/
特点包括:
我目前正在处理其他项目,但我将继续增强这个平台。 请随意测试 - 只要确保您使用真实的电子邮件即可,以避免影响我的 SendGrid 声誉。
我欢迎任何反馈或报告的问题。 有的话请联系!
## 问题与解答
毫无疑问,安全是首要考虑因素。 您可能会质疑系统针对潜在攻击和数据泄露的稳健性。
考虑到 Cloudflare Pages 在 Cloudflare 平台上运行,允许您访问其高级 Web 应用程序防火墙 (WAF) 和一套安全工具。
CSRF 实际上是如何工作的?
您可能已经观察到每次加载时都会动态生成并将 CSRF 令牌 注入到表单中。 以下是该机制如何确保请求的安全性:
CSRF 令牌组件:
- IP: 捕获用户当前的 IP 地址。
- Country: 识别用户的当前位置。
- UserAgent: 记录用户的浏览器详细信息。
- expiry: 通过在当前时间上添加一分钟来设置计时器。
该数据以 JSON 格式组装:{i, c, u, e}
,随后被加密并转换为十六进制:
1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
关于加密功能:
- 使用用户定义的密码对数据进行加密,生成密码密钥,然后从中导出 AES 密钥。
- 该 AES 密钥使用即时生成的盐和 IV(初始化向量)对数据进行加密。
- 加密输出是一个ArrayBuffer,封装了迭代次数、salt、IV 和加密数据。
简而言之,此加密过程遵循行业标准实践,使加密数据既不可破译又可防止篡改。
验证 CSRF 令牌:
1const unhex = Utils.HexToBytes(formData.csrf);
2const decrypted = new TextDecoder().decode(await decryptData(unhex, env.CSRF_SECRET));
3const parsed = JSON.parse(decrypted);
4
5if (!Utils.isCRSFData(parsed))
6{
7 return new Response(JSON.stringify({
8 result: null,
9 success: false,
10 // An ambigous message; don't tell the hacker what's missing.
11 error: { code: 1001, message: "CSRF 令牌无效。 请刷新页面并重试。" }
12 }),
13 {
14 status: 403,
15 headers: { 'content-type': 'application/json;charset=UTF-8'
16 }
17 });
18}
19
20const IP = request.headers.get('CF-Connecting-IP');
21const Country = request.headers.get('CF-IPCountry') || '';
22const UserAgent = request.headers.get('User-Agent');
23
24if(IP !== parsed.i || Country !== parsed.c || UserAgent !== parsed.u || Date.now() > parsed.e)
25{
26 return new Response(JSON.stringify({
27 result: null,
28 success: false,
29 // An ambigous message; don't tell the hacker what's missing.
30 error: { code: 1002, message: "CSRF 令牌无效。 请刷新页面并重试。" }
31 }),
32 {
33 status: 403,
34 headers: { 'content-type': 'application/json;charset=UTF-8'
35 }
36 });
37}
验证过程严格检查 CSRF 令牌,确保其:
- 源自同一 IP 地址。
- 从同一国家发货。
- 从同一浏览器发送。
- 在有效时间内提交。
- 如果这些检查中的任何一个失败,系统会将 CSRF 令牌识别为无效,从而提供针对潜在威胁的强大保护。
如果这些检查中的任何一个失败,系统都会将 CSRF 令牌识别为无效,从而提供针对潜在威胁的强大保护。
密码散列/加密
对于密码散列/加密,我采用了 PBKDF2 (基于密码的密钥派生函数 2)与 [SHA-256](https://en.wikipedia.org/wiki/SHA-2 )哈希算法。
该方法为每个密码使用独特的、加密安全的伪随机“盐”,以确保增强的安全性。 这种方法可以防止彩虹表和暴力攻击。
存储密码的散列版本,而不是明文,进一步增强数据完整性。
为什么不使用 JWT 令牌?
JWT(JSON Web 令牌)是一种流行的方法,用于以紧凑、URL 安全的方式处理用户身份验证并在各方之间传输信息。
虽然它们有很多优点,但人们可能有理由选择不在特定环境中使用它们,例如完整的用户注册系统。 下面是更深入的了解:
- 无状态和撤销:JWT 的标志之一是其无状态。 然而,这种性质可能会给用户管理系统带来问题。 例如,如果用户的 JWT 令牌被盗或泄露,则没有直接的方法可以撤销该令牌,除非您维护令牌黑名单,这违背了无状态的目的。
- 大小:当您向 JWT 添加更多数据时,它的大小会增加。 如果您有一个全面的用户注册系统,您可能需要在令牌中存储更多与用户相关的数据,这可能会导致更大的 HTTP 标头和增加的延迟。
- 存储安全:对于JWT的客户端存储,常见的存储位置包括本地存储或cookie。 本地存储很容易受到 XSS 攻击,虽然 cookie 可以在更大程度上得到保护,但如果处理不当,它们会为 CSRF 攻击开辟途径。
- 过期处理:管理 JWT 的过期可能很复杂。 虽然短期令牌可以降低风险,但它们需要刷新机制,这会给身份验证流程带来更多复杂性。
- 没有内置撤销:如前所述,如果令牌被泄露,则没有固有的机制可以撤销或使其失效,直到其过期。 如果令牌的使用寿命很长,这可能会带来重大的安全风险。
- 开发人员的复杂性:对于那些不熟悉 JWT 的人来说,了解如何正确生成、验证和使用它们需要一个学习曲线。 如果不彻底理解和实施,这种复杂性可能会带来错误和漏洞。
在我对各种系统及其对用户数据的处理的探索中,我发现一些公司在其 JWT 令牌中存储了大量信息,包括用户角色等敏感细节。
需要明确的是,我的目的并不是批评或诽谤其他方法。 然而,根据我对安全的专业立场,将此类关键信息直接嵌入代币中会引起担忧。 暴露过多的数据存在固有风险,特别是当这些数据可能被利用来推断用户权限或其他功能时。
考虑到这些保留,我有意识地决定放弃这种方法。 相反,我倾向于将经过验证的服务器端身份验证与安全 cookie 处理相结合。 在我看来,这种方法在用户体验和强大的安全性之间取得了平衡,确保敏感数据受到保护,并且系统对潜在漏洞保持弹性。
可靠地发送电子邮件
我选择了带有动态模板的 SendGrid,因为它被广泛采用并且我对其细微差别非常熟悉。 要更深入地了解,请参阅他们的 API 文档 。
虽然它的集成可能会带来一些复杂性,但这都是有益挑战的一部分。 这是一个供您参考的示例:
1const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send';
2const SENDGRID_API_KEY = 'YOUR_SENDGRID_API_KEY'; // 替换为您的实际 API 密钥
3
4interface EmailDetails {
5 to: string;
6 from: string;
7 subject: string;
8 content: string;
9}
10
11async function sendEmail(details: EmailDetails): Promise<void> {
12 const data = {
13 personalizations: [{
14 to: [{
15 email: details.to
16 }]
17 }],
18 from: {
19 email: details.from
20 },
21 subject: details.subject,
22 content: [{
23 type: 'text/plain',
24 value: details.content
25 }]
26 };
27
28 try {
29 const response = await fetch(SENDGRID_API_URL, {
30 method: 'POST',
31 headers: {
32 'Authorization': `Bearer ${SENDGRID_API_KEY}`,
33 'Content-Type': 'application/json'
34 },
35 body: JSON.stringify(data)
36 });
37
38 if (response.status === 202) {
39 console.log('邮件发送成功!');
40 } else {
41 const responseBody = await response.json();
42 console.error('发送邮件失败:', responseBody.errors);
43 }
44 } catch (error) {
45 console.error('发送电子邮件时出错:', error);
46 }
47}
48
49// 使用示例:
50sendEmail({
51 to: '[email protected]',
52 from: '[email protected]',
53 subject: 'Hello from SendGrid!',
54 content: '这是使用 SendGrid API 发送的测试电子邮件。'
55});
如何进一步提高安全性?
尽管 Cloudflare Workers KV 会对存储的数据进行加密,但您可以为用户数据添加额外的加密层。
或者想为潜在的攻击者添加一些变化? 考虑实施虚拟表单输入来迷惑机器人和黑客:
1return new HTMLRewriter()
2 .on("form",
3 {
4 element(form)
5 {
6 // 虚拟输入...只是为了让黑客更加头疼并使他困惑:)
7 const randomNumber = Utils.RandomNumber(1, 5);
8
9 for (let i = 0; i < randomNumber; i++)
10 {
11 form.append(
12 `<input type="hidden" name="${Utils.RandomString(Utils.RandomNumber(i + 5, i + 25))}" value="${Utils.RandomString(Utils.RandomNumber(i + 25, i + 256))}" />`,
13 { html: true }
14 );
15 }
16 },
17 })
18.transform(response);
此代码片段在每次页面加载时动态生成 1 到 5 个随机命名的输入。
功能几乎是无限的,你只需要发挥你的想象力。
包起来
我们终于得到它了! 我真诚地希望这篇文章对您来说既富有洞察力又有吸引力。 无论您是经验丰富的开发人员还是刚刚进入可扩展云网站的世界,共享知识对于我们社区的发展都至关重要。
如果您发现本文有价值,请考虑将其转发给其他开发人员或对该主题感兴趣的人。 每一次分享都会拓宽我们的集体理解。
非常感谢您的反馈和问题。 请随时发表评论或联系我们——让我们继续对话。 安全编码!
评论