用户认证

  • 使用 Supabase Auth 和 Google 登录功能构建登录和注册流程,以控制谁可以访问您的应用
  • 在 Express.js 后端中添加身份验证中间件,以保护您的 API 路由免受未经授权的请求
  • 测试前端和后端的安全性,以确保只有已登录的用户才能触发您的 AI 功能
  • 用于创建用户账户并保持用户登录状态的注册和登录页面
  • 通过 后端中间件保护的 API 路由 ,该中间件会在处理请求前验证身份验证令牌

学生们已在 GitHub 仓库中构建了一个经过调试的最小可行产品(MVP),其中包含 React 前端和 Express.js 后端。在前端和后端可能作为两个独立进程在 Codespaces 中运行。

通过身份验证保障应用程序的安全

目前,任何拥有您的 Codespace 链接的人都可以使用您的应用。系统没有登录界面,您的 Express.js 后端会无条件处理收到的每个请求。如果有人发现了您的 URL,他们就可以触发 AI 调用,这会给您带来费用。正如您在制定财务计划时所了解的那样,每次 API 调用都会产生费用。保护这些调用不仅是一个技术决策,更是一个商业决策。

在本单元中,我们将以TutorMatch作为指导示例。你将按照 TutorMatch 的示例操作,然后针对自己的最小可行产品(MVP)调整每个步骤。

该设备增加了两层保护。

  1. 用户身份验证——设置登录界面,确保只有注册用户才能查看您的应用。 
  2. 安全检查 在您的后端中,确保对每个 API 请求进行验证,以确认其确实来自已登录的用户。

不妨这样理解:登录页面就像是保护“大门”的屏障,后端安全检查则像保护“窗户”的屏障。二者结合,意味着您的应用程序从两方面都得到了保护。

为什么不自己保存密码呢?

处理用户账户最直观的方法也是最危险的:创建一个用户表,在用户注册时存储其电子邮件和密码,并在用户登录时验证密码。

问题在于,这种方式会将密码以明文形式存储,任何访问您数据库的人都能读取。一旦您的数据库遭到泄露,所有用户的密码都会面临泄露风险。专业级应用绝不会这样做。

 安全的身份验证需要以下几个方面协同工作:

  • 密码哈希—— 在存储前将密码转换为不可读的字符串
  • 会话令牌—— 一种临时标识符,可让用户保持登录状态,而无需在每个页面都输入密码
  • 安全存储——对凭证进行加密处理,确保其在传输或静止状态下均不会泄露。

从头开始构建所有这些内容既复杂又容易出错。这就是身份验证服务存在的意义——它们负责处理安全问题,让你能够专注于自己的应用。

user authentication login screen

在选择用户身份验证服务时,有以下几种选项:

选项

工作原理

免费层级

复杂性

结论

自行保存密码

创建用户表,对密码进行哈希处理,手动管理会话

免费

高——安全失误很容易发生,且后果严重

不推荐用于MVP

Google 登录(OAuth)

用户点击“使用 Google 登录”。所有安全事宜均由 Google 负责。

免费

中等 — 需要配置 Google Cloud Console

这是一个不错的选择,但需要多几个设置步骤

Auth0 / 文员

带有预置登录页面的第三方身份验证服务

提供免费套餐

Medium — 需要注册新账号并安装 SDK

不错,但需要管理一项额外服务

Supabase 身份验证

内置于 Supabase 中。自动处理哈希、会话和存储。

免费(已包含)

Low —— 您已有 Supabase 账户

MVP 的绝佳选择

在本节课中,我们将使用 Supabase Auth 进行用户身份验证。此外,我们还将添加 Google 登录功能,因为这是专业应用的标准登录选项,且配置起来并不太难。选择 Supabase Auth 主要有以下三个原因:

  1. 您已经通过“创始人工具包”拥有了一个 Supabase 账户。
  2. Supabase 开箱即用,即可处理上述所有复杂的安全问题。
  3. 免费套餐涵盖了您开发最小可行产品(MVP)所需的一切。

