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 仍然存储本地用户,以便角色、所有权和禁用用户检查继续工作。
设置第一个用户
首次访问管理面板时,设置向导会引导你创建管理员账户。
-
导航到
http://localhost:4321/_emdash/admin -
在 Set up your site 中,输入站点标题和可选的标语。模板也可以提供示例内容。选择 Continue。
-
在 Create your account 中,输入你的电子邮件地址和可选的名称。选择 Continue。
-
在 Secure your account 中,创建 passkey 或选择已配置的登录提供者之一。如果选择 passkey,浏览器会询问保存位置:
- macOS:Touch ID、设备密码或安全密钥
- Windows:Windows Hello 或安全密钥
- 移动设备:Face ID、指纹或 PIN
-
完成浏览器或提供者流程。EmDash 将第一个用户创建为 Admin 并打开仪表板。
使用 passkey 登录
设置完成后,返回管理面板会触发 passkey 身份验证:
-
访问
/_emdash/admin -
如果未登录,你将看到登录页面
-
点击 Sign in 进行身份验证
-
浏览器提示使用 passkey(生物识别、PIN 或安全密钥)
-
验证后,你将被重定向到管理仪表板
使用魔法链接登录
如果无法使用 passkey,魔法链接提供替代方案。站点必须在 EmDash 发送链接之前配置好电子邮件提供者。
-
在登录页面,点击 Sign in with email
-
输入你的电子邮件地址
-
检查收件箱中的登录链接
-
点击链接进行身份验证(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_ID | OAuth 应用客户端 ID |
EMDASH_OAUTH_GITHUB_CLIENT_SECRET / GITHUB_CLIENT_SECRET | OAuth 应用密钥 |
将 GitHub OAuth 应用的回调 URL 配置为 https://your-site.example.com/_emdash/api/auth/oauth/github/callback。
以下示例启用 Google 提供者:
import { google } from "emdash/auth/providers/google";
emdash({ authProviders: [google()] });
通过环境变量设置凭据。EmDash 先检查带前缀的名称,然后回退到不带前缀的:
| 变量 | 用途 |
|---|---|
EMDASH_OAUTH_GOOGLE_CLIENT_ID / GOOGLE_CLIENT_ID | OAuth 应用客户端 ID |
EMDASH_OAUTH_GOOGLE_CLIENT_SECRET / GOOGLE_CLIENT_SECRET | OAuth 应用密钥 |
将 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 使用基于角色的访问控制,共五个级别:
| 角色 | 级别 | 描述 |
|---|---|---|
| Subscriber | 10 | 读取已发布内容(无草稿访问权限) |
| Contributor | 20 | 创建内容(需要批准才能发布) |
| Author | 30 | 创建/编辑/发布自己的内容 |
| Editor | 40 | 管理所有内容 |
| Admin | 50 | 完全访问权限(包括设置) |
每个角色继承所有较低级别的权限。第一个用户始终创建为 Admin。
Subscriber 和草稿内容
Subscriber 拥有 content:read 权限,以便会员专属的已发布内容可以提供给经过身份验证的读者。他们无法看到草稿、计划项目、已删除项目、修订版本或预览 URL — 这些由 content:read_drafts 控制,授予 Contributor 及以上。列表和获取端点对 Subscriber 透明地过滤为 status=published;仅编辑器视图(/compare、/revisions、/trash、/preview-url)直接拒绝 Subscriber 请求。
邀请用户
Admin 可以通过管理面板邀请新用户:
-
转到 Settings > Users
-
点击 Invite User
-
输入用户的电子邮件并选择角色
-
点击 Send Invite
-
如果配置了电子邮件,EmDash 会发送邀请。否则,复制生成的链接并自行发送给用户。
-
用户打开链接,使用 passkey 或邀请页面上提供的登录提供者创建账户。
邀请链接是一次性的,7 天后过期。
管理 passkey
用户可以在账户设置中管理其 passkey:
- 添加 passkey — 注册额外的 passkey 作为备份或用于其他设备
- 删除 passkey — 删除不再使用的 passkey
- 重命名 passkey — 给 passkey 起描述性名称
每个用户最多可以注册 10 个 passkey。
EmDash 不允许用户删除最后一个 passkey。在删除旧的之前添加替代品。
让群组无需邀请即可登录
要让群组无需逐一邀请即可登录,配置带有允许列表的登录提供者。Atmosphere 提供者接受 allowedHandles 和 allowedDIDs(参见 Atmosphere 登录);Cloudflare Access 适配器通过 autoProvision 和 roleMapping 从你的身份提供者供应用户。文档中的 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 的访问权限:
- 请另一位管理员发送恢复魔法链接。站点必须配置了电子邮件。
- 在 15 分钟内使用链接登录。
- 在账户设置中注册新的 passkey。
如果你是唯一的管理员且未配置电子邮件,你需要通过数据库重置站点的身份验证。
Cloudflare Access
部署到 Cloudflare 时,你可以使用 Cloudflare Access 替代内置的登录方法。Access 在边缘使用你的身份提供者对用户进行身份验证。EmDash 验证签名的 Access JWT,加载此人的身份和组,并将该身份映射到本地 EmDash 用户。
何时使用 Cloudflare Access
- 单点登录 — 用户使用公司的 IdP 进行身份验证
- 集中访问控制 — 在 Cloudflare 仪表板中管理谁可以访问管理面板
- 无需 passkey 管理 — 无需注册或管理 passkey
- 基于组的角色 — 自动将 IdP 组映射到 EmDash 角色
设置 Access
- 为站点的
/_emdash/*路径创建 Cloudflare Access 应用程序和策略。仅保护/_emdash/admin/*会使 REST API 缺少 EmDash 期望的 JWT。 - 复制应用程序的 Application Audience (AUD) Tag。
- 将标签存储在
CF_ACCESS_AUDIENCE运行时环境变量中。有关本地和已部署的值,请遵循 EmDash 密钥指南。 - 配置 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 应用程序的令牌会被拒绝。
配置选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
teamDomain | string | 必填 | 你的 Access 团队域名(例如 myteam.cloudflareaccess.com) |
audience | string | — | 直接提供的 Application Audience (AUD) 标签。Workers 上推荐使用 audienceEnvVar。 |
autoProvision | boolean | true | 在首次 Access 登录时创建 EmDash 用户 |
defaultRole | number | 30 | 不匹配任何组的用户的角色(30 = Author) |
syncRoles | boolean | false | 每次登录时基于 IdP 组更新角色 |
roleMapping | object | — | 将 IdP 组名映射到角色级别 |
audienceEnvVar | string | "CF_ACCESS_AUDIENCE" | 包含受众标签的环境变量。当省略 audience 时使用。 |
提供 audience 或 audienceEnvVar 下的环境值。
角色映射
将你的 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 — 用户的角色将在每次登录时基于其当前组进行更新。
请求和会话流程
- 用户访问受 Access 应用程序保护的路径。
- 当不存在 Access 会话时,Cloudflare Access 将用户重定向到你的身份提供者。
- 身份验证后,Access 在
Cf-Access-Jwt-Assertion中向源站发送签名的 JWT。 - EmDash 验证令牌的签名、签发者和受众,然后读取 Access 身份和组。
- EmDash 查找或供应本地用户,应用配置的角色行为,并在 Astro 会话中记录用户。
- 后续对受保护 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 身份验证,但 autoProvision 为 false 且用户不存在于 EmDash 中。选项:
- 设置
autoProvision: true,或 - 在用户登录前手动创建用户