认证系统
Better Auth 配置与使用指南
认证系统
Sistine Starter 使用 Better Auth 作为认证解决方案,支持邮箱密码登录和 Google OAuth。本指南将介绍如何配置认证系统、保护路由以及使用认证 API。
Better Auth 简介
Better Auth 是一个现代化的 TypeScript 认证库,具有以下特点:
- 类型安全: 完整的 TypeScript 支持
- 框架无关: 支持 Next.js、React、Svelte、Vue 等
- 开箱即用: 内置邮箱密码、OAuth、魔法链接等多种认证方式
- 灵活扩展: 通过 hooks 和插件系统轻松扩展
- 安全优先: 遵循最佳安全实践(密码哈希、CSRF 保护等)
相比 NextAuth.js,Better Auth 提供更好的类型推断和更简洁的 API。
配置步骤
1. 生成密钥
Better Auth 需要一个至少 32 字符的密钥用于加密会话和 Token。
生成方法:
openssl rand -base64 32
将生成的密钥添加到 .env.local
:
BETTER_AUTH_SECRET="your-super-secret-key-generated-above"
2. 配置基础 URL
设置应用的基础 URL,用于生成回调链接:
# 开发环境
BETTER_AUTH_URL="http://localhost:3000"
# 生产环境
BETTER_AUTH_URL="https://yourdomain.com"
3. 配置 Google OAuth (可选)
如果你想启用 Google 登录,需要在 Google Cloud Console 中创建 OAuth 凭据。
步骤 A: 创建 Google OAuth 凭据
-
创建新项目或选择现有项目
-
启用 Google+ API:
- 左侧菜单 > APIs & Services > Library
- 搜索 "Google+ API" 并启用
-
创建 OAuth 2.0 凭据:
- 左侧菜单 > APIs & Services > Credentials
- 点击 "Create Credentials" > "OAuth client ID"
- 应用类型: Web application
- 名称:
Sistine Starter
-
配置授权重定向 URI:
# 开发环境 http://localhost:3000/api/auth/callback/google # 生产环境 https://yourdomain.com/api/auth/callback/google
-
保存后获取 Client ID 和 Client Secret
步骤 B: 配置环境变量
将凭据添加到 .env.local
:
AUTH_GOOGLE_ID="123456789-xxxxxxxxxxxxx.apps.googleusercontent.com"
AUTH_GOOGLE_SECRET="GOCSPX-xxxxxxxxxxxxxxxx"
4. 配置信任的源 (可选)
如果你有多个域名需要访问 API,配置信任的源:
BETTER_AUTH_TRUSTED_ORIGINS="http://localhost:3000,https://yourdomain.com,https://preview.yourdomain.com"
多个源用逗号分隔。
服务端配置
Better Auth 的服务端配置位于 lib/auth.ts
:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createAuthMiddleware } from "better-auth/api";
import { db } from "./db";
import { refundCredits } from "./credits";
const defaultTrustedOrigins = ["http://localhost:3000"];
const trustedOrigins = process.env.BETTER_AUTH_TRUSTED_ORIGINS
? process.env.BETTER_AUTH_TRUSTED_ORIGINS.split(",")
.map((origin) => origin.trim())
.filter(Boolean)
: defaultTrustedOrigins;
export const auth = betterAuth({
// 数据库适配器 (使用 Drizzle ORM)
database: drizzleAdapter(db, {
provider: "pg",
}),
// 应用基础 URL
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
// 加密密钥
secret: process.env.BETTER_AUTH_SECRET,
// 启用邮箱密码登录
emailAndPassword: {
enabled: true,
},
// 配置 Google OAuth
socialProviders: {
google: {
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
},
},
// 信任的源
trustedOrigins,
// 注册后赠送积分的 Hook
hooks: {
after: createAuthMiddleware(async (ctx) => {
// 监听用户注册事件 (邮箱和 OAuth)
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
try {
// 赠送 300 积分作为注册奖励
await refundCredits(
newSession.user.id,
300,
"registration_bonus"
);
console.log(`[Auth] New user registered, granted 300 credits: ${newSession.user.email}`);
} catch (error) {
console.error("[Auth] Failed to grant registration bonus:", error);
}
}
}
}),
},
});
export { hashPassword } from "better-auth/crypto";
关键配置说明
配置项 | 说明 |
---|---|
database | 使用 Drizzle 适配器连接 PostgreSQL |
baseURL | 应用的公开 URL,用于生成回调链接 |
secret | 加密密钥,必须至少 32 字符 |
emailAndPassword | 启用邮箱密码登录 |
socialProviders.google | 配置 Google OAuth |
trustedOrigins | 允许的 CORS 源 |
hooks.after | 注册后执行的钩子函数 |
客户端使用
创建客户端实例
客户端配置位于 lib/auth-client.ts
:
"use client";
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000",
});
export const { signIn, signUp, signOut, useSession } = authClient;
在组件中使用
获取当前会话
"use client";
import { useSession } from "@/lib/auth-client";
export function ProfileComponent() {
const session = useSession();
if (session.isPending) {
return <div>加载中...</div>;
}
if (!session.data) {
return <div>未登录</div>;
}
return (
<div>
<p>欢迎, {session.data.user.name}!</p>
<p>邮箱: {session.data.user.email}</p>
<p>积分: {session.data.user.credits}</p>
</div>
);
}
邮箱密码登录
"use client";
import { signIn } from "@/lib/auth-client";
import { useState } from "react";
export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await signIn.email({
email,
password,
});
if (result.error) {
setError(result.error.message);
} else {
// 登录成功,重定向到仪表板
window.location.href = "/dashboard";
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
required
/>
{error && <p className="text-red-500">{error}</p>}
<button type="submit">登录</button>
</form>
);
}
邮箱密码注册
"use client";
import { signUp } from "@/lib/auth-client";
export function SignUpForm() {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const result = await signUp.email({
email: formData.get("email") as string,
password: formData.get("password") as string,
name: formData.get("name") as string,
});
if (result.error) {
alert(result.error.message);
} else {
// 注册成功,用户将自动获得 300 积分
window.location.href = "/dashboard";
}
};
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="姓名" required />
<input name="email" type="email" placeholder="邮箱" required />
<input name="password" type="password" placeholder="密码" required />
<button type="submit">注册</button>
</form>
);
}
Google OAuth 登录
"use client";
import { signIn } from "@/lib/auth-client";
export function GoogleLoginButton() {
const handleGoogleLogin = async () => {
await signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
};
return (
<button onClick={handleGoogleLogin}>
使用 Google 登录
</button>
);
}
登出
"use client";
import { signOut } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
export function LogoutButton() {
const router = useRouter();
const handleLogout = async () => {
await signOut();
router.push("/");
};
return <button onClick={handleLogout}>退出登录</button>;
}
服务端保护路由
方法 1: 使用 SessionGuard 组件
SessionGuard
是一个客户端组件,用于保护需要登录才能访问的页面。
组件代码 (features/auth/components/session-guard.tsx
):
"use client";
import * as React from "react";
import { useRouter, usePathname } from "next/navigation";
import { useLocale } from 'next-intl';
import { useSession } from "@/lib/auth-client";
export function SessionGuard({ children }: { children: React.ReactNode }) {
const router = useRouter();
const session = useSession();
const locale = useLocale();
React.useEffect(() => {
if (!session.isPending && !session.data) {
// 未登录,重定向到登录页
router.replace(`/${locale}/login`);
}
}, [router, session.data, session.isPending]);
if (session.isPending) {
// 加载中显示占位符
return (
<div className="flex min-h-screen items-center justify-center">
<div className="h-12 w-12 animate-pulse rounded-full bg-muted" />
</div>
);
}
if (!session.data?.user) {
return null;
}
return <>{children}</>;
}
使用示例:
// app/[locale]/(protected)/dashboard/page.tsx
import { SessionGuard } from "@/features/auth/components/session-guard";
export default function DashboardPage() {
return (
<SessionGuard>
<div>
<h1>仪表板</h1>
<p>这是受保护的页面,只有登录用户才能访问。</p>
</div>
</SessionGuard>
);
}
方法 2: 在 API 路由中验证
在 API 路由中验证用户身份:
// app/api/user/profile/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth";
export async function GET(request: NextRequest) {
// 从请求头中获取会话
const session = await auth.api.getSession({ headers: request.headers });
if (!session) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
);
}
// 已登录,返回用户信息
return NextResponse.json({
user: session.user,
});
}
方法 3: 在 Server Action 中验证
// app/actions/update-profile.ts
"use server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function updateProfile(name: string) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
throw new Error("Unauthorized");
}
// 更新用户资料
// ...
return { success: true };
}
新用户注册赠送 300 积分
Sistine Starter 在用户注册时自动赠送 300 积分作为新人礼包。这个功能通过 Better Auth 的 hooks.after
实现。
实现机制
在 lib/auth.ts:36-56
中:
hooks: {
after: createAuthMiddleware(async (ctx) => {
// 监听用户注册事件 (邮箱和 OAuth)
if (ctx.path.startsWith("/sign-up")) {
const newSession = ctx.context.newSession;
if (newSession) {
try {
// 赠送 300 积分作为注册奖励
await refundCredits(
newSession.user.id,
300,
"registration_bonus"
);
console.log(`[Auth] New user registered, granted 300 credits: ${newSession.user.email}`);
} catch (error) {
console.error("[Auth] Failed to grant registration bonus:", error);
}
}
}
}),
},
工作流程
- 用户注册: 通过邮箱密码或 Google OAuth 注册
- Better Auth 创建账户: 插入
user
和account
表 - Hook 触发:
ctx.path.startsWith("/sign-up")
检测到注册事件 - 发放积分: 调用
refundCredits(userId, 300, "registration_bonus")
- 更新数据库:
user.credits
增加 300creditLedger
插入记录:{ delta: 300, reason: 'registration_bonus' }
积分账本记录
在 creditLedger
表中会生成如下记录:
id: "uuid-xxx"
userId: "user_123"
delta: 300
reason: "registration_bonus"
paymentId: null
createdAt: 2025-10-15T08:30:00.000Z
自定义赠送金额
如需修改赠送积分数量,编辑 lib/auth.ts:44
:
await refundCredits(
newSession.user.id,
500, // 改为 500 积分
"registration_bonus"
);
最佳实践
1. 密钥管理
- ✅ 使用至少 32 字符的随机密钥
- ✅ 开发和生产使用不同的密钥
- ✅ 定期轮换生产环境密钥
- ✅ 永远不要将密钥提交到 Git
2. OAuth 回调 URL
确保回调 URL 与环境匹配:
环境 | 回调 URL |
---|---|
本地开发 | http://localhost:3000/api/auth/callback/google |
预览环境 | https://preview.yourdomain.com/api/auth/callback/google |
生产环境 | https://yourdomain.com/api/auth/callback/google |
3. 会话管理
Better Auth 自动管理会话,默认配置:
- 会话有效期: 7 天
- 自动续期: 是
- 存储位置: 数据库 (
session
表)
如需自定义会话时长,在 lib/auth.ts
中添加:
export const auth = betterAuth({
// ...
session: {
expiresIn: 60 * 60 * 24 * 30, // 30 天 (秒)
updateAge: 60 * 60 * 24, // 每 24 小时更新一次
},
});
4. 密码要求
Better Auth 默认的密码要求:
- 最少 8 字符
- 无复杂度要求
如需自定义密码要求,在 lib/auth.ts
中配置:
export const auth = betterAuth({
// ...
emailAndPassword: {
enabled: true,
minPasswordLength: 10,
maxPasswordLength: 128,
},
});
5. 错误处理
始终处理认证错误:
const result = await signIn.email({ email, password });
if (result.error) {
switch (result.error.status) {
case 401:
setError("邮箱或密码错误");
break;
case 403:
setError("账户已被禁用");
break;
default:
setError("登录失败,请稍后重试");
}
}
6. 多因素认证 (未来计划)
Better Auth 支持 MFA,可通过插件启用:
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
// ...
plugins: [
twoFactor({
issuer: "Sistine AI",
}),
],
});
常见问题
如何获取当前登录用户?
客户端:
const session = useSession();
const user = session.data?.user;
服务端 (API 路由):
const session = await auth.api.getSession({ headers: request.headers });
const user = session?.user;
服务端 (Server Component):
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
const session = await auth.api.getSession({ headers: await headers() });
如何实现"记住我"功能?
Better Auth 默认支持持久化会话,无需额外配置。
如何自定义注册字段?
修改 signUp
调用时传入的数据:
await signUp.email({
email: "user@example.com",
password: "password123",
name: "张三",
// 自定义字段需要在 user 表中添加对应列
});
如何发送邮箱验证链接?
Better Auth 内置邮箱验证功能,需要配置 verification
插件:
import { verification } from "better-auth/plugins";
export const auth = betterAuth({
// ...
plugins: [
verification({
sendEmail: async (user, url) => {
// 发送验证邮件
await sendEmail({
to: user.email,
subject: "验证你的邮箱",
html: `点击链接验证: <a href="${url}">${url}</a>`,
});
},
}),
],
});
如何实现密码重置?
Better Auth 支持密码重置,需要配置 resetPassword
插件:
import { resetPassword } from "better-auth/plugins";
export const auth = betterAuth({
// ...
plugins: [
resetPassword({
sendResetPasswordEmail: async (user, url) => {
// 发送密码重置邮件
await sendEmail({
to: user.email,
subject: "重置你的密码",
html: `点击链接重置密码: <a href="${url}">${url}</a>`,
});
},
}),
],
});
如何禁用 Google OAuth?
从 .env.local
中删除 AUTH_GOOGLE_ID
和 AUTH_GOOGLE_SECRET
,并从 lib/auth.ts
中移除 socialProviders
配置。
如何添加其他 OAuth 提供商?
Better Auth 支持多种 OAuth 提供商,在 lib/auth.ts
中添加:
socialProviders: {
google: { /* ... */ },
github: {
clientId: process.env.AUTH_GITHUB_ID!,
clientSecret: process.env.AUTH_GITHUB_SECRET!,
},
facebook: {
clientId: process.env.AUTH_FACEBOOK_ID!,
clientSecret: process.env.AUTH_FACEBOOK_SECRET!,
},
},
会话是否支持跨域?
是的,通过 trustedOrigins
配置信任的源即可支持跨域。
如何查看所有登录会话?
查询 session
表:
import { db } from "@/lib/db";
import { session } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
const userSessions = await db
.select()
.from(session)
.where(eq(session.userId, userId));
如何强制用户登出所有设备?
删除用户的所有会话:
import { db } from "@/lib/db";
import { session } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
await db.delete(session).where(eq(session.userId, userId));