起始项目使用 syntax 团队成员 CJ 的 hono-open-api-starter。你可以去 GitHub 去下载源码和我一起做。
这里只演示使用 email 登录注册的功能,具体功能你可以去 Better Auth 文档 查看
为什么选择 Better Auth
我们看它官方的说法:
Better Auth 是一个与框架无关的 TypeScript 身份验证和授权框架。它提供了一套全面的开箱即用功能,并包含一个插件生态系统,简化了高级功能的添加。无论您需要双因素身份验证 (2FA)、多租户、多会话支持,还是像单点登录 (SSO) 这样的企业级功能,它都能让您专注于构建应用程序,而无需重复造轮子。
而且它在 GitHub 上很活跃,更新速度也蛮快。
Better Auth 的集成
对 hono-open-api-starter 项目的改造
我这里使用 postgresql 数据库,所以需要安装一些依赖
shell
pnpm add drizzle-orm pg dotenv
pnpm add -D drizzle-kit tsx @types/pg
相应的 db/index.ts
需要改造
ts
import env from "@/helper/env";
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema/index";
import "dotenv/config";
const db = drizzle({
connection: {
connectionString: env.DATABASE_URL,
},
schema,
});
export { db };
drizzle.config.ts
文件改动
ts
import { defineConfig } from "drizzle-kit";
import env from "./src/helper/env";
import "dotenv/config";
export default defineConfig({
out: "./src/db/migrations",
schema: "./src/db/schema/**.ts",
dialect: "postgresql",
dbCredentials: {
url: env.DATABASE_URL,
},
});
安装 Better Auth
bash
pnpm add better-auth
配置 .env 文件
.env
BETTER_AUTH_SECRET= # 可以在文档中生成一个随机字符串使用
BETTER_AUTH_URL=http://localhost:9999 # 后端地址
DOMAIN=localhost # 项目启动域名
FRONT_END_URL=http://localhost:5173 # 前端地址
创建 auth.ts
文件
ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, openAPI, organization } from "better-auth/plugins";
export const auth = betterAuth({
database: drizzleAdapter(db, {
// 这里我使用了 pg 数据库,所以这里填 pg
provider: "pg",
}),
// openAPI 插件可以让我们在项目启动后使用 项目地址 + /api/auth/reference 访问其提供的 api
// 这里我还启用了 admin 和 organization 插件
plugins: [openAPI(), admin(), organization()],
})
生成相关 schema
shell
npx @better-auth/cli generate
生成的 schema 会在项目根目录中,我们将其移至 db/schema/
文件夹中
使用 drizzle 相关命令来迁移数据库改动
pnpm drizzle-kit generate
pnpm drizzle-kit migrate
完善 auth.ts 文件
ts
import type { AppOpenAPI } from "@/lib/types";
import { db } from "@/db/index";
import env from "@/helper/env";
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, openAPI, organization } from "better-auth/plugins";
export const auth = betterAuth({
database: drizzleAdapter(db, {
// 这里我使用了 pg 数据库,所以这里填 pg
provider: "pg",
}),
// 这里配置是使用 邮箱和密码 登录注册
emailAndPassword: {
enabled: true,
},
// openAPI 插件可以让我们在项目启动后使用 项目地址 + /api/auth/reference 访问其提供的 api
// 这里我还启用了 admin 和 organization 插件
plugins: [openAPI(), admin(), organization()],
// 由于我们使用的是前后端分离的方式,所以我们需要配置 trustedOrigins
trustedOrigins: [env.BETTER_AUTH_URL, env.FRONT_END_URL],
// 这里是需要配置 cookie 的同域名下共享
advanced: {
crossSubDomainCookies: {
enabled: true,
domain: env.DOMAIN,
},
defaultCookieAttributes: {
secure: true,
httpOnly: true,
sameSite: "none",
partitioned: true,
},
},
})
// 挂载相关 api
export function authRoutes(app: AppOpenAPI) {
app.all("/api/auth/*", c => auth.handler(c.req.raw));
}
登录中间件,使用后可以在后续的请求中在 context 中访问 user 和 session 信息
ts
import type { AppOpenAPI } from "@/lib/types";
import { auth } from "@/auth";
import env from "@/helper/env";
import { cors } from "hono/cors";
export function authMiddleware(app: AppOpenAPI) {
app.use(
"/api/auth/*", // or replace with "*" to enable cors for all routes
cors({
origin: env.FRONT_END_URL, // replace with your origin
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.use("*", async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) {
c.set("user", null);
c.set("session", null);
return next();
}
c.set("user", session.user);
c.set("session", session.session);
return next();
});
}
对 AppOpenAPI
的类型做补充
解决 ts 类型报错的问题
ts
import type { auth } from "@/auth";
import type { OpenAPIHono, RouteConfig, RouteHandler } from "@hono/zod-openapi";
import type { PinoLogger } from "hono-pino";
export interface AppBindings {
Variables: {
logger: PinoLogger
user: typeof auth.$Infer.Session.user | null
session: typeof auth.$Infer.Session.session | null
}
}
export type AppOpenAPI = OpenAPIHono<AppBindings>;
export type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>;
在主路由中使用 Better Auth
ts
import { authRoutes } from "@/auth";
import env from "@/helper/env";
import configureOpenAPI from "@/lib/configure-open-api";
import createApp from "@/lib/create-app";
import { authMiddleware } from "@/middleware/sign-in";
import index from "@/routes/index.route";
import tasks from "@/routes/tasks/tasks.index";
import { cors } from "hono/cors";
const app = createApp();
configureOpenAPI(app);
authMiddleware(app);
authRoutes(app);
app.use(
"*",
cors({ origin: env.FRONT_END_URL, credentials: true }),
);
const routes = [
index,
tasks,
] as const;
routes.forEach(route => app.route("/api", route));
export type AppType = typeof routes[number];
export default app;
前端使用
前端这里我使用 vite 搭建一个简单的项目来进行演示
shell
pnpm create vite --template vue-ts
安装 Better Auth
shell
pnpm add better-auth
使用
使用 Better Auth 提供的工具来访问它提供的接口是很方便的。我们需要在 /src/
下创建一个 auth-client
ts
import { adminClient, organizationClient } from "better-auth/client/plugins"
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient({
baseURL: "http://localhost:9999",
plugins: [
// 这里如果后端有一些better-auth插件,这里也要引入对应的前端插件
organizationClient(),
adminClient()
]
})
创建好后,我们就可以在页面中使用它了
vue
<script setup lang="ts">
import { ref } from "vue"
import { authClient } from "./lib/auth-client"
const loginInfo = ref()
const loginError = ref()
// 登录
async function signIn() {
const { data, error } = await authClient.signIn.email({
email: "[email protected]",
password: "admin888",
})
loginInfo.value = data
loginError.value = error
}
// 注册
async function signUp() {
const { data, error } = await authClient.signUp.email({
email: "[email protected]",
password: "admin888",
name: "test user",
})
loginInfo.value = data
loginError.value = error
}
const session = await authClient.getSession()
// 登录用户创建组织
async function createOrganization() {
if (!authClient.getSession()) {
alert("Please sign in first")
return
}
await authClient.organization.create({
name: "test organization",
slug: "test-organization",
})
}
</script>
<template>
<button @click="signIn">
登录
</button>
<button @click="signUp">
注册
</button>
<button @click="createOrganization">
创建组织
</button>
loginInfo:{{ loginInfo }} loginError:{{ loginError }}
<hr>
{{ session }}
</template>
可能遇到的问题
跨域
因为是前后端分离项目,所以在使用时需要配置跨域相关内容,以上文章都有说明。跨域问题和 cookie 相关配置需要谨慎,最好限制指定域名跨域。
❌前后端不同源时,前端使用 better-auth 需要配置(不推荐)
这是我开始使用的一种方式,后端如果没有配置好跨域和 cookie 相关内容,会导致登录后使用 Better Auth clieht 相关的获取 session 的方法无法拿到对应的 session
ts
import { createAuthClient } from "better-auth/vue"
export const authClient = createAuthClient({
/** The base URL of the server (optional if you're using the same domain) */
baseURL: "http://localhost:9999",
fetchOptions: {
credentials: 'omit'
}
})