身份验证

本页内容

EmDash 使用 passkey 身份验证作为主要登录方法。Passkey 具有抗钓鱼能力,不需要密码,并通过浏览器或密码管理器在设备间工作。

除了 passkey,你还可以添加可插拔的登录提供者。GitHub 和 Google 已包含在 EmDash 中。单独安装的 Atmosphere 提供者 添加了 AT Protocol 账户,相同的提供者接口也对其他包开放。文档中的 GitHub、Google 和 Atmosphere 提供者可以创建第一个管理员账户或使用已链接的 EmDash 用户登录。

对于 Cloudflare 部署,Cloudflare Access 是生产环境中独立的排他性身份验证模式。它在受保护的 EmDash 路由上验证 Access 凭据,而不是显示 EmDash 登录方法。

选择身份验证模式

Passkey 使用 WebAuthn,这是一种创建存储在设备上或通过密码管理器同步的公钥凭据的 Web 标准。登录时,你的设备证明凭据的所有权,而无需通过网络发送密码。

Passkey 是默认选项。GitHub、Google 和 Atmosphere 提供者是额外的登录方法:每个都验证用户身份、链接或创建 EmDash 账户,并建立与 passkey 登录使用的相同 EmDash 会话。

Passkey 身份验证提供:

  • 无需记忆或可能泄露的密码
  • 抗钓鱼 — 凭据绑定到你的站点域名
  • 跨设备同步 — 与 iCloud 钥匙串、Google 密码管理器、1Password 等配合使用
  • 快速登录 — 生物识别或 PIN 一键完成

Cloudflare Access 使用 auth 选项而非 authProviders。在生产环境中,它成为受保护 /_emdash 路由的权威。EmDash 仍然存储本地用户,以便角色、所有权和禁用用户检查继续工作。

设置第一个用户

首次访问管理面板时,设置向导会引导你创建管理员账户。

  1. 导航到 http://localhost:4321/_emdash/admin

  2. Set up your site 中,输入站点标题和可选的标语。模板也可以提供示例内容。选择 Continue

  3. Create your account 中,输入你的电子邮件地址和可选的名称。选择 Continue

  4. Secure your account 中,创建 passkey 或选择已配置的登录提供者之一。如果选择 passkey,浏览器会询问保存位置:

    • macOS:Touch ID、设备密码或安全密钥
    • Windows:Windows Hello 或安全密钥
    • 移动设备:Face ID、指纹或 PIN
  5. 完成浏览器或提供者流程。EmDash 将第一个用户创建为 Admin 并打开仪表板。

使用 passkey 登录

设置完成后,返回管理面板会触发 passkey 身份验证:

  1. 访问 /_emdash/admin

  2. 如果未登录,你将看到登录页面

  3. 点击 Sign in 进行身份验证

  4. 浏览器提示使用 passkey(生物识别、PIN 或安全密钥)

  5. 验证后,你将被重定向到管理仪表板

使用魔法链接登录

如果无法使用 passkey,魔法链接提供替代方案。站点必须在 EmDash 发送链接之前配置好电子邮件提供者。

  1. 在登录页面,点击 Sign in with email

  2. 输入你的电子邮件地址

  3. 检查收件箱中的登录链接

  4. 点击链接进行身份验证(15 分钟有效)

配置登录提供者

除 passkey 外,EmDash 支持可插拔的登录提供者,出现在登录页面和设置向导中。GitHub 和 Google 已包含在 EmDash 中。Atmosphere 和第三方提供者是通过相同接口注册的单独包。

提供者是累加的 — 启用提供者后 passkey 仍然有效。GitHub 和 Google 仅在提供者提供相同的已验证电子邮件地址时自动链接现有 EmDash 用户。Atmosphere 账户通过其去中心化标识符(DID)链接,因为 EmDash 的 Atmosphere 流程不接收电子邮件地址。每个包含的提供者都可以创建第一个用户,因此全新安装可以完全跳过 passkey。

向 Astro 添加提供者

将提供者传递给 EmDash 集成中的 authProviders 数组。以下示例启用 GitHub、Google 和 Atmosphere:

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";
import { atproto } from "@emdash-cms/auth-atproto";

export default defineConfig({
	integrations: [
		emdash({
			authProviders: [github(), google(), atproto()],
		}),
	],
});

