Webhooks
通过 Webhooks 同步用户
当您在 Authon 中删除或封禁用户时,您应用的数据库不会自动感知。Webhooks 通过向您的后端实时投递事件来解决这个问题。
问题所在
没有 Webhooks,您的应用无从得知 Authon 用户状态何时变化。这会导致数据陈旧 — 已删除的用户仍然显示,已封禁的用户仍可访问功能。
1
管理员在 Authon 控制台中删除用户2
Authon 的用户记录被移除3
您应用的数据库仍保留陈旧的用户行 — 未发生同步解决方案
订阅 user.* 事件。当事件触发时,更新您的数据库以匹配 Authon 的状态。
1
在 Authon 控制台中注册 Webhook 端点2
订阅 user.created、user.updated、user.deleted、user.banned、user.unbanned3
在处理器中,对数据库中对应的行执行 upsert 或 delete 操作Express + Prisma 示例
一个完整的 Webhook 处理器,使 Prisma 管理的 users 表与 Authon 保持同步。
routes/webhooks.ts
import express from "express";
import { createHmac, timingSafeEqual } from "crypto";
import { PrismaClient } from "@prisma/client";
const app = express();
const prisma = new PrismaClient();
function verifyWebhook(
rawBody: Buffer,
signature: string,
timestamp: string,
secret: string,
): boolean {
const payload = `${timestamp}.${rawBody.toString()}`;
const expected = createHmac("sha256", secret).update(payload).digest("hex");
const actual = signature.replace("v1=", "");
return timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"));
}
app.post(
"/webhooks/authon",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["x-authon-signature"] as string;
const ts = req.headers["x-authon-timestamp"] as string; // ISO 8601
if (!verifyWebhook(req.body, sig, ts, process.env.AUTHON_WEBHOOK_SECRET!)) {
return res.status(401).json({ error: "Invalid signature" });
}
const { event, data } = JSON.parse(req.body.toString());
switch (event) {
case "user.created":
case "user.updated":
await prisma.user.upsert({
where: { authonId: data.user.id },
create: {
authonId: data.user.id,
email: data.user.email,
name: data.user.displayName,
avatarUrl: data.user.avatarUrl,
},
update: {
email: data.user.email,
name: data.user.displayName,
avatarUrl: data.user.avatarUrl,
},
});
break;
case "user.deleted":
await prisma.user.update({
where: { authonId: data.user.id },
data: { deletedAt: new Date() }, // soft-delete
}).catch(() => {});
break;
case "user.banned":
await prisma.user.update({
where: { authonId: data.user.id },
data: { suspended: true, suspendedAt: new Date() },
});
break;
case "user.unbanned":
await prisma.user.update({
where: { authonId: data.user.id },
data: { suspended: false, suspendedAt: null },
});
break;
}
res.json({ received: true });
}
);Next.js App Router 示例
使用 App Router 处理用户同步事件的 Next.js API 路由。
app/api/webhooks/authon/route.ts
import { createHmac, timingSafeEqual } from "crypto";
import { NextRequest, NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get("x-authon-signature")!;
const timestamp = req.headers.get("x-authon-timestamp")!; // ISO 8601
const payload = `${timestamp}.${rawBody}`;
const expected = createHmac("sha256", process.env.AUTHON_WEBHOOK_SECRET!)
.update(payload)
.digest("hex");
const actual = signature.replace("v1=", "");
if (!timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(actual, "hex"))) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const { event, data } = JSON.parse(rawBody);
if (event === "user.created" || event === "user.updated") {
await db.user.upsert({
where: { authonId: data.user.id },
create: {
authonId: data.user.id,
email: data.user.email,
name: data.user.displayName,
},
update: {
email: data.user.email,
name: data.user.displayName,
},
});
} else if (event === "user.deleted") {
await db.user.update({
where: { authonId: data.user.id },
data: { deletedAt: new Date() }, // soft-delete
}).catch(() => {});
}
return NextResponse.json({ received: true });
}边缘情况
Webhook 失败怎么办?
Authon 以指数退避方式最多重试3次(1秒、2秒)。若所有尝试均失败,事件将在控制台中标记为失败,您可以手动重放。
事件顺序
事件按顺序投递,但重试期间可能乱序到达。当顺序重要时,使用 timestamp 字段检测并忽略过期更新。
初始迁移
Webhooks 仅捕获未来事件。对于现有用户的初始同步,请使用 Authon API 的 users.list() 分页遍历所有用户并填充您的数据库。
反向同步(应用 → Authon)
Webhooks 处理 Authon → 应用方向的同步。反向同步 — 从您的应用在 Authon 中创建或更新用户 — 请使用带私密密钥的后端 API。
lib/authon-sync.ts
import { AuthonBackend } from "@authon/node";
const authon = new AuthonBackend(process.env.AUTHON_SECRET_KEY!);
// Create a user in Authon from your app (e.g. after admin invite)
export async function createAuthonUser(email: string, name: string) {
return authon.users.create({
email,
displayName: name,
emailVerified: true,
});
}
// Update user metadata in Authon when your app data changes
export async function syncMetadataToAuthon(
authonId: string,
publicMetadata: Record<string, unknown>,
) {
return authon.users.update(authonId, { publicMetadata });
}
// Bulk import existing users from your app into Authon
export async function bulkImportToAuthon(
users: Array<{ email: string; name: string }>,
) {
for (const user of users) {
await authon.users.create({
email: user.email,
displayName: user.name,
emailVerified: true,
});
}
}所有用户管理操作均可通过 REST API 端点访问 POST /v1/backend/users.