还有一点需要考虑:Supabase Auth 支持行级安全(RLS), 即您可配置的数据库 规则, 用于过滤数据,确保每位用户只能查看自己的记录。我们在将 Supabase 添加到“创始人工具包”时曾简要提及过这一点。不过,RLS 无法保护您的 AI 路由。因此,我们需要添加进一步的保护措施。

“现阶段无需使用 RLS,因为我们没有创建任何自定义表。Supabase 会在内部处理用户数据的存储。如果你在最小可行产品(MVP)中存储用户特定数据——例如保存用户的历史记录、偏好设置或交易记录——那么 RLS 就会派上用场。”

Supabase 身份验证

创建 Supabase 项目时,身份验证会自动启用。您无需在 Supabase 设置中进行任何特殊配置。虽然我们在“创始人工具包”中针对匿名用户和团队成员分别设置了一些具体的 RLS 安全策略,但对于此数据库,我们将使用 Supabase 的默认内置身份验证功能。

Supabase 会自动配置以下内容:

  •  已启用电子邮件/密码认证
  • 已创建用户表(auth.users)
  • 已签发的 JSON Web Tokens (JWT)
  • 会话管理已处理

在第一个练习中,你将为你的MVP添加登录和注册流程,以便只有注册用户才能访问你的应用。我们将按照一系列提示逐步操作,并在每个步骤后进行测试,以确保一切正常运行后再继续下一步。 

本课活动中的每个提示都使用了 “角色 + 任务” 结构。这是一种值得练习的提示设计模式:

  • 角色 告知AI应成为何种专家
  • 任务 明确告诉它该做什么。

将这两者分开,可以使您的提示更清晰,并获得更好的结果,特别是在使用较小的AI模型时。

活动 1

使用 Supabase 添加用户身份验证

预计时间:30分钟

您将更新应用,使其在使用时需要登录。新用户可以注册,老用户可以登录,而没有账户的用户将被重定向至登录页面。

  1. 前往 GitHub,打开你的 MVP 代码库。 
  2. 创建一个名为“add-auth”之类的新分支。 
  3. 在新分支上打开您的 Codespace。

你即将进行重大更改——功能分支能确保主分支的安全。只有在测试完成后,你才会将代码合并到主分支。

  1. 访问 Supabase。您应该已经通过“创始人工具包”拥有了一个账户和一个项目。
  2. 创建一个新项目。为该项目起一个适合您MVP的名称。
  3. 您需要设置一个数据库密码。请设置一个密码,并将其保存在安全的地方,因为保存后您将无法查看该密码。
  4. 配置身份验证设置:
    1. 进入您的Supabase 仪表盘 → 身份验证 → 设置
    2. 向下滚动至“登录/服务提供商”部分
    3. 查找“启用电子邮件确认”
    4. 将其切换为“关闭”(用于测试)
    5. 投入生产环境时,请记得将其重新启用!
supabase user authentications screen
  1. 获取您的项目 URL 和 API 密钥
    1. 在左侧边栏中,点击“项目设置”(底部的小齿轮图标)
    2. 点击“数据 API”。
    3. 您将看到API URL。请将其复制并保存在电脑上的某个位置。您在开发应用时会用到它。
    4. 返回“项目设置”菜单,然后点击“API密钥”。
    5. 你会看到两个密钥:“可发布密钥”和“私钥”。
    6. 复制可发布密钥。您的应用也需要用到它。

公共 URL 和可发布密钥均为公开状态,可在您的应用前端使用。由于 Supabase 采用了 RLS(行级安全),数据得到了有效保护。用户可以注册和登录,但只能查看自己的数据,且不具备管理员权限。

这里我们不会使用密钥。该密钥绝不能在前端使用。我们稍后在处理支付时会用到它。

  1. 在您的项目中,添加一个新的 .env 文件,将其放置在 根目录中添加一个新的.env文件。你应该已经有一个 .env 文件 server 文件夹中,但由于 Supabase 的集成是在前端进行的,因此该文件必须位于项目的根目录下。
  2. 在根目录下添加 2 行代码 .env 文件中,添加以下两行:
    VITE_SUPABASE_URL=your_public_URL
    VITE_SUPABASE_KEY=your_publishable_key
  3. 请在该文件的相应行中填写您实际的 Supabase URL 和可发布密钥。