登录页面的顺序很重要:提供者按你列出的顺序渲染,紧凑的仅按钮提供者在前,需要自定义表单的提供者(如需要输入 handle 的 Atmosphere)在后。

GitHub

以下示例启用 GitHub 提供者:

import { github } from "emdash/auth/providers/github";

emdash({ authProviders: [github()] });

通过环境变量设置凭据。EmDash 先检查带前缀的名称,然后回退到不带前缀的:

变量用途
EMDASH_OAUTH_GITHUB_CLIENT_ID / GITHUB_CLIENT_IDOAuth 应用客户端 ID
EMDASH_OAUTH_GITHUB_CLIENT_SECRET / GITHUB_CLIENT_SECRETOAuth 应用密钥

将 GitHub OAuth 应用的回调 URL 配置为 https://your-site.example.com/_emdash/api/auth/oauth/github/callback

Google

以下示例启用 Google 提供者:

import { google } from "emdash/auth/providers/google";

emdash({ authProviders: [google()] });

通过环境变量设置凭据。EmDash 先检查带前缀的名称,然后回退到不带前缀的:

变量用途
EMDASH_OAUTH_GOOGLE_CLIENT_ID / GOOGLE_CLIENT_IDOAuth 应用客户端 ID
EMDASH_OAUTH_GOOGLE_CLIENT_SECRET / GOOGLE_CLIENT_SECRETOAuth 应用密钥

将 Google OAuth 客户端的重定向 URI 配置为 https://your-site.example.com/_emdash/api/auth/oauth/google/callback

Atmosphere(AT Protocol)

对于贡献者已有 Atmosphere 账户 的站点 — Bluesky 及更广泛 AT Protocol 网络背后的用户拥有身份 — 安装 Atmosphere 提供者:

pnpm add @emdash-cms/auth-atproto

以下示例使用 handle 允许列表启用 Atmosphere 提供者:

import { atproto } from "@emdash-cms/auth-atproto";

emdash({
	authProviders: [
		atproto({
			allowedHandles: ["*.example.com"],
		}),
	],
});

不需要客户端密钥或环境变量。有关 handle/DID 允许列表、角色映射以及 AT Protocol OAuth 配置文件所需的本地开发设置,请参阅 Atmosphere 登录指南

构建提供者

提供者是一个 AuthProviderDescriptor:一个 id、一个人类可读的标签,以及其登录流程需要的管理组件、路由处理器、公共路由前缀和存储集合。如果提供者应在首次用户设置期间出现,请从 adminEntry 导出 SetupStep。形状从 emdash 导出:

import type { AuthProviderDescriptor } from "emdash";

export function myProvider(): AuthProviderDescriptor {
	return {
		id: "my-provider",
		label: "My Provider",
		adminEntry: "my-provider/admin", // 导出 LoginButton / LoginForm / SetupStep
		routes: [
			{ pattern: "/_emdash/api/auth/my-provider/login", entrypoint: "my-provider/routes/login.ts" },
			{ pattern: "/_emdash/api/auth/my-provider/callback", entrypoint: "my-provider/routes/callback.ts" },
		],
		publicRoutes: ["/_emdash/api/auth/my-provider/"],
		storage: {
			sessions: {},
		},
	};
}

Atmosphere 包(@emdash-cms/auth-atproto)是需要自定义登录表单、OAuth 路由处理器和持久存储的提供者最完整的实际参考。

用户角色

EmDash 使用基于角色的访问控制,共五个级别:

角色级别描述
Subscriber10读取已发布内容(无草稿访问权限)
Contributor20创建内容(需要批准才能发布)
Author30创建/编辑/发布自己的内容
Editor40管理所有内容
Admin50完全访问权限(包括设置)

每个角色继承所有较低级别的权限。第一个用户始终创建为 Admin。

Subscriber 和草稿内容

Subscriber 拥有 content:read 权限,以便会员专属的已发布内容可以提供给经过身份验证的读者。他们无法看到草稿、计划项目、已删除项目、修订版本或预览 URL — 这些由 content:read_drafts 控制,授予 Contributor 及以上。列表和获取端点对 Subscriber 透明地过滤为 status=published;仅编辑器视图(/compare/revisions/trash/preview-url)直接拒绝 Subscriber 请求。

邀请用户

