本文档说明如何在 Astro 项目中集成 Better Auth,使用 Drizzle 作为数据库适配器,PostgreSQL 作为数据库。
emmmm,文档是在实践完之后写的,不是边实践边写的,可能会有所遗漏,但是扫了几遍源码,应该八九不离十。
注意这个需要 server 支持,所以需要 server adapter,比如 node 的@astrojs/node。
我使用的是默认 static,需要后端运行时渲染的地方用export const prerender = false;,如果你的 output 设置为了 server,那么就不需要 prerender=false。
📖 官方文档参考:
步骤 1:安装依赖
首先,安装 Better Auth 及相关依赖:
1 2 3 4 5
| pnpm add better-auth drizzle-orm pg dotenv
pnpm add -D drizzle-kit tsx @types/pg
|
依赖说明:
better-auth: 认证核心库
drizzle-orm: ORM 库,用于数据库操作
pg: PostgreSQL 驱动
drizzle-kit: Drizzle 开发工具(用于数据库迁移)
dotenv: 环境变量加载(drizzle.config.ts 需要)
步骤 2:配置环境变量
在项目根目录创建 .env 文件:
1 2 3 4 5 6 7 8
| # 数据库连接字符串 DATABASE_URL="postgresql://USER:[email protected]:PORT/DATABASE"
# Better Auth 密钥(用于加密会话,至少 32 字符) BETTER_AUTH_SECRET="your-secret-key-here"
# Better Auth 基础 URL BETTER_AUTH_BASE_URL="http://localhost:4321"
|
💡 生成密钥:可以使用 openssl rand -base64 32 生成随机密钥
⚠️ Astro 环境变量注意事项:
Astro 默认不注入 process.env,必须使用 import.meta.env 访问环境变量。
在 src/env.d.ts 中添加类型定义:
1 2 3 4 5 6 7 8 9 10 11 12
| interface ImportMetaEnv { readonly BETTER_AUTH_SECRET: string; readonly BETTER_AUTH_BASE_URL: string; readonly DATABASE_URL: string; }
declare namespace App { interface Locals { user: import("better-auth").User | null; session: import("better-auth").Session | null; } }
|
步骤 3:配置 Drizzle
创建 drizzle.config.ts(用于数据库迁移工具):
1 2 3 4 5 6 7 8 9 10 11
| import "dotenv/config"; import { defineConfig } from "drizzle-kit";
export default defineConfig({ out: "./drizzle", schema: "./src/db/schema.ts", dialect: "postgresql", dbCredentials: { url: process.env.DATABASE_URL!, }, });
|
在 package.json 中添加快捷命令:
1 2 3 4 5 6 7
| { "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:pull": "drizzle-kit pull" } }
|
步骤 4:创建数据库连接
创建 src/db/database.ts:
1 2 3 4 5 6 7 8
| import { drizzle } from "drizzle-orm/node-postgres"; import * as schema from "./schema";
export const db = drizzle({ connection: import.meta.env.DATABASE_URL, schema, });
|
🚨 重要警告:
必须传入 schema 参数!如果不传入,Better Auth 会报错:找不到 user schema。
步骤 5:创建 Better Auth 实例
创建 src/lib/auth.ts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "@/db/database";
export const auth = betterAuth({ secret: import.meta.env.BETTER_AUTH_SECRET,
baseURL: import.meta.env.BETTER_AUTH_BASE_URL,
emailAndPassword: { enabled: true, },
database: drizzleAdapter(db, { provider: "pg", }), });
|
配置说明:
secret: 用于加密会话 token,必须是至少 32 字符的随机字符串
baseURL: 应用的基础 URL,用于生成回调链接
emailAndPassword.enabled: 启用邮箱密码认证方式
database: 使用 drizzleAdapter 连接数据库,传入之前初始化的 db 实例
步骤 6:生成数据库 Schema
现在运行 Better Auth CLI 工具生成所需的数据库 schema:
1 2 3 4 5
| pnpm dlx @better-auth/cli generate
npx @better-auth/cli generate
|
⚠️ 重要:npx @better-auth/cli generate 可能无法正常工作,强烈推荐使用 pnpm dlx。
这个命令会:
- 读取你的
auth.ts 配置
- 分析你启用的插件和认证方式
- 自动生成
src/db/schema.ts 文件,包含所有必需的表定义
生成的 schema 文件会包含以下核心表:
user: 用户基本信息
session: 会话管理
account: 密码和 OAuth 凭证
verification: 邮件验证和密码重置
生成的 schema 示例(自动生成,无需手写):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm/relations";
export const user = pgTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() .$onUpdate(() => new Date()) .notNull(), });
export const session = pgTable( "session", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .$onUpdate(() => new Date()) .notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }, (table) => [index("session_userId_idx").on(table.userId)] );
|
步骤 7:应用数据库迁移
生成 schema 后,使用 Drizzle Kit 创建并应用数据库迁移:
1 2 3 4 5
| pnpm db:generate
pnpm db:migrate
|
执行流程:
drizzle-kit generate 读取 src/db/schema.ts,生成 SQL 迁移文件到 drizzle/ 目录
drizzle-kit migrate 执行迁移文件,在数据库中创建表
步骤 8:配置 API 路由
创建 src/pages/api/auth/[...all].ts(catch-all 路由):
1 2 3 4 5 6 7 8 9 10
| import { auth } from "@/lib/auth"; import type { APIRoute } from "astro";
export const prerender = false;
export const ALL: APIRoute = async (ctx) => { return auth.handler(ctx.request); };
|
路由说明:
[...all] 会捕获所有 /api/auth/* 请求
- Better Auth 自动处理以下端点:
POST /api/auth/sign-in/email - 邮箱登录
POST /api/auth/sign-up/email - 邮箱注册
POST /api/auth/sign-out - 登出
GET /api/auth/session - 获取当前会话
- 等等…
步骤 9:配置中间件
创建 src/middleware.ts,在每个请求中检查用户会话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { auth } from "@/lib/auth"; import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware(async (context, next) => { if (context.isPrerendered) { return next(); }
const isAuthed = await auth.api.getSession({ headers: context.request.headers, });
console.log("middleware - isAuthed", isAuthed);
if (isAuthed) { context.locals.user = isAuthed.user; context.locals.session = isAuthed.session; } else { context.locals.user = null; context.locals.session = null; }
return next(); });
|
⚠️ 中间件注意事项:
- 必须过滤预渲染页面:
if (context.isPrerendered) return next();
- 如果不过滤,构建时会对预渲染页面尝试获取会话,导致警告或错误
- 预渲染页面在构建时生成,无法访问动态会话信息
中间件功能:
- 为每个动态请求验证用户会话(跳过预渲染页面)
- 将用户信息注入到
Astro.locals,方便页面访问
- 在
.astro 文件中可以通过 Astro.locals.user 访问当前用户
步骤 10:创建客户端实例
创建 src/lib/auth-client.ts(用于 React 组件):
1 2 3
| import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
|
💡 提示:如果客户端和服务端不在同一域名,需要传入 baseURL 参数:
1 2 3
| export const authClient = createAuthClient({ baseURL: "http://localhost:4321", });
|
步骤 11:页面集成
登录页面
创建 src/pages/login.astro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| --- import BaseLayout from "@/layouts/BaseLayout.astro"; import LoginForm from "@/components/auth/login-form";
export const prerender = false;
// 如果已登录,重定向到首页 if (Astro.locals.user) { return Astro.redirect("/"); } ---
<BaseLayout pageTitle="登录"> <LoginForm client:load /> </BaseLayout>
|
创建 src/components/auth/login-form.tsx(React 组件):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| import { authClient } from "@/lib/auth-client"; import { useState } from "react";
export default function LoginForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault();
const { data, error } = await authClient.signIn.email({ email, password, });
if (error) { console.error("登录失败:", error); return; }
window.location.href = "/"; };
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 /> <button type="submit">登录</button> </form> ); }
|
注册页面
创建 src/pages/register.astro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| --- import BaseLayout from "@/layouts/BaseLayout.astro"; import RegisterForm from "@/components/auth/register-form";
export const prerender = false;
if (Astro.locals.user) { return Astro.redirect("/"); } ---
<BaseLayout pageTitle="注册"> <RegisterForm client:load /> </BaseLayout>
|
创建 src/components/auth/register-form.tsx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import { authClient } from "@/lib/auth-client"; import { useState } from "react";
export default function RegisterForm() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [name, setName] = useState("");
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault();
const { data, error } = await authClient.signUp.email({ email, password, name, });
if (error) { console.error("注册失败:", error); return; }
window.location.href = "/"; };
return ( <form onSubmit={handleSubmit}> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="用户名" required /> <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 /> <button type="submit">注册</button> </form> ); }
|
登出功能
创建 src/components/auth/logout-button.tsx:
1 2 3 4 5 6 7 8 9 10
| import { authClient } from "@/lib/auth-client";
export default function LogoutButton() { const handleLogout = async () => { await authClient.signOut(); window.location.href = "/login"; };
return <button onClick={handleLogout}>登出</button>; }
|
在页面中使用:
1 2 3 4 5 6 7 8 9 10
| --- import LogoutButton from "@/components/auth/logout-button"; ---
{Astro.locals.user && ( <div> <p>欢迎, {Astro.locals.user.name}!</p> <LogoutButton client:load /> </div> )}
|
路由保护
保护需要登录才能访问的页面:
1 2 3 4 5 6 7 8 9 10 11 12
| --- // 必须禁用预渲染 export const prerender = false;
// 检查用户是否登录 if (!Astro.locals.user) { return Astro.redirect("/login"); } ---
<h1>受保护的页面</h1> <p>只有登录用户才能看到这个页面</p>
|
认证流程说明
用户注册流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 1. 用户访问 /register ↓ 2. 中间件检查会话(middleware.ts) ↓ 3. 如果未登录,显示注册表单 ↓ 4. 用户填写表单并提交 ↓ 5. RegisterForm 调用 authClient.signUp.email() ↓ 6. 请求发往 POST /api/auth/sign-up/email ↓ 7. auth.handler() 处理请求 ↓ 8. Better Auth 使用 Drizzle 适配器: - 在 user 表创建用户记录 - 在 account 表创建密码凭证 - 在 session 表创建会话 ↓ 9. 返回会话信息 ↓ 10. 客户端保存会话,重定向到首页
|
用户登录流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 1. 用户访问 /login ↓ 2. 中间件检查会话 ↓ 3. 如果未登录,显示登录表单 ↓ 4. 用户填写表单并提交 ↓ 5. LoginForm 调用 authClient.signIn.email() ↓ 6. 请求发往 POST /api/auth/sign-in/email ↓ 7. Better Auth 验证密码并创建会话 ↓ 8. 返回会话信息 ↓ 9. 客户端保存会话,重定向到首页
|
会话验证流程
1 2 3 4 5 6 7 8 9 10
| 1. 用户访问任意页面 ↓ 2. 中间件 (middleware.ts) 拦截请求 ↓ 3. 调用 auth.api.getSession() 验证会话 ↓ 4. 如果会话有效: - 将 user 和 session 注入到 Astro.locals ↓ 5. 页面可以通过 Astro.locals.user 访问当前用户
|
实践注意事项
1. 环境变量使用区分
- Astro 运行时(
auth.ts, database.ts):使用 import.meta.env
- Node.js 脚本 配置了 dotenv(
drizzle.config.ts):使用 process.env
2. Drizzle 必须传入 Schema
1 2 3 4 5 6 7 8 9 10
| export const db = drizzle({ connection: import.meta.env.DATABASE_URL, schema, });
export const db = drizzle({ connection: import.meta.env.DATABASE_URL, });
|
如果不传入 schema,Better Auth 会报错:**”找不到 user schema”**。
3. 中间件必须过滤预渲染页面
1 2 3
| if (context.isPrerendered) { return next(); }
|
如果不过滤,构建时会尝试获取会话,导致警告或错误。
4. 动态页面必须禁用预渲染
所有使用认证的页面必须设置:
1 2 3
| --- export const prerender = false; ---
|
参考资源