起始项目使用 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 数据库,所以需要安装一些依赖
pnpm add drizzle-orm pg dotenv pnpm add -D drizzle-kit tsx @types/pg
相应的 db/index.ts
需要改造
import { drizzle } from "drizzle-orm/node-postgres"; import env from "@/helper/env"; import * as schema from "./schema/index"; import "dotenv/config"; const db = drizzle({ connection: { connectionString: env.DATABASE_URL, }, schema, }); export { db };
drizzle.config.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
pnpm add better-auth
配置 .env 文件
BETTER_AUTH_SECRET= # 可以在文档中生成一个随机字符串使用 BETTER_AUTH_URL=http://localhost:9999 # 后端地址 DOMAIN=localhost # 项目启动域名 FRONT_END_URL=http://localhost:5173 # 前端地址
创建 auth.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
npx @better-auth/cli generate
生成的 schema 会在项目根目录中,我们将其移至 db/schema/
文件夹中
使用 drizzle 相关命令来迁移数据库改动
pnpm drizzle-kit generate pnpm drizzle-kit migrate
完善 auth.ts 文件
import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { admin, openAPI, organization } from "better-auth/plugins"; import type { AppOpenAPI } from "@/lib/types"; import { db } from "@/db/index"; import env from "@/helper/env"; 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 信息
import { cors } from "hono/cors"; import type { AppOpenAPI } from "@/lib/types"; import { auth } from "@/auth"; import env from "@/helper/env"; 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 类型报错的问题
import type { OpenAPIHono, RouteConfig, RouteHandler } from "@hono/zod-openapi"; import type { PinoLogger } from "hono-pino"; import type { auth } from "@/auth"; 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
import { cors } from "hono/cors"; 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"; 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 搭建一个简单的项目来进行演示
pnpm create vite --template vue-ts
安装 Better Auth
pnpm add better-auth
使用
使用 Better Auth 提供的工具来访问它提供的接口是很方便的。我们需要在 /src/
下创建一个 auth-client
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() ] })
创建好后,我们就可以在页面中使用它了
<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: "test@example.com", password: "admin888", }) loginInfo.value = data loginError.value = error } // 注册 async function signUp() { const { data, error } = await authClient.signUp.email({ email: "test@example.com", 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
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' } })