Admin 可以通过管理面板邀请新用户:

  1. 转到 Settings > Users

  2. 点击 Invite User

  3. 输入用户的电子邮件并选择角色

  4. 点击 Send Invite

  5. 如果配置了电子邮件,EmDash 会发送邀请。否则,复制生成的链接并自行发送给用户。

  6. 用户打开链接,使用 passkey 或邀请页面上提供的登录提供者创建账户。

邀请链接是一次性的,7 天后过期。

管理 passkey

用户可以在账户设置中管理其 passkey:

  • 添加 passkey — 注册额外的 passkey 作为备份或用于其他设备
  • 删除 passkey — 删除不再使用的 passkey
  • 重命名 passkey — 给 passkey 起描述性名称

每个用户最多可以注册 10 个 passkey。

EmDash 不允许用户删除最后一个 passkey。在删除旧的之前添加替代品。

让群组无需邀请即可登录

要让群组无需逐一邀请即可登录,配置带有允许列表的登录提供者。Atmosphere 提供者接受 allowedHandlesallowedDIDs(参见 Atmosphere 登录);Cloudflare Access 适配器通过 autoProvisionroleMapping 从你的身份提供者供应用户。文档中的 GitHub、Google 和 Atmosphere 提供者也可以创建初始管理员账户。

会话

Passkey、魔法链接、邀请和登录提供者回调将 EmDash 用户 ID 存储在 Astro 的会话存储中。浏览器接收 Astro 的不透明 astro-session 标识符;用户和凭据记录保留在 EmDash 数据库中。

Cloudflare Access 也将已解析的 EmDash 用户写入 Astro 会话。这使公共页面在读取 Astro.locals.user 时能够识别已登录用户。会话不替代受保护 /_emdash 路由上的 Access 身份验证:EmDash 在这些请求上再次验证 Access JSON Web Token(JWT)。

身份验证速率限制

EmDash 限制启动未经身份验证的登录或注册流程的端点。限制对每个端点和受信任的客户端 IP 是独立的:

端点限制
POST /_emdash/api/auth/passkey/options每分钟 10 个请求
POST /_emdash/api/auth/magic-link/send每 5 分钟 3 个请求
POST /_emdash/api/auth/signup/request每 5 分钟 3 个请求

在 Cloudflare 上,EmDash 从 Cloudflare 的请求元数据中读取客户端 IP。反向代理后面的自托管站点必须在 EmDash 使用代理的客户端 IP 头之前配置 trustedProxyHeaders。当没有可用的受信任 IP 时,由于没有安全的计数键,这些每 IP 检查将被跳过。

Passkey 存储公钥凭据;私钥保留在用户的认证器中。魔法链接令牌存储为 SHA-256 哈希,使用后删除。

故障排除

”No passkeys registered”

如果登录时看到此错误,你的 passkey 可能已从密码管理器中删除。请管理员发送恢复魔法链接;站点必须配置了电子邮件。

“Passkey authentication failed”

这通常意味着 passkey 是为不同的域创建的。Passkey 绑定到域 — localhost:4321 的 passkey 不适用于 example.com。为每个域注册新的 passkey。

丢失所有 passkey

如果你丢失了所有已注册 passkey 的访问权限:

  1. 请另一位管理员发送恢复魔法链接。站点必须配置了电子邮件。
  2. 在 15 分钟内使用链接登录。
  3. 在账户设置中注册新的 passkey。

如果你是唯一的管理员且未配置电子邮件,你需要通过数据库重置站点的身份验证。

Cloudflare Access

部署到 Cloudflare 时,你可以使用 Cloudflare Access 替代内置的登录方法。Access 在边缘使用你的身份提供者对用户进行身份验证。EmDash 验证签名的 Access JWT,加载此人的身份和组,并将该身份映射到本地 EmDash 用户。

何时使用 Cloudflare Access

  • 单点登录 — 用户使用公司的 IdP 进行身份验证
  • 集中访问控制 — 在 Cloudflare 仪表板中管理谁可以访问管理面板
  • 无需 passkey 管理 — 无需注册或管理 passkey
  • 基于组的角色 — 自动将 IdP 组映射到 EmDash 角色

