该帖子系列已在 NgateSystems.com 上建立索引。您还可以在那里找到超级有用的关键字搜索工具。
最后评论:24 年 11 月
Post 3.3 带来了一些坏消息 - 用于客户端提供有关登录用户信息的 Firestore 身份验证对象在服务器端不可用。这会产生以下后果:
服务器端数据库代码必须使用 Firestore Admin API。这是因为 Firestore Client API 代码使调用遵循数据库“规则”,当身份验证不可用时,引用身份验证会失败。相比之下,管理 API 调用并不关心数据库规则。如果您放弃了规则,客户端 API 调用将在服务器端工作,但这将使您的数据库容易受到网络攻击(自从您开始使用本地 VSCode 终端以来,您一直在使用实时 Firestore 数据库 - 想想看) .
使用从 auth 派生的 userName 和 userEmail 等数据项的服务器端代码必须找到另一种方式来获取此信息。
这篇文章描述了如何克服这些问题来生成一个在服务器端安全、高效运行的高性能 Web 应用程序。
如果您已经习惯了客户端调用签名,那么切换到 Firestore Admin API 的要求会很麻烦。但你很快就会习惯这一点,所以它不会对你造成很大的阻碍。
然而,获取用户数据是另一回事。对于许多应用程序来说,访问用户属性(例如 uId)对其设计至关重要。例如,网络应用程序可能需要确保用户只能看到他们的自己的数据。不幸的是,安排这个是相当困难的。我们开始吧:
看看 <script> 中的以下代码: products-maintenance-sv/ page.svelte “规则友好”products-maintenance-rf 代码的“服务器”版本的部分。这使用 getIdToken() 访问用户的 Firebase 身份验证会话并构建 idToken<br> </script>
// src/routes/products-maintenance-sv/+page.svelte <script> import { auth } from "$lib/utilities/firebase-client"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; onMount(async () => { if (!auth.currentUser) { // Redirect to login if not authenticated, with a redirect parameter goto("/login?redirect=/products-maintenance-sv"); return; } try { // Fetch the ID token directly const idToken = await auth.currentUser.getIdToken(); window.alert("idToken:" + JSON.stringify(idToken)); } catch (error) { window.alert("Error retrieving ID token:", error); } }); </script>
您之前在 products-maintenance-rf/ page.svelte 中看到了 onMount() 安排,它用于确保用户登录。现在它还用于通过调用异步身份验证来获取 idToken 变量。 currentUser.getIdToken()。
创建一个新的 src/routes/products-maintenance-sv 文件夹,并将上面列出的代码粘贴到其中的新 page.svelte 文件中。 现在尝试在开发服务器中运行它:http://localhost:5173/products-maintenance-sv。登录后(使用最后在 [Post 3.4](https://ngatelive.nw.r.appspot.com/redirect?post=3.4 中看到的 /login/ page.svelte 版本),您应该会看到显示的 idToken在警报消息中。
Firebase ID 令牌是 JSON Web 令牌 (JWT)。 JSON 位意味着它是一个使用“Javascript 对象表示法”编码为字符串的对象(如果这是您第一次看到“JSON”,您可能会发现向 chatGPT 询问背景信息很有用)。 JSON 广泛用于需要将 Javascript 对象作为字符串传递的地方。 JWT JSON 包含您可能需要了解的有关用户的所有信息。我将在本文后面向您展示如何提取这些信息 - 这并不复杂。
本文中描述的机制将“IdToken”作为服务器请求附带的“请求标头”中的“cookie”发送。 “http header”是当基于客户端的 page.svelte 文件向基于服务器的 page.server.js 文件发送请求时通过 Web 传递的信息包。每次您读取或写入 Firestore 文档时都会发送此类请求。 “cookie”是一个添加到每个请求标头中的字符串。
这种安排很复杂,但被认为是安全的。 从您的角度来看,作为一名 IT 学生,它也很有趣且具有教育意义,因为它可以深入了解网页设计的内部结构。
客户端 Javascript 程序可以轻松设置包含 JWT 的 “常规” cookie,但出于安全原因,您非常不希望这样做。如果你能做到这一点,那么任何人都可以。另一方面,服务器端 page.server.js 文件可以使用 set-cookie 调用在客户端浏览器中设置 "http-only" cookie。这是一个例子:
// src/routes/products-maintenance-sv/+page.svelte <script> import { auth } from "$lib/utilities/firebase-client"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; onMount(async () => { if (!auth.currentUser) { // Redirect to login if not authenticated, with a redirect parameter goto("/login?redirect=/products-maintenance-sv"); return; } try { // Fetch the ID token directly const idToken = await auth.currentUser.getIdToken(); window.alert("idToken:" + JSON.stringify(idToken)); } catch (error) { window.alert("Error retrieving ID token:", error); } }); </script>
上面的 httpOnly: true 设置意味着,尽管 cookie 保存在客户端,但它无法从 Javascript 访问。 这样您就可以确保您在此处设置的值不会被篡改。
您现在应该问的问题是“当服务器端 page.server.js 文件不知道 idToken 时,如何启动 Set-Cookie 命令来设置 idToken?” 。
欢迎使用 Svelte server.js 文件。这是服务器端代码,可以使用 Javascript fetch 命令从客户端代码调用。这样的服务器端代码称为“端点”。获取命令是 JavaScript 的本机方法,用于向基于 Web 的“端点”提交请求。该命令使您能够在请求中包含数据,因此这就是您在服务器上获取 idToken 值的方式。这是一个例子:
// Set a secure, HTTP-only cookie with the `idToken` token const headers = { 'Set-Cookie': cookie.serialize('idToken', idToken, { httpOnly: true }) }; let response = new Response('Set cookie from server', { status: 200, headers, body: { message: 'Cookie set successfully' } // Optional message }); return response;
以下是接收者 server.js 文件如何检索此内容并提取其 idToken。
// client-side +page.svelte code const idToken = await user.getIdToken(); // Send token to the server to set the cookie fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idToken }), });
您可能在想“为什么这段代码使用“fetch”命令来“发送”某些东西?”但你就在那里。 “Fetch”被设计为一个多功能 API,用于发出许多不同类型的 HTTP 请求。如果您想了解一些背景知识,请向 chatGPT 索取教程。请参阅一些示例。
现在的建议是让您的登录页面负责进行设置浏览器仅http cookie的server.js调用。设置 cookie 后,它将自动添加到浏览器发出的每个 HTTP 调用中,直到过期。
要启动此操作,请为登录页面的以下 login-and-set-cookie/ page.svelte 版本及其随附的 api/set-cookie/ server.js 端点创建新文件夹和文件:
// server-side +server.js code export async function POST({ request }) { const { idToken } = await request.json(); }
请注意,为了使用 api/set-cookie/ server.js,您首先需要安装 npm "cookie" 库。该库有助于创建格式正确的 cookie,以便包含在 HTTP 响应标头中。
// src/routes/login-and-set-cookie/+page.svelte <script> import { onMount } from "svelte"; import { auth, app } from "$lib/utilities/firebase-client"; import { goto } from "$app/navigation"; // SvelteKit's navigation for redirection import { signInWithEmailAndPassword } from "firebase/auth"; let redirect; let email = ""; let password = ""; onMount(() => { // Parse the redirectTo parameter from the current URL const urlParams = new URLSearchParams(window.location.search); redirect = urlParams.get("redirect") || "/"; }); // this code will run after a successful login. auth.onAuthStateChanged(async (user) => { if (user) { const idToken = await user.getIdToken(); console.log("In login_awith-cookie : idToken: ", idToken); // Send token to the server to set the cookie fetch("/api/set-cookie", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idToken }), }); window.alert("In with-cookie : cookie set"); goto(redirect); } }); async function loginWithMail() { try { const result = await signInWithEmailAndPassword( auth, email, password, ); } catch (error) { window.alert("login with Mail failed" + error); } } </script> <div> <pre class="brush:php;toolbar:false">// src/routes/api/set-cookie/+server.js import admin from 'firebase-admin'; import cookie from 'cookie'; export async function POST({ request }) { const { idToken } = await request.json(); try { // Verify the token with Firebase Admin SDK const decodedToken = await admin.auth().verifyIdToken(idToken); // Use the cookie.serialize method to create a 'Set-Cookie' header for inclusion in the POST // response. This will instruct the browser to create a cookie called 'idToken' with the value of idToken // that will be incorporated in all subsequent browser communication requests to pages on this domain. const headers = { 'Set-Cookie': cookie.serialize('idToken', idToken, { httpOnly: true, // Ensures the cookie is only accessible by the web server secure: true, // Ensures the cookie is only sent over HTTPS sameSite: 'None', // Allows the cookie to be sent in cross-site requests maxAge: 60 * 60, // 1 hour (same as Firebase ID token expiry) path: '/' // Ensures the cookie is sent with every request, regardless of the path. }) }; let response = new Response('Set cookie from login', { status: 200, headers, body: { message: 'Cookie set successfully' } // Optional message }); console.log("Cookie set") return response; } catch (err) { console.error("Error in login server function: ", err); let response = new Response('Set cookie from login', { status: 401, body: { message: 'Unauthorized' } // Optional message }); return response; } };
不需要“注销并删除 cookie”注销页面。设置新的 cookie 将覆盖任何同名的旧版本。
项目的服务帐户是一个包含安全密钥和“所有者”信息(例如项目的projectId)的对象。当“ page.server.js”文件运行时,嵌入其中的服务帐户的副本将呈现给Google。如果两者匹配,则服务器文件通过验证。
以下程序:
现在单击“创建并继续”按钮,然后转到“授予此服务帐户访问项目的权限”部分。首先打开字段上的下拉菜单。这有点复杂,因为它有两个面板。左侧面板(有一个滑块)允许您选择产品或服务。右侧列出了该服务可用的角色。使用左侧面板选择“Firebase”服务,然后从右侧面板中选择“Admin SDK 管理员服务代理”角色。单击“继续”,然后单击“完成”返回服务帐户屏幕
最后,单击您刚刚创建的“Firebase Admin SDK Service Agent”密钥条目右侧的“三点”菜单,然后选择“管理密钥”。点击“添加密钥”>创建新密钥> JSON>创建并注意一个新文件已出现在您的“下载”文件夹中。这是您的“服务帐户密钥”。您现在要做的就是将其嵌入到您的项目中。
// src/routes/products-maintenance-sv/+page.svelte <script> import { auth } from "$lib/utilities/firebase-client"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; onMount(async () => { if (!auth.currentUser) { // Redirect to login if not authenticated, with a redirect parameter goto("/login?redirect=/products-maintenance-sv"); return; } try { // Fetch the ID token directly const idToken = await auth.currentUser.getIdToken(); window.alert("idToken:" + JSON.stringify(idToken)); } catch (error) { window.alert("Error retrieving ID token:", error); } }); </script>
这是前面在 3.3 篇文章中描述的保护机制的另一个实例,用于阻止您无意中泄露 Git 存储库中的文件。对于 Windows 用户,更安全的方法是创建 Windows GOOGLE_APPLICATION_CREDENTIAL 环境变量来提供关键引用。
要运行“服务器登录”过程,您的 page.server.js 代码需要访问 Firebase 管理 API。您可以通过在项目中安装“firebase-admin”来获得此信息:
// Set a secure, HTTP-only cookie with the `idToken` token const headers = { 'Set-Cookie': cookie.serialize('idToken', idToken, { httpOnly: true }) }; let response = new Response('Set cookie from server', { status: 200, headers, body: { message: 'Cookie set successfully' } // Optional message }); return response;
您现在可以使用以下命令在代码中创建管理员引用:
// client-side +page.svelte code const idToken = await user.getIdToken(); // Send token to the server to set the cookie fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ idToken }), });
请注意,此导入的语法与您迄今为止使用的语法不同 - “admin”位周围没有大括号。虽然您使用的其他库允许您导入命名组件,但此版本要求您从 default 管理导出导入整个内容。这提供了组件作为 properties,例如父管理对象的 admin.auth()、admin.firestore() 等。该图书馆的设计者认为,在这种情况下这是一个更实用的安排。
使用默认导入时,您可以将导入的父对象称为您喜欢的任何名称(例如,您可以将其称为 myFirebaseAdmin 而不是 admin)。将此安排与您之前创建的 lib/utilities/firebase-config 文件的命名导出方法进行比较
这是您最终了解使用 Firestore 管理 API 访问 Firestore 数据库服务器端的实质内容的地方。
首先,您使用服务帐户密钥“初始化”您的应用程序,从而获得创建使用管理 API 所需的 adminDb 对象的权限(就像您需要用于客户端 API 的 db 一样)。然后,您需要从 cookie 中获取 idToken,并从中提取您在 Firestore 调用中可能需要的任何用户内容。此时,您终于可以使用 Firestore 管理 API 来编写这些调用的代码了。
将下面列出的代码复制到 src/routes/products-maintenance-sv 文件夹中的新 page.server.js 文件中。这是产品维护代码的“服务器版本”,首次出现在 Post 3.3 中。它用于展示尝试使用 Firestore 客户端 API 的服务器端代码在其所寻址的集合受 Firestore 数据库规则约束的情况下如何失败。这个新版本的好处是:
// src/routes/products-maintenance-sv/+page.svelte <script> import { auth } from "$lib/utilities/firebase-client"; import { onMount } from "svelte"; import { goto } from "$app/navigation"; onMount(async () => { if (!auth.currentUser) { // Redirect to login if not authenticated, with a redirect parameter goto("/login?redirect=/products-maintenance-sv"); return; } try { // Fetch the ID token directly const idToken = await auth.currentUser.getIdToken(); window.alert("idToken:" + JSON.stringify(idToken)); } catch (error) { window.alert("Error retrieving ID token:", error); } }); </script>
注意代码构建 userEmail 字段的奇怪方式
// Set a secure, HTTP-only cookie with the `idToken` token const headers = { 'Set-Cookie': cookie.serialize('idToken', idToken, { httpOnly: true }) }; let response = new Response('Set cookie from server', { status: 200, headers, body: { message: 'Cookie set successfully' } // Optional message }); return response;
verifyIdToken 方法名称可能会让您怀疑这是否正在尝试再次验证您的用户。别担心——事实并非如此。它只是对令牌的嵌入“签名”进行安全检查,以确保它没有被篡改并且没有过期。
verifyIdToken创建的decodedToken是一个简单的对象,包含经过身份验证的用户的电子邮件和用户名属性等。后续的 Firestore 代码没有使用其中任何一个,但我相信您可以轻松想象它是如何做到这一点的。
我建议您在编写管理 API 调用时再次使用“样板”方法 - 如果需要,请使用 chatGPT 转换 10.1 篇文章中记录的客户端代码。
现在用下面所示的代码替换您之前创建的 src/routes/products-maintenance-sv/ page.svelte 文件的内容。这将为 products-maintenance-sv/ page.server.js 文件提供客户端前端:
// src/routes/products-maintenance-sv/ page.svelte 从“svelte”导入{onMount}; 从“$lib/utilities/firebase-client”导入{auth}; 从“$app/navigation”导入{goto}; 从“$lib/utilities/productNumberIsNumeric”导入{productNumberIsNumeric}; // 将对身份验证状态更改的检查放在 onMount 内的 onAuthStateChanged 回调中。这似乎 // 奇怪,但似乎是完成服务器端活动并使 auth.currentUser 进入稳定状态的唯一方法 // 状态。这在客户端“规则友好”版本上不是问题,但很快就成为问题 // 添加了带有 actions() 的 page.server.js 文件。 onMount(异步() => { auth.onAuthStateChanged(异步(用户)=> { if (!auth.currentUser) { // 如果未通过身份验证,则重定向到登录。该参数告诉登录如何返回这里 goto(“/login-and-set-cookie?redirect=/products-maintenance-sv”); } }); }); 让产品编号; 让产品详细信息; 让产品编号类=“产品编号”; 让submitButtonClass =“submitButton”; 出口许可证表格; </脚本> 产品编号 <p>要在您的开发服务器中运行此程序,请首先使用 http://localhost:5173/logout 注销。然后运行http://localhost:5173/products-maintenance-sv。这将邀请您登录登录并设置 cookie 页面。 </p> <p>成功登录后,您将看到熟悉的表单,邀请您创建新产品。 </p> <p>此时,登录并设置 cookie 页面应该已在您的浏览器中安全地设置了 idToken cookie。当您输入数据并提交表单时,控制权将传递到 products-maintenance-sv/page.server.js 中的服务器端代码。它通过呈现项目中内置的服务代码来进行自身身份验证,然后从 Sveltekit 请求中的表单对象中获取标头中的 idToken 及其输入数据。该代码不会对 idToken 中可用的用户数据执行任何有用的操作,但会在 VSCode 终端中显示一条显示 userEmail 值的日志消息。最后,Firestore 管理代码会将新产品添加到产品数据库集合中。</p> <p>您可以通过运行旧的 http://localhost:5173/products-display-rf 页面来确认更新已成功应用。</p> <p>请注意,提交表单后,它会显示一条确认消息并清除其输入字段。 “表单刷新”是表单提交后 Javascript 的默认操作。 </p><p>您可能想知道当 http://localhost:5173/products-display-rf 页面仍在运行 Firestore <strong>客户端</strong> API 代码服务器端并在产品集合上设置 Firestore 身份验证规则时,它是如何工作的。不同之处在于这些规则仅适用于写入。 products-display-rfcode 只是读取文档。 </p> <p>在实践中,我认为如果您担心避免混淆并决定创建 products-display-sv 的 products-display-sv 版本,您会希望在整个过程中使用 Firestore <strong>Admin</strong> API 调用。但请记住,您需要首先提供您的服务帐户凭据来初始化应用程序。</p> <h3> 三、总结 </h3> <p>这是一篇很长的文章,将会使您的 Javascript 达到极限。如果此时你仍然和我在一起——干得好。真的,干得好——你表现出了非凡的毅力! </p> <p>上一篇文章介绍的“客户端”技术使用起来很愉快,但我希望您会欣赏服务器端安排的安全性和速度优势。凭借经验,服务器端代码开发将变得像客户端工作一样简单自然。</p> <p>但是还有一件事要做。尽管您现在已经开发了一个包含许多页面的 Web 应用程序,这些页面在您的开发服务器中运行得很好,但这些页面在 Web 上尚不可见。</p> <p>下一篇文章将告诉您如何“构建”和部署”您的 Web 应用程序到 Google AppEngine 上,从而将其发布给热切的公众。这将是一个重要的时刻!</p> <p>我希望您仍有精力继续阅读并找出您需要做什么。不是太难。</p> <h3> 后记:当出现问题时 - 在检查器中查看标题 </h3> <p>实际上,在本节中可能出现问题的地方可能接近无限。尽量不要过于恐慌,并密切关注项目的文件结构。很容易将正确的代码放入错误的文件中,或者将正确的文件放入错误的文件夹中。此外,您可能会发现通过终止并重新启动终端会话定期“清理地面”很有帮助。至少,这可以让您在寻找错误序列的最初原因时获得一张干净的纸。</p> <p>但是,由于您现在正在使用标头和 cookie,您还会发现了解浏览器的检查器工具可以让您直观地了解这些内容很有用。检查器可以<strong>向您显示</strong>嵌入在页面请求标头中的cookie。</p><p>要查看此功能的运行情况,首先请确保您已使用 https://myLiveUrl/logout 在实时系统上注销(其中 myLiveUrl 是您部署的 Web 应用程序的地址)。然后运行 products-maintenance=sv 页面(网址为 https://https://myLiveUrl/products-maintenance-sv)。登录后,打开“输入新产品”表单上的检查器,然后单击“网络”选项卡。现在显示页面发出的网络请求的列表。</p> <p>现在使用网络应用程序插入新产品并注意“请求”列表是如何刷新的。这些是执行简单更新所需的网络请求 - 一个令人惊讶的长列表!向上滚动到此列表的顶部,您应该会找到 products-maintenance-sv 页面的条目。如果单击此按钮,请求列表右侧的面板应显示事务的响应和请求标头的完整详细信息。下面的屏幕截图显示了嵌入在请求标头中的 cookie。</p> <p><img src="https://img.php.cn/upload/article/000/000/000/173301818045981.jpg" alt="NgSysV.A Serious Svelte InfoSys: A Client-Server Version"></p>
以上是NgSysV.A Serious Svelte InfoSys:客户端-服务器版本的详细内容。更多信息请关注PHP中文网其他相关文章!