注意:我们使用的是 React,因此需要在前端环境变量前添加前缀 VITE_。您的框架可能有所不同。

在您的 Codespace 终端中,输入:
npm install @supabase/supabase-js

提示 Copilot(或其他大型语言模型)协助生成您应用的登录/注册代码。以下是一个示例提示。

角色:你是一名网页开发人员,正在帮助我将用户身份验证功能集成到我的 React 应用中。

任务:添加一个 Supabase 注册和登录表单,并与 Supabase 网址进行交互。初始界面为登录页面,用户登录后,其他标签页将显示出来,用户可访问这两个标签页。登录表单应作为 Authform.jsx 文件添加到 Pages 文件夹中。 在用户登录后,需在屏幕上其他标签页按钮旁边添加一个注销按钮,以便用户注销并返回主界面。

请为“AuthForm”采用与“FindTutor”和“GetAdvice”选项卡相同的格式设置。 

我已将 Supabase 项目的 URL 和可发布的 API 密钥作为 VITE_SUPABASE_URL 和 VITE_SUPABASE_URL 添加到我的 .env 文件中。 

测试整个流程:

  1. 注册:
    • 访问 /signup
    • 请输入电子邮箱和密码(至少6个字符)
    • 点击“注册”
    • 应重定向至首页
  2. 在 Supabase 中验证:
    • 打开 Supabase 仪表盘 → 身份验证 → 用户
    • 您应该能看到新用户已列入列表
  3. 注销:
    • 点击“注销”按钮
    • 应重定向至登录页面
  4. 登录:
    • 请输入相同的电子邮箱和密码
    • 应重定向至首页
  5. 会话持久化:
    • 刷新页面
    • 应该仍然处于登录状态

如果任何一步操作失败,请使用 Copilot 的“教我”模式:描述你预期的结果、实际发生的情况,并请 Copilot 先进行解释,然后再提供解决方案。

请保持代码空间和分支处于打开状态,以便进行下一项活动。

虽然添加 Google 登录选项并非强制要求,但您可以免费实现这一功能,而且这能为您的应用增添一份专业感。

活动 2

(可选)添加 Google 登录功能

预计时间:30分钟

  1. 转到您的Supabase 仪表盘 → 身份验证 → 提供商。
  2. 在列表中找到“Google”,然后将其启用。 
  3. 找到“回调 URL”,并将其复制下来。该 URL 通常如下所示:https://[YOUR-PROJECT-ID].supabase.co/auth/v1/callback。

重要提示:请忽略 屏幕顶部醒目的“免费试用”或“激活 300 美元信用额度”横幅。如果您点击这些按钮,Google会要求您提供信用卡信息以进行身份验证。

您无需300美元的信用额度或“计费账户”即可使用身份/身份验证API。

  1. 访问Google Cloud 控制台。您 应该在使用 Google AI Studio 时已拥有一个账户。

  2. 创建一个新项目,并为其命名。 Google 允许每个账户创建多个免费项目,无需付费或使用信用卡。

  3. 转到“API 和服务”→“OAuth 同意页面”。

    • 点击“开始使用”。
    • 请填写您的应用名称和用户支持邮箱。
    • 点击 外部 面向观众。

      重要提示:请仅提供“应用名称”和“支持邮箱”。如果系统要求您添加徽标或敏感的“权限范围”(例如读取用户的整个云端硬盘),Google 可能会将其标记为需要通过信用卡进行验证。

      MVP 实用技巧:
      只要您保持在“测试”模式,或者用户数量较少,目前还无需提交审核。

  1. 转到“API 和服务”→“凭据”。
  2. 点击“+ 创建凭据”→“OAuth 客户端 ID”。
  3. 应用程序类型:选择“Web 应用程序”。
  4. 授权的重定向 URI:粘贴您从 Supabase 复制的回调 URL。
  5. 点击“创建”。您将收到一个客户端 ID和客户端密钥。
  6. 请务必复制这些内容,因为关闭窗口后将无法再访问它们。您可以将其下载为 JSON 格式,这样两种格式都能供您使用。
  1. 返回Supabase 仪表盘 → 身份验证 → 提供商 → Google。
  2. 请粘贴您的客户端 ID 和客户端密钥。
  3. 将“启用 Google 提供商”切换为“开”,然后点击“保存”。

当用户使用 Google 登录时,系统需要知道认证完成后应跳转到哪里。默认跳转地址为http://localhost:3000/,但我们的前端运行在http://localhost:5173 上。

  1. 为确保其正常运行,请在应用运行时复制 GitHub 链接(链接形式大致如下:https://codespace_name.app.github.dev/
  2. 在 Supabase 中,转到“身份验证”→“URL 配置”。 
  3. 将您的 GitHub 网址添加为重定向网址。
  1. 在您的 .env 文件中,作为变量VITE_DEVELOPMENT_URL(仅当您的应用是 React 时才需要 VITE)。

您将要求 Copilot 添加 Google 登录功能。请注意,我们正在添加包含重定向 URL 的操作说明。 

  1. 提示 Copilot 在您的登录界面中添加“Google 登录”作为选项。

角色:你是一名网页开发人员,正在帮助我将用户身份验证功能集成到我的 React 应用中。

任务:在我的登录页面中添加“Google 登录”选项。使用 Supabase 的 signInWithOAuth 方法,并指定“Google”提供商。重定向至环境变量 VITE_DEVELOPMENT_URL。

注意:如果您 切换代码空间,则需要创建一个包含重定向设置的新 .env 文件(同时还需包含 Supabase 的 URL 以及前端所需的 API 密钥)。

在部署应用时,您需要在 Supabase 中将生产环境的应用 URL 添加为重定向,同时在代码中也将该 URL 添加为重定向。

  1. 将更改提交到你的分支。

请保持该分支和代码空间处于打开状态,以便进行下一项活动。

保护您的 API 路由

您的应用现在已拥有登录页面,因此用户必须登录才能查看您的应用。但您的 Express.js 后端仍会处理收到的每个请求,即使用户并未通过前端登录也是如此。有人可能会打开浏览器控制台,直接调用您的 API,从而完全绕过登录页面。

这就是“窗口”问题。你虽然用登录页面锁上了“前门”,但“窗口”(即你的 API 路由)却仍然敞开着。要解决这个问题,你需要添加一个 安全检查 (名为 中间件),用于验证每个 API 请求是否确实来自已登录的用户。

您需要:

保护符合以下条件的路线:

  • 产生费用(AI 调用、API 使用)
  • 修改数据(创建、更新、删除)
  • 显示用户特定信息

保持公开:

  • 健康检查
  • 静态资源
  • 主页(如对公众开放)

当您的后端收到请求时:

  1. 检查:“此请求是否附有证明用户已登录的凭据?”
  2. 如果是→ 按常规流程处理该请求
  3. 如果不存在→ 返回“401 未授权”错误并拒绝该请求

该“证明”是一个 JWT(JSON Web Token),即 Supabase 为每位已登录用户生成的长字符串。您的前端在向后端发送的每次请求中都会包含此令牌。后端会通过 Supabase 验证该令牌,以确认其真实有效且未过期。

内容包括:

  • 用户是谁(其用户 ID、电子邮箱)
  • 发布时
  • 到期时
  • 一个加密签名,用于证明该签名由 Supabase 签发且未被篡改
活动 3

保护您的 API 路由

预计时间:25分钟

如果您尚未进入 Codespace,请打开它,并选择与活动 1 和 2 相同的分支。

  1. 在您的 服务器 文件夹(您的 Express 后端所在的位置)中,安装 Supabase:
    • 在终端窗口中,输入:
      ,然后输入:cd server,接着输入:
      ,最后输入:npm install @supabase/supabase-js
  1. 返回您的 Supabase 项目,并获取 秘密 API 密钥 (不是可发布密钥)。
    • 转到“项目设置”→“API密钥”
    • 向下滚动至“密钥”部分。
    • 将其复制并保存在电脑上的某个安全位置。
  2. 更新 .env 文件,位于您的 server 文件夹中更新.env文件(不是根目录下的 .env,而是 server/.env 是后端专用的独立文件)。
  3. 添加以下几行代码,并将您的 Supabase URL 和密钥粘贴进去:
    SUPABASE_URL=您的_公开_URL
    SUPABASE_SERVICE_ROLE_KEY=您的_密钥

重要提示:

  • 请使用与前端相同的SUPABASE_URL
  • 请将“SECRET KEY”(非公开密钥)用于SUPABASE_SERVICE_ROLE_KEY
  • 密钥具有管理员权限——切勿在前端代码中暴露该密钥
  1. 使用此提示语,让 Copilot 在后端代码中添加代码,以确保 API 调用安全。

角色:你是一名后端开发人员,负责协助我加强 Express.js API 的安全性。

任务: 

  1. 导入 Supabase 客户端库 (@supabase/supabase-js)
  2. 使用环境变量中的 SUPABASE_URL 和 SUPABASE_SERVICE_ROLE_KEY 初始化一个 Supabase 客户端。
  3. 在我的 Express.js 后端中添加一个身份验证中间件函数。
    • 提取令牌
    • 请求 Supabase 验证该令牌
    • 如果是 → 允许请求继续
    • 如果为空 → 返回 401 错误
  4. 仅将此中间件应用于调用外部 API 的路由

  5. 请勿将其应用于首页或健康检查等公共路径

  1. 查看生成的代码。

    你会看到一个名为requireAuth(或类似名称)的函数,该函数用于检查 Authorization 头,提取令牌,并通过 Supabase 进行验证。你还会发现该函数已应用于你的 API 路由,但未应用于公共路由。

    关键部分应与以下代码类似:
				
					async function requireAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const token = authHeader.split(' ')[1];
  const { data, error } = await supabase.auth.getUser(token);

  if (error || !data.user) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  req.user = data.user;
  next();
}

// Protected route — security check runs first
app.post('/api/ai-query', requireAuth, async (req, res) => {
  // This code only runs if the user is logged in
});

// Public route — no security check needed
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

				
			

对于 TutorMatch 而言,受保护的路由是app.post('/api/ai-query', requireAuth, …)。您的应用程序可能会使用不同的路由名称,但模式是一样的——对运行会产生成本的路由进行保护。

您的后端现在要求每次请求都提供登录凭证,但您的前端尚未发送该凭证。

  1. 提示 Copilot 更新您的前端代码。以下是一个示例提示。

角色:你是一名前端开发人员,负责帮助我将 React 应用与我的安全 Express.js 后端进行连接。

任务:将我所有的前端 API 调用更新为受保护的路由,并在 Authorization 头部中包含 Supabase 会话令牌。

  1. 请查看 Copilot 更新的代码。

    你会发现,你的fetch调用现在包含了 Authorization 头部。以下是修改后的代码:
				
					const { data: { session } } = await supabase.auth.getSession();

if (!session) {
  window.location.href = '/login';
  return;
}

const response = await fetch('/api/ai-query', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${session.access_token}`
  },
  body: JSON.stringify({ prompt: userInput })
});

				
			

这里就是一切的交汇点:

  1. 当用户登录时,Supabase Auth 会向前端返回一个令牌。
  2. 您的前端会在每次请求中发送该令牌。
  3. 您的后端会进行检查。
    • 如果请求有效,则该请求将被处理。
    • 如果不是,那就说明被屏蔽了。
  1. 在 Codespaces 中运行您的应用,并测试以下两种场景:

测试 1 — 已登录用户(应能正常工作):

    1. 打开您的应用,并使用测试凭据登录。
    2. 请照常使用您的AI功能。
    3. 它应该和以前一样正常工作。

如果这能正常工作,说明你的前端正在发送令牌,而你的后端正在接受该令牌。

测试 2 — 未登录(应被阻止):

    1. 从应用中注销,以便用户处于未登录状态。
    2. 打开开发者工具(按 F12 或右键单击“检查”),然后单击“控制台”选项卡。
    3. 请尝试将以下内容粘贴到控制台中,直接调用您的 API(注意:我们的 fetch 命令是 /api/ai-query——请将您的 fetch 命令修改为与前端代码中一致的命令。)
				
					fetch('/api/ai-query', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ prompt: 'test' })
}).then(r => r.json()).then(console.log);
				
			

You should see { error: “Unauthorized” } — NOT an AI response.

如果这两项测试都通过,说明您的应用是安全的。登录页面保护着“大门”,中间件则保护着“窗户”。

  1. 请最后再检查一次,确保用户身份验证正常:

    • 请完全退出登录并刷新页面——此时您应该会看到登录页面
    • 登录并验证您的 AI 功能是否正常工作
    • 打开开发者工具控制台,尝试在不使用令牌的情况下调用您的 API(如测试 2 所示)——确认是否收到 401 错误
  2. 在源代码控制系统中将更改进行暂存并提交,提交信息可写为“添加基于身份验证中间件的 API 路由保护”,然后将代码推送到你的分支。

  3. 前往 GitHub,从你的add-auth 分支 创建一个 Pull Request,审核更改,然后合并到主分支。 

故障排除

以下是一些可能出现的问题及相应的解决方案

打开开发者工具,切换到“网络”选项卡,点击该 API 请求,然后查看“请求头”。您应该会看到Authorization: Bearer eyJ…这一条。如果该请求头缺失,请确认您的getSession()调用是否正常。

请确认您已将requireAuth应用到了正确的路由上。

请确保在后端使用的是SUPABASE_SERVICE_ROLE_KEY(而非SUPABASE_ANON_KEY)。该服务角色密钥具有验证任何用户令牌的权限。

请确认已安装 Supabase 客户端库(npm install @supabase/supabase-js),并在服务器文件的开头导入该库。

反思

添加用户身份验证是让您的应用更专业、更安全的重要一步。请考虑以下问题:

Sunset and reflection over lake
01

安全
如果只有登录页面却没有后端安全验证,会发生什么情况?
02

您的 API 路由
在你的MVP中,哪些路由需要身份验证中间件,哪些可以保持为公共路由?你是如何决定的?
03

提示
“角色+任务”结构对AI生成的代码质量产生了怎样的影响?与之前的提示词生成的结果相比,这些代码质量是否有差异(更好/更差)?

关键术语

  • 身份验证:验证用户的身份——确认其确为自称的身份(注册和登录)。这与授权不同,授权用于控制用户可以访问的内容。
  • 授权:控制经过身份验证的用户被允许执行哪些操作。行级安全(RLS)是在数据库层实现此功能的一种方式。
  • 密码哈希:在存储密码之前,将其转换为不可读的字符串。Supabase Auth 会自动执行此操作。存储明文密码会带来严重的安全风险。
  • 会话令牌:一种临时标识符,用于在用户通过身份验证后保持其登录状态。Supabase Auth 会自动管理这些令牌。
  • 身份验证中间件:在路由处理程序之前运行的代码,用于验证请求是否来自已登录的用户。如果用户未通过身份验证,该中间件将阻止该请求。
  • 授权头:一种HTTP头,用于在每次请求中将身份验证凭据(通常为令牌)从前端传递到后端。
  • Bearer Token: A type of authentication token sent in the Authorization header. The format is Bearer <token>. “Bearer” means “whoever carries this token is authorized.”
  • JSON Web Token(JWT):一种身份验证令牌的标准格式。其中包含关于用户的编码信息以及过期时间。Supabase 使用 JWT 进行会话管理。
  • OAuth:一种身份验证 标准,允许用户使用现有账户(如 Google)登录,而无需创建新密码。是“开放授权”(Open Authorization)的缩写。

其他资源