设置 Access

  1. 为站点的 /_emdash/* 路径创建 Cloudflare Access 应用程序和策略。仅保护 /_emdash/admin/* 会使 REST API 缺少 EmDash 期望的 JWT。
  2. 复制应用程序的 Application Audience (AUD) Tag
  3. 将标签存储在 CF_ACCESS_AUDIENCE 运行时环境变量中。有关本地和已部署的值,请遵循 EmDash 密钥指南
  4. 配置 EmDash 在运行时读取该值:
import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import emdash from "emdash/astro";
import { d1, access } from "@emdash-cms/cloudflare";

export default defineConfig({
	output: "server",
	adapter: cloudflare(),
	integrations: [
		emdash({
			database: d1({ binding: "DB" }),
			auth: access({
				teamDomain: "myteam.cloudflareaccess.com",
				audienceEnvVar: "CF_ACCESS_AUDIENCE",
			}),
		}),
	],
});

应用程序受众标识哪个 Access 应用程序签发了 JWT。EmDash 连同签发者和签名一起验证它;另一个 Access 应用程序的令牌会被拒绝。

配置选项

选项类型默认值描述
teamDomainstring必填你的 Access 团队域名(例如 myteam.cloudflareaccess.com
audiencestring直接提供的 Application Audience (AUD) 标签。Workers 上推荐使用 audienceEnvVar
autoProvisionbooleantrue在首次 Access 登录时创建 EmDash 用户
defaultRolenumber30不匹配任何组的用户的角色(30 = Author)
syncRolesbooleanfalse每次登录时基于 IdP 组更新角色
roleMappingobject将 IdP 组名映射到角色级别
audienceEnvVarstring"CF_ACCESS_AUDIENCE"包含受众标签的环境变量。当省略 audience 时使用。

提供 audienceaudienceEnvVar 下的环境值。

角色映射

将你的 IdP 组映射到 EmDash 角色:

emdash({
	auth: access({
		teamDomain: "myteam.cloudflareaccess.com",
		audienceEnvVar: "CF_ACCESS_AUDIENCE",
		roleMapping: {
			Admins: 50, // Admin
			"Content Editors": 40, // Editor
			Writers: 30, // Author
		},
		defaultRole: 20, // 不属于任何组的用户为 Contributor
	}),
});

如果用户属于多个组,第一个匹配的组优先。访问站点的第一个用户无论组如何始终成为 Admin。

角色同步行为

默认情况下(syncRoles: false),用户的角色在首次登录时设置,之后不会更改。这允许管理员在 EmDash 中手动调整角色。

如果希望 IdP 组具有权威性,请设置 syncRoles: true — 用户的角色将在每次登录时基于其当前组进行更新。

请求和会话流程

  1. 用户访问受 Access 应用程序保护的路径。
  2. 当不存在 Access 会话时,Cloudflare Access 将用户重定向到你的身份提供者。
  3. 身份验证后,Access 在 Cf-Access-Jwt-Assertion 中向源站发送签名的 JWT。
  4. EmDash 验证令牌的签名、签发者和受众,然后读取 Access 身份和组。
  5. EmDash 查找或供应本地用户,应用配置的角色行为,并在 Astro 会话中记录用户。
  6. 后续对受保护 EmDash 路由的请求重复 Access 验证。公共页面可以使用 EmDash 会话来识别用户,而不将其视为新 Access 请求的证明。

Access 替代的功能

当 Access 启用时,以下功能不可用:

  • 登录页面(/_emdash/admin/login
  • Passkey 注册和管理
  • GitHub、Google 和 Atmosphere 登录
  • 魔法链接登录
  • 自助注册
  • 用户邀请

Access 策略决定谁可以到达 EmDash。EmDash 仍然拥有本地角色、内容所有权和禁用用户标志。使用 syncRoles: false 时,管理员可以在 EmDash 中更改已供应用户的角色。使用 syncRoles: true 时,映射的 Access 组在每次登录时替换该角色。

故障排除

”No Access JWT present”

请求到达 EmDash 时没有 Access JWT。这意味着:

  • Access 未配置为保护你的应用程序
  • Access 策略未匹配管理路由

验证 Access 应用程序覆盖完整的 /_emdash/* 路径,并且其策略包含用户。

“JWT audience mismatch”

配置中的 audience 与 JWT 不匹配。检查 Access 应用程序设置中的 Application Audience Tag。

“User not authorized”

用户通过 Access 身份验证,但 autoProvisionfalse 且用户不存在于 EmDash 中。选项:

  • 设置 autoProvision: true,或
  • 在用户登录前手动创建用户