Waku 集成
将 Better Auth 集成到 Waku。
Better Auth 可以轻松地与 Waku 集成。在开始之前,请确保您已经配置了 Better Auth 实例。如果还没有,请查看 安装指南。
创建 auth 实例
在您的应用程序中创建一个名为 auth.ts 的文件。导入 Better Auth 并创建您的实例。
确保使用变量名 auth 或作为 default 导出实例。
import { betterAuth } from "better-auth"
export const auth = betterAuth({
database: {
provider: "postgres", //根据您的数据库提供者修改
url: process.env.DATABASE_URL, // 数据库路径或连接字符串
}
})创建 API 路由
我们需要将处理器挂载到 API 路由。为 Waku 的文件系统路由在 src/pages/api/auth 创建一个目录。在 src/pages/api/auth 目录中创建一个捕获所有路由文件 [...route].ts,并添加以下代码:
import { auth } from "../../../../auth" // 根据需要调整路径
export const GET = async (request: Request): Promise<Response> => {
return auth.handler(request)
}
export const POST = async (request: Request): Promise<Response> => {
return auth.handler(request)
}您可以在 better-auth 配置中更改路径,但建议保持为 src/pages/_api/api/auth/[...route].ts
创建客户端
创建客户端实例。这里我们在 lib/ 目录下创建 auth-client.ts 文件。
import { createAuthClient } from "better-auth/react" // 确保从 better-auth/react 导入
export const authClient = createAuthClient({
// 可在这里传入客户端配置
})
export type Session = typeof authClient.$Infer.Session // 可以从 authClient 推断 Typescript 类型创建客户端后,您可以用它来注册、登录和执行其他操作。 部分操作是响应式的。客户端使用 nano-store 存储状态,并在状态变化时重新渲染组件。
客户端还使用 better-fetch 来发起请求,您可以将 fetch 配置传给客户端。
RSC 和服务器操作
从 auth 实例导出的 api 对象包含了所有可在服务器端执行的操作。Better Auth 内部创建的每个端点都可以作为函数被调用,包括插件端点。
示例:在服务器操作中获取 Session
"use server" // Waku 当前仅支持文件级 "use server"
import { auth } from "./auth"
import { unstable_getHeaders as getHeaders } from "waku/server"
export const someAuthenticatedAction = async () => {
"use server"
const headers = getHeaders()
const session = await auth.api.getSession({
headers,
})
};示例:在 RSC 中获取 Session
import { auth } from "../auth"
import { unstable_getHeaders as getHeaders } from "waku/server"
export async function ServerComponent() {
const headers = getHeaders()
const session = await auth.api.getSession({
headers,
})
if(!session) {
return <div>未认证</div>
}
return (
<div>
<h1>欢迎 {session.user.name}</h1>
</div>
)
}服务器操作中的 Cookie
当您在服务器操作中调用需要设置 Cookie 的函数(如 signInEmail 或 signUpEmail)时,Cookie 不会被设置。
我们可以创建一个插件,与中间件配合以设置 Cookie。
import { betterAuth } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";
import { unstable_getContextData as getContextData } from "waku/server"
export const auth = betterAuth({
//...您的配置
plugins: [wakuCookies()] // 确保这是数组中的最后一个插件
})
function wakuCookies() {
return {
id: "waku-cookies",
hooks: {
after: [
{
matcher(ctx) {
return true;
},
handler: createAuthMiddleware(async (ctx) => {
const returned = ctx.context.responseHeaders;
if ("_flag" in ctx && ctx._flag === "router") {
return;
}
if (returned instanceof Headers) {
const setCookieHeader = returned?.get("set-cookie");
if (!setCookieHeader) return;
const contextData = getContextData();
contextData.betterAuthSetCookie = setCookieHeader;
}
}),
},
],
},
} satisfies BetterAuthPlugin;
}下面展示了用于将 contextData.betterAuthSetCookie 添加到响应的中间件。
现在,当您调用设置 Cookie 的函数时,它们会被自动设置。
"use server";
import { auth } from "../auth"
const signIn = async () => {
await auth.api.signInEmail({
body: {
email: "[email protected]",
password: "password",
}
})
}中间件
在 Waku 中间件中,建议只检查会话 Cookie 是否存在以处理重定向。这避免了通过 API 或数据库调用阻塞请求。
您可以使用 Better Auth 的 getSessionCookie 辅助函数:
getSessionCookie() 函数不会自动引用在 auth.ts 中指定的 auth 配置。因此,如果您自定义了 Cookie 名称或前缀,需要确保 getSessionCookie() 中的配置与 auth.ts 中定义的配置匹配。
import type { MiddlewareHandler } from "hono"
import { getSession } from "../auth"
import { getSessionCookie } from "better-auth/cookies"
import type { MiddlewareHandler } from "hono";
import { unstable_getContextData as getContextData } from "waku/server";
const authMiddleware: () => MiddlewareHandler = () => {
return async (c, next) => {
const reqUrl = new URL(c.req.url);
const sessionCookie = getSessionCookie(c.req.raw);
// 这不是安全的!
// 这是建议的乐观重定向用户的方法
// 推荐在每个页面/路由中处理认证检查
if (
!sessionCookie &&
reqUrl.pathname !== "/" &&
!reqUrl.pathname.startsWith("/api")
) {
if (!reqUrl.pathname.endsWith(".txt")) {
// 当前 RSC 请求以 .txt 结尾,且不处理重定向响应
// 需要在 React Flight 流里编码重定向
// Waku 中有部分功能可在服务器组件中实现
// 但不能在中间件中实现。
return c.redirect("/", 302);
}
}
// TODO 可根据 c.req.url 优化,避免每次请求都执行
// 或跳过此处启动 Promise,只在服务器组件与函数调用时执行
getSession();
await next();
const contextData = getContextData();
const betterAuthSetCookie = contextData.betterAuthSetCookie as
| string
| undefined;
if (betterAuthSetCookie) {
c.header("set-cookie", betterAuthSetCookie, { append: true });
}
};
};
export default authMiddleware;安全警告: getSessionCookie 函数只检查
会话 Cookie 是否存在;它不会对其进行验证。仅依赖此检查来保证安全是危险的,因为任何人都可以手动创建
Cookie 来绕过它。对于任何受保护的操作或页面,您都必须在服务器端验证会话。
如果您有自定义 Cookie 名称或前缀,可以将其传递给 getSessionCookie 函数。
const sessionCookie = getSessionCookie(request, {
cookieName: "my_session_cookie",
cookiePrefix: "my_prefix"
})或者,您也可以使用 getCookieCache 辅助函数从 Cookie 缓存获取会话对象。
import { getCookieCache } from "better-auth/cookies"
const authMiddleware: () => MiddlewareHandler = () => {
return async (c, next) => {
const reqUrl = new URL(c.req.url);
const session = await getCookieCache(c.req.raw)
if (!session && reqUrl.pathname !== "/") {
if (!reqUrl.pathname.endsWith(".txt")) {
ctx.res.status = 302
ctx.res.headers = {
Location: new URL("/", reqUrl).toString(),
}
}
}
}
await next();
}
}
export default authMiddleware;如果您将中间件文件放在 ./src/middleware,它会被 Waku 默认服务器适配器自动加载。
如何在每个页面/路由处理中进行认证检查
本示例中,我们在服务器组件中使用 auth.api.getSession 函数获取会话对象,
然后检查会话是否有效。如果无效,则重定向用户到登录页。
Waku 提供了 getContext 获取请求头,和 getContextData() 用于每次请求存储数据。我们可以利用它避免同一请求中多次获取会话。
import { unstable_getContext as getContext, unstable_getContextData as getContextData } from "waku/server";
// 以上用于创建服务器端 auth 配置的代码
// export const auth = ...
export function getSession(): Promise<Session | null> {
const contextData = getContextData();
const ctx = getContext();
const existingSessionPromise = contextData.sessionPromise as
| Promise<Session | null>
| undefined;
if (existingSessionPromise) {
return existingSessionPromise;
}
const sessionPromise = auth.api.getSession({
headers: new Headers(ctx.req.headers),
});
contextData.sessionPromise = sessionPromise;
return sessionPromise;
}import { getSession } from "../auth";
import { unstable_redirect as redirect } from 'waku/router/server';
export default async function DashboardPage() {
const session = await getSession()
if (!session) {
redirect("/sign-in")
}
return (
<div>
<h1>欢迎 {session.user.name}</h1>
</div>
)
}示例用法
注册
"use client"
import { useState } from "react"
import { authClient } from "../lib/auth-client"
export default function SignUp() {
const [email, setEmail] = useState("")
const [name, setName] = useState("")
const [password, setPassword] = useState("")
const signUp = async () => {
await authClient.signUp.email(
{
email,
password,
name,
},
{
onRequest: (ctx) => {
// 显示加载状态
},
onSuccess: (ctx) => {
// 重定向到首页
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
<div>
<h2>
注册
</h2>
<form
onSubmit={signUp}
>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="姓名"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="密码"
/>
<button
type="submit"
>
注册
</button>
</form>
</div>
)
}登录
"use client"
import { useState } from "react"
import { authClient } from "../lib/auth-client"
export default function SignIn() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const signIn = async () => {
await authClient.signIn.email(
{
email,
password,
},
{
onRequest: (ctx) => {
// 显示加载状态
},
onSuccess: (ctx) => {
// 重定向到首页
},
onError: (ctx) => {
alert(ctx.error)
},
},
)
}
return (
<div>
<h2>
登录
</h2>
<form onSubmit={signIn}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="submit"
>
登录
</button>
</form>
</div>
)
}