hono 集成 Better Auth

起始项目使用 syntax 团队成员 CJhono-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'
    }
})
相关推荐
夕水12 分钟前
这个提升效率宝藏级工具一定要收藏使用
前端·javascript·trae
会飞的鱼先生26 分钟前
vue3 内置组件KeepAlive的使用
前端·javascript·vue.js
前端大白话1 小时前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
Rabbb1 小时前
C# JSON属性排序、比较 Newtonsoft.Json
后端
蓝易云1 小时前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
一千柯橘1 小时前
Nestjs 解决 request entity too large
javascript·后端
举个栗子dhy2 小时前
如何处理动态地址栏参数,以及Object.entries() 、Object.fromEntries()和URLSearchParams.entries()使用
javascript
宁静_致远2 小时前
React Native 技术栈:基于 macOS 开发平台的 iOS 应用开发指南
前端·javascript·react native
H5开发新纪元2 小时前
VS Code 插件开发实战:代码截图工具
javascript·visual studio code
userkang2 小时前
消失的前后端,崛起的智能体
前端·人工智能·后端·ai·硬件工程