认证与账号

注册、登录、验证邮箱、社交登录、2FA 和账号中心。根据产品需求完成配置后即可使用,无需重新实现认证系统。

Saavo 模板内置了完整的账号系统,涵盖注册、登录、邮箱验证、找回密码、社交登录、两步验证、账号中心和管理员身份等常见能力。

开发自己的产品时,通常不需要重新实现这些功能:根据产品需求调整认证策略,配置好邮件、OAuth 等依赖服务,然后在页面、API 和前端组件中接入并校验用户身份即可。

已有功能

模板初始化后,以下认证能力开箱即用:

能力默认入口默认状态
注册/auth/signup已启用
登录/auth/login已启用
找回密码/auth/forgot-password已启用
邮箱验证/auth/verify-email注册后发送验证邮件,但默认不强制验证
Google 登录登录页配置密钥后启用
GitHub 登录登录页配置密钥后启用
两步验证/account用户可自行开启
账号中心/account已登录用户可访问
管理后台/dashboard管理员可访问

除上述功能外,Saavo 还实现了 Session 管理、登录弹窗、恢复码、高敏操作二次验证,以及其他认证相关的安全限制。

当基于 Saavo 模板开发自己的产品时,不需要再创建另一套登录状态,也不需要在业务代码里自己解析 Cookie 或直接操作 Session 数据,业务代码应基于 Saavo 提供的认证结果来二次开发。

先体验注册和登录

修改认证配置之前,建议先用默认配置完整走一遍认证流程,快速了解模板已提供的能力。

启动开发服务器后,打开首页,右上角会显示登录按钮:

登录按钮

点击登录按钮,可以通过登录弹窗直接完成登录。Saavo 也提供了独立的登录页面:

登录页面

还没有账号的话,可以进入注册页面,填写邮箱和密码完成注册:

注册页面

默认情况下,Saavo 会发送邮箱验证邮件,但不会强制用户验证,未验证也能继续使用产品。

本地开发时,模板不会真正发送邮件,而是把模拟邮件输出到开发服务器控制台。你可以直接从日志里找到验证码,例如:

[DEV EMAIL PREVIEW] {
  recipients: [ 'saavo@live.com' ],
  subject: 'Welcome to Saavo Starter',
  text: 'Hello, saavo,\n' +
    '\n' +
    'Thank you for registering with Saavo Starter! To ensure the security of your account, we need to verify your email address.\n' +
    '\n' +
    'Your verification code is 2RBFH7IA\n' +
    '\n' +
    'If you did not sign up for a Saavo Starter account, please disregard this email.\n' +
    '\n' +
    'If you have any questions, feel free to contact our support team.\n' +
    '\n' +
    'Best regards,\n' +
    'The Saavo Starter Team',
  html: ''
}

用上述邮件中的验证码即可完成邮箱验证。如果没有开启强制验证,也可以先跳过:

跳过邮箱验证

登录后,通过右上角头像进入用户中心:

用户中心

用户中心(/account)已包含个人信息、安全设置(密码、邮箱、两步验证)、已购产品和支付记录等功能。

除了上述功能,模板还为管理员提供了独立的控制台页面:

管理后台

管理员邮箱在 adminEmails 中配置,只有完成邮箱匹配和邮箱验证的用户才能获得管理员身份,访问 /dashboard。

提示

开发人员通常不需要重新实现这些页面,而是根据产品需求调整认证策略,让业务直接复用已有的认证状态。

配置用户认证

默认配置以快速跑通模板为目标。正式开发产品时,建议至少确认以下几个关键选项。

是否强制验证邮箱

默认情况下,用户注册后会收到验证邮件,即使未完成验证也可以登录。

产品正式对外服务后,尤其是资源消耗较高、需要防范批量注册或对账号真实性有要求的场景,建议开启强制邮箱验证:

emailVerification: {
    defaultRequired: true,
}

开启后,未完成邮箱验证的用户访问受保护页面时,会被引导去完成邮箱验证。

注册后是否发送验证邮件,由另一个配置决定:

emailVerification: {
    sendRegisterEmail: true,
}

也就是说,“是否发送验证邮件”和“是否必须完成邮箱验证”是两个独立的设置。

邮箱验证默认使用验证码方式:

emailVerification: {
    sendVerifyType: 'code',
}

如果想让用户直接点击邮件中的链接完成验证,改为:

emailVerification: {
    sendVerifyType: 'link',
}

注意事项

开启强制邮箱验证前,请先确保生产环境的邮件服务能正常使用,否则新注册用户将无法通过邮箱验证继续使用产品。

选择登录方式

邮箱和密码登录默认已启用。

此外,Saavo 还支持 Google 和 GitHub 登录。第三方登录根据当前环境变量按需启动,没有配置对应的 OAuth 密钥时,登录入口不会显示。

如果你的 SaaS 产品主要面向普通消费者,那么开启 Google 登录通常能降低注册门槛;面向开发者时,再根据目标用户决定是否提供 GitHub 登录,如果你需要开启其它第三方登录,例如 Apple 登录,则需要参考相关文档自行实现。

使用 Google 登录时,还可以通过下面的配置开启 Google One Tap:

enableGoogleOneTap: true

Client ID、Client Secret 和 Callback URL 的具体配置见后文“配置认证依赖的服务”。

是否强制使用双重验证

Saavo 已经提供基于验证器 App 的两步验证和恢复码。

默认配置为:

twoFactorAuth: {
    defaultRequired: false,
}

2FA 功能是可选的,上述选项表示所有用户默认不启用,但用户可以随时在 /account 的安全设置里自行开启。

两步验证设置

这种方式适合大多数 SaaS 产品:需要额外安全保护的用户可以主动开启,其他用户也不必为此付出额外的注册和使用成本。

如果产品有更高的安全要求,可以设置:

twoFactorAuth: {
    defaultRequired: true,
}

开启后,用户需要先完成两步验证设置才能使用产品。

恢复码数量可以通过下面的配置调整:

twoFactorAuth: {
    recoveryCodeCount: 5,
}

另外,初始化产品时记得修改 twoFactorAuth.issuer 配置:

twoFactorAuth: {
    issuer: 'Acme',
}

issuer 会显示在 Google Authenticator、1Password 等验证器 App 中,应改成自己的产品名称。

设置登录后的跳转页面

模板默认在注册、登录和退出后返回首页。

真实的 SaaS 产品中,用户登录后通常应直接进入应用。例如产品主界面在 /app:

ui: {
    redirectTo: {
        afterSignup: '/app',
        afterLogin: '/app',
        afterLogout: '/',
    },
}

调整后的流程:

注册成功 → /app
登录成功 → /app
退出登录 → /

建议尽早修改这部分配置,避免用户登录后仍被带回模板默认首页。

选择登录弹窗或登录页面

Saavo 默认启用登录弹窗功能,用户可以直接通过右上角的登录按钮打开弹窗,也可能在业务操作中被动触发显示登录界面。

例如未登录用户点击购买按钮时,可以在当前页面完成登录,然后继续原来的付款流程,而不需要跳转到登录页面。

如果不需要登录弹窗,可以关闭 ui.useLoginModal 配置:

ui: {
    useLoginModal: false,
}

关闭后,需要登录时统一跳转到完整登录页面。

两种方式各有适用场景:用户经常在操作过程中被要求登录时,弹窗更自然;登录本身就是明确的独立步骤时,完整登录页更简单。

添加管理员

Saavo 通过 config/base.ts 中的 adminEmails 设置管理员:

adminEmails: ['admin@saavo.dev'],

用该邮箱注册账号并完成邮箱验证后,即可获得管理员身份并访问:

/dashboard

adminEmails 不是绕过认证的超级账号。管理员首先是普通用户,只有已完成验证的邮箱与配置中的地址完全匹配,才会获得管理员身份。

建议产品初始化后尽早创建自己的管理员账号,并确认能正常进入管理后台。

准备认证服务

认证逻辑已内置在模板中,但邮件、OAuth、Turnstile 等功能仍依赖外部服务和环境变量。

SAAS_SECRET

每个项目都需要配置 SAAS_SECRET 环境变量,避免重复使用相同的值:

SAAS_SECRET=...

Session 和其他敏感数据的加密都依赖它,建议每个项目都独立设置。

邮件服务

以下认证流程都需要发送邮件:

  • 注册后的邮箱验证
  • 重新发送验证码
  • 找回密码
  • 修改邮箱
  • 其他需要邮件确认的安全操作

本地开发时,Saavo 会直接使用控制台输出的模拟邮件,不会真正发送邮件。

正式部署后,需要配置真正用于发送邮件的服务。支持的邮件服务和配置方式见:

邮件

如果开启了强制邮箱验证,应先确认邮件服务能正常发送邮件。

Google 和 GitHub 登录

如果需要 Google 登录,在 .env 中配置:

GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...

Google OAuth 应用中的 Redirect URI 应设置为:

https://your-domain.com/api/auth/oauth2/google

如果需要 GitHub 登录:

GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

对应的 Redirect URI 为:

https://your-domain.com/api/auth/oauth2/github

没有配置对应密钥时,该登录方式不会显示。

修改 .env 后需要重启开发服务器。

完整的 Google 登录配置流程见:

接入 Google 登录

Turnstile 人机验证

Saavo 支持用 Cloudflare Turnstile 为注册、登录等认证页面增加人机验证。

默认不是每次访问都显示 Turnstile,而是根据认证操作的风险计数触发:

useTurnstile: {
    threshold: 2,
    interval: '1h',
}

其中:

  • threshold 表示触发人机验证的风险计数阈值。
  • interval 表示风险计数的有效期。

按默认配置,对应认证操作的风险计数达到 2 时,就会要求人机验证。参数错误、认证失败和部分检查事件会增加计数,成功事件可以降低计数,因此不能把它理解为“刷新页面超过两次”。

Turnstile 人机验证

需要更早触发验证时,可以调低阈值。不需要 Turnstile 时,也可以直接设置:

useTurnstile: false

启用 Turnstile 时需要配置:

CLOUDFLARE_TURNSTILE_SITE_KEY=...
CLOUDFLARE_TURNSTILE_SECRET_KEY=...

本地开发可以使用 Cloudflare 提供的测试密钥,生产环境应使用自己站点申请的正式密钥。

在业务中验证用户身份

配置完成后,下一步就是让业务代码读取当前用户的认证状态。

基本原则是:

不要在业务代码中自己读取 Cookie,也不要直接查询或修改 Session 表。

Saavo 已通过 Guard 统一处理登录状态、邮箱验证、两步验证等认证条件,业务代码直接用认证结果即可。

保护页面

很多 SaaS 页面只允许登录用户访问,例如账号中心、工作台或用户自己的项目页面。

服务端渲染页面可以在页面 Handler 中调用 authenticatedGuard:

const accountPageHandlers = createLayoutHandlers(
    createLayoutRenderer('page'),
    (c: Ctx) => {
        const locale = c.var.getLocale();
        const guardResult = authenticatedGuard(c);

        if (!guardResult.success) {
            return c.redirect(
                getFullPath(guardResult.details?.redirectUrl as string, locale),
            );
        }

        const { authContext } = guardResult.data;

        return <AccountPage c={c} />;
    },
);

export default accountPageHandlers;

authenticatedGuard 不只是判断用户有没有 Session。

它会根据当前认证策略自动处理不同的认证状态:

未登录
↓
登录页面

需要验证邮箱,但尚未验证
↓
邮箱验证

要求 2FA,但尚未完成
↓
两步验证流程

所有认证要求均满足
↓
继续执行业务页面

因此,业务页面不需要重复实现登录、邮箱验证和 2FA 判断。

如果页面还需要管理员权限、角色或 Capability,在认证通过后再做相应检查即可。

完整示例见:

登录后页面

保护 API

API 使用同一套认证守卫,只是失败时返回 JSON 而不是重定向:

const guardResult = authenticatedGuard(c);

if (!guardResult.success) {
    return c.json(
        guardResult,
        guardResult.code === gResultCode.authLoginRequired ? 401 : 403,
    );
}

const { authContext } = guardResult.data;

这样页面和 API 的认证规则保持一致。

以后把邮箱验证或 2FA 从可选改为强制时,也无需逐个修改业务接口的认证逻辑。

使用 authContext 处理用户业务

认证成功后,从 authContext 对象中可以获取当前请求的认证信息,它包含当前用户和 Session 的信息,是后续业务逻辑的身份入口,也是后续权限控制的基础。

例如,一个用户正在创建自己的项目:

POST /api/projects

服务端需要知道这个项目属于谁。

这时应该使用认证上下文中的当前用户身份,而不是直接信任前端提交的 userId。

类似的业务还有:

  • 创建属于当前用户的数据
  • 查询当前用户的项目
  • 修改自己的资源
  • 判断当前角色
  • 检查 Entitlement
  • 检查 Capability
  • 判断当前用户是否允许执行某个操作

整个流程通常是:

请求
↓
authenticatedGuard()
↓
authContext
↓
角色 / 权益 / 能力
↓
业务逻辑

认证负责确定“当前是谁”,Role、Entitlement 和 Capability 则进一步决定“当前用户可以做什么”。

在前端读取当前用户

除了服务端认证,前端组件有时也需要知道当前用户是否已登录。

Saavo 使用 MPA 渲染方案,跨组件共享的前端状态需要通过 Store 进行管理。用户状态目前基于 nanostores 实现,你可以通过 useCurrentUser 获取:

import { setLoginModalOpen } from '@/stores/login-modal';
import { useCurrentUser } from '@/stores/user';

const { isLoggedIn } = useCurrentUser();

if (!isLoggedIn) {
    setLoginModalOpen(true);
    return;
}

这个例子会在用户未登录时打开登录弹窗。

其他组件修改用户信息后,也可以通过 setCurrentUser 更新共享状态。

需要注意,前端的 isLoggedIn 主要用于控制界面行为,例如显示登录按钮、打开登录弹窗或隐藏某些入口。

数据访问和权限控制仍必须在服务端用 Guard、Role、Entitlement 或 Capability 检查,前端状态不能作为安全边界。

扩展账号设置

/account 已经覆盖了大多数账号级别的设置,无需重复实现。

适合继续放在 /account 的内容包括:

  • 用户资料
  • 邮箱
  • 密码
  • 两步验证
  • 恢复码
  • 已购产品
  • 支付和账单信息

这些功能都属于用户账号本身。

而属于具体 SaaS 业务的设置,应放在业务自己的页面里。例如一个 AI 产品可能还需要:

默认 AI 模型
生成参数
工作区设置
通知设置
团队配置

这些内容由业务模块自己实现,而不是继续堆到 /account 中。

一个简单的判断方式是:

与“用户是谁”有关的设置放在 /account,与“产品如何工作”有关的设置放在业务页面。

上线检查

认证是产品上线前必须完整验证的基础功能,建议至少检查以下流程:

  • 注册、邮箱验证、登录和退出流程正常。
  • 找回密码可以正常完成。
  • /account 中的账号功能可以正常使用。
  • 已启用的 Google / GitHub 第三方登录可以正常完成。
  • 如启用 2FA,可以完成开启、验证和恢复码流程。
  • 未登录用户无法访问受保护页面和 API。
  • 普通用户无法访问 /dashboard。
  • 管理员可以正常访问 /dashboard。
  • 生产环境使用独立的 SAAS_SECRET。
  • 生产环境邮件可以正常发送。
  • OAuth 的 Redirect URI 已经切换为生产域名。
  • Turnstile 等认证安全配置已经使用生产环境密钥。

建议用一个全新注册的普通账号和一个管理员账号完整测试,不要只用开发期间一直存在的老测试账号。

常见问题

接下来

根据接下来要开发的功能,可以继续阅读:

大多数产品走完本章后,就不需要再修改认证系统本身。

接下来直接基于当前登录用户,接入产品真正的业务能力即可。