Astro 项目集成 Better Auth 指南(使用drizzle+PG)

本文档说明如何在 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!, // 注意:这里使用 process.env, 引入dotenv读取env文件
},
});

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";

// ⚠️ 关键配置:必须传入 schema
export const db = drizzle({
connection: import.meta.env.DATABASE_URL,
schema, // 不写这个 Better Auth 会报错:"找不到 user 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({
// 这两个如果是用process.env可以读取就不用写,但因为是astro,所以用import.meta.env读取
// 会话加密密钥
secret: import.meta.env.BETTER_AUTH_SECRET,

// 应用基础 URL
baseURL: import.meta.env.BETTER_AUTH_BASE_URL,

// 启用邮箱密码认证
emailAndPassword: {
enabled: true,
},

// 使用 Drizzle 适配器连接数据库
database: drizzleAdapter(db, {
provider: "pg", // PostgreSQL
}),
});

配置说明

  • secret: 用于加密会话 token,必须是至少 32 字符的随机字符串
  • baseURL: 应用的基础 URL,用于生成回调链接
  • emailAndPassword.enabled: 启用邮箱密码认证方式
  • database: 使用 drizzleAdapter 连接数据库,传入之前初始化的 db 实例

步骤 6:生成数据库 Schema

现在运行 Better Auth CLI 工具生成所需的数据库 schema:

1
2
3
4
5
# 推荐使用 pnpm
pnpm dlx @better-auth/cli generate

# 或使用 npx(我用这个无法生成...找到issue用pnpm就可以 很奇怪)
npx @better-auth/cli generate

⚠️ 重要npx @better-auth/cli generate 可能无法正常工作,强烈推荐使用 pnpm dlx

这个命令会:

  1. 读取你的 auth.ts 配置
  2. 分析你启用的插件和认证方式
  3. 自动生成 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
# 1. 生成迁移文件(基于 schema.ts)
pnpm db:generate

# 2. 应用迁移到数据库
pnpm db:migrate

执行流程

  1. drizzle-kit generate 读取 src/db/schema.ts,生成 SQL 迁移文件到 drizzle/ 目录
  2. 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";

// 禁用预渲染,确保 API 路由动态处理
export const prerender = false;

// 处理所有 HTTP 方法(GET、POST、PUT、DELETE 等)
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) => {
// ⚠️ 重要:跳过预渲染页面,避免dev和构建时出现警告
if (context.isPrerendered) {
return next();
}

// 从请求头中获取会话信息
const isAuthed = await auth.api.getSession({
headers: context.request.headers,
});

console.log("middleware - isAuthed", isAuthed);

// 将用户和会话信息注入到 context.locals
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 脚本 配置了 dotenvdrizzle.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, // Better Auth 需要访问表
});

// ❌ 错误
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;
---

参考资源