如何使用 Lucia 为 React/Next.js 应用添加身份验证 - 分步指南
尽管身份验证是 Web 应用中最常见的功能之一,但实现方式却多种多样,这使得它成为一项非常复杂的任务。在本文中,我将分享我使用 Lucia 的个人经验——Lucia 是一个现代化的、与框架无关的身份验证库,近几个月来,它受到了社区的广泛好评,实至名归。
首先,我将通过一个循序渐进的指南,演示如何在你的 Next.js 应用程序中实现它。这需要编写相当多的代码并进行一些配置,但过程本身非常简单明了。
其次,我们将看看如何使用Wasp仅用几行代码实现同样的功能。Wasp 是一个功能齐全的 React 和 Node.js 全栈框架,它底层使用 Lucia 来实现身份验证。它完全运行在您的基础设施上,并且 100% 开源免费。
为什么是露西亚?
在为应用程序添加身份验证时,有几种流行的解决方案可供选择。例如,Clerk提供付费服务,而NextAuth.js和Lucia都是开源解决方案,Lucia 最近也变得非常流行。
这些工具功能强大,但对于小型项目而言,采用第三方服务可能有些过度——这不仅会增加复杂性,而且还需要关注付费层级。内部解决方案虽然可以保持集中化,但某些上述功能的具体实现则需要开发人员参与。
就我们而言,Lucia 已被证明是一个完美的折衷方案——它不是第三方服务,也不需要专门的基础设施,但它也提供了一个非常坚实的基础,很容易在此基础上进行构建。
现在,让我们深入了解如何使用 Next.js 和 Lucia 实现您自己的身份验证。
步骤 1:设置 Next.js
首先,创建一个新的 Next.js 项目:
npx create-next-app@latest my-nextjs-app
cd my-nextjs-app
npm install
步骤二:安装 Lucia
接下来,安装 Lucia:
npm install lucia
步骤 3:设置身份验证
在你的项目中创建一个auth文件,并添加导入和初始化 Lucia 所需的必要文件。它包含许多针对不同数据库的适配器,你可以在这里查看所有适配器。在本例中,我们将使用 SQLite:
// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
const adapter = new BetterSQLite3Adapter(db); // your adapter
export const lucia = new Lucia(adapter, {
sessionCookie: {
// this sets cookies with super long expiration
// since Next.js doesn't allow Lucia to extend cookie expiration when rendering pages
expires: false,
attributes: {
// set to `true` when using HTTPS
secure: process.env.NODE_ENV === "production"
}
}
});
// To get some good Typescript support, add this!
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
}
}
步骤 4:将用户添加到数据库
现在我们先添加一个数据库文件来存放我们的模式:
// lib/db.ts
import sqlite from "better-sqlite3";
export const db = sqlite("main.db");
db.exec(`CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY,
github_id INTEGER UNIQUE,
username TEXT NOT NULL
)`);
db.exec(`CREATE TABLE IF NOT EXISTS session (
id TEXT NOT NULL PRIMARY KEY,
expires_at INTEGER NOT NULL,
user_id TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id)
)`);
export interface DatabaseUser {
id: string;
username: string;
github_id: number;
}
第五步:实现登录和注册功能
要实现这一点,我们首先需要创建一个 GitHub OAuth 应用。这相对简单,创建完成后,将必要的环境变量和回调 URL 添加到您的应用中即可。您可以参考 GitHub 文档了解具体操作方法。
//.env.local
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
接下来,只需在页面上添加登录和注册功能即可,我们这就快速完成:
// login/page.tsx
import { validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function Page() {
const { user } = await validateRequest();
if (user) {
return redirect("/");
}
return (
<>
<h1>Sign in</h1>
<a href="/login/github">Sign in with GitHub</a>
</>
);
}
添加页面后,我们还需要添加到 GitHub 的登录重定向以及要调用的回调函数。首先,我们添加包含授权 URL 的登录重定向:
// login/github/route.ts
import { generateState } from "arctic";
import { github } from "../../../lib/auth";
import { cookies } from "next/headers";
export async function GET(): Promise<Response> {
const state = generateState();
const url = await github.createAuthorizationURL(state);
cookies().set("github_oauth_state", state, {
path: "/",
secure: process.env.NODE_ENV === "production",
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax"
});
return Response.redirect(url);
}
最后,是回调函数(也就是我们在 GitHub OAuth 中实际添加的部分):
// login/github/callback/route.ts
import { github, lucia } from "@/lib/auth";
import { db } from "@/lib/db";
import { cookies } from "next/headers";
import { OAuth2RequestError } from "arctic";
import { generateId } from "lucia";
import type { DatabaseUser } from "@/lib/db";
export async function GET(request: Request): Promise<Response> {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const storedState = cookies().get("github_oauth_state")?.value ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400
});
}
try {
const tokens = await github.validateAuthorizationCode(code);
const githubUserResponse = await fetch("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${tokens.accessToken}`
}
});
const githubUser: GitHubUser = await githubUserResponse.json();
const existingUser = db.prepare("SELECT * FROM user WHERE github_id = ?").get(githubUser.id) as
| DatabaseUser
| undefined;
if (existingUser) {
const session = await lucia.createSession(existingUser.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}
const userId = generateId(15);
db.prepare("INSERT INTO user (id, github_id, username) VALUES (?, ?, ?)").run(
userId,
githubUser.id,
githubUser.login
);
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
} catch (e) {
if (e instanceof OAuth2RequestError && e.message === "bad_verification_code") {
// invalid code
return new Response(null, {
status: 400
});
}
return new Response(null, {
status: 500
});
}
}
interface GitHubUser {
id: string;
login: string;
}
这里还有一件重要的事情是,现在我们使用的是 GitHub OAuth,但一般来说,这些库包含许多不同的登录提供程序(包括简单的用户名和密码),因此如果您想添加其他提供程序,通常只需选择即可。
// lib/auth.ts
import { Lucia } from "lucia";
import { BetterSqlite3Adapter } from "@lucia-auth/adapter-sqlite";
import { db } from "./db";
import { cookies } from "next/headers";
import { cache } from "react";
import { GitHub } from "arctic";
import type { Session, User } from "lucia";
import type { DatabaseUser } from "./db";
// these two lines here might be important if you have node.js 18 or lower.
// you can check Lucia's documentation in more detail if that's the case
// (https://lucia-auth.com/getting-started/nextjs-app#polyfill)
// import { webcrypto } from "crypto";
// globalThis.crypto = webcrypto as Crypto;
const adapter = new BetterSqlite3Adapter(db, {
user: "user",
session: "session"
});
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production"
}
},
getUserAttributes: (attributes) => {
return {
githubId: attributes.github_id,
username: attributes.username
};
}
});
declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: Omit<DatabaseUser, "id">;
}
}
export const validateRequest = cache(
async (): Promise<{ user: User; session: Session } | { user: null; session: null }> => {
const sessionId = cookies().get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
return {
user: null,
session: null
};
}
const result = await lucia.validateSession(sessionId);
// next.js throws when you attempt to set cookie when rendering page
try {
if (result.session && result.session.fresh) {
const sessionCookie = lucia.createSessionCookie(result.session.id);
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!result.session) {
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
} catch {}
return result;
}
);
export const github = new GitHub(process.env.GITHUB_CLIENT_ID!, process.env.GITHUB_CLIENT_SECRET!);
步骤 6:保护路由
添加完所有使登录正常工作的内容后,我们只需要通过检查身份验证状态来确保路由受到保护——在本例中,这是一个简单的页面,显示用户名、ID 和一个按钮(如果已登录),并重定向到 /login,用户将在该页面上通过表单完成上述登录。
import { lucia, validateRequest } from "@/lib/auth";
import { redirect } from "next/navigation";
import { cookies } from "next/headers";
export default async function Page() {
const { user } = await validateRequest();
if (!user) {
return redirect("/login");
}
return (
<>
<h1>Hi, {user.username}!</h1>
<p>Your user ID is {user.id}.</p>
<form action={logout}>
<button>Sign out</button>
</form>
</>
);
}
async function logout(): Promise<ActionResult> {
"use server";
const { session } = await validateRequest();
if (!session) {
return {
error: "Unauthorized"
};
}
await lucia.invalidateSession(session.id);
const sessionCookie = lucia.createBlankSessionCookie();
cookies().set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
return redirect("/login");
}
interface ActionResult {
error: string | null;
}
小菜一碟,对吧?嗯,其实不然。
让我们回顾一下实现这一目标所必需的步骤:
- 设置您的应用。
- 加入露西亚。
- 设置身份验证。
- 将用户添加到数据库。
- 获取 GitHub OAuth 凭据并配置环境变量。
- 创建一些实用函数。
- 添加登录和注册路由,并使用自定义组件。
- 最后,创建一条受保护的路由。
说实话,当想要快速开发出一些很棒的功能时,反复执行这些步骤,还要调试时不时出现的一些逻辑问题,确实会让人有点沮丧。很快,我们将了解 Wasp 是如何解决这个问题的,并比较 Wasp 的身份验证实现流程究竟有多么简单。
如果您想查看这部分的完整代码,Lucia 提供了一个示例仓库(其中包含了大部分显示的代码),您可以根据需要查看。
黄蜂实施
现在,让我们来看看如何使用 Wasp 🐝 实现同样的功能。虽然 Wasp 仍然在后台使用 Lucia,但它会帮你处理所有繁重的工作,使整个过程更加快捷简便。让我们亲自体验一下它的开发者体验吧。
在我们正式开始之前,如果您更倾向于视觉学习,这里有一个 1 分钟的视频,展示了 Wasp 的身份验证功能。
正如视频所示,Wasp 是一个用于构建应用程序的框架,它利用配置文件简化开发流程。Wasp 可以处理许多重复性任务,让您专注于创建独特的功能。在本教程中,我们还将深入了解 Wasp 配置文件,并了解它如何简化身份验证的设置。
步骤 1:创建 Wasp 项目
curl -sSL https://get.wasp-lang.dev/installer.sh | sh
wasp new my-wasp-app
cd my-wasp-app
步骤 2:将用户实体添加到我们的数据库中
只需在文件app.auth.userEntity中定义实体schema.prisma并运行一些迁移即可:
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
// Add your own fields below
// ...
}
步骤 3:定义身份验证
在 Wasp 主配置中,添加您希望应用程序使用的身份验证提供程序。
//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {}
},
onAuthFailedRedirectTo: "/login"
},
}
之后,只需在终端中运行:
wasp db migrate-dev
步骤 4:获取您的 GitHub OAuth 凭据并运行应用程序
这部分对于两个框架来说类似,您可以参考 GitHub 提供的文档进行操作:创建 OAuth 应用 - GitHub 文档。对于 wasp 应用,回调 URL 如下:
- 在开发过程中:
http://localhost:3001/auth/github/callback - 部署后:
https://your-server-url.com/auth/github/callback
之后,获取你的密钥并将其添加到环境变量文件中:
//.env.server
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
步骤 5:添加路由和页面
现在,我们只需添加一些路由和登录所需的页面——由于 Wasp 已经预置了登录和注册表单,这个过程要简单得多,我们可以直接添加它们:
// main.wasp
route SignupRoute { path: "/signup", to: SignupPage }
page SignupPage {
component: import { SignupPage } from "@src/SignupPage"
}
route LoginRoute { path: "/login", to: LoginPage }
page LoginPage {
component: import { LoginPage } from "@src/LoginPage"
}
// src/LoginPage.jsx
import { Link } from 'react-router-dom'
import { LoginForm } from 'wasp/client/auth'
export const LoginPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<LoginForm />
<br />
<span>
I don't have an account yet (<Link to="/signup">go to signup</Link>).
</span>
</div>
)
}
// src/SignupPage.jsx
import { Link } from 'react-router-dom'
import { SignupForm } from 'wasp/client/auth'
export const SignupPage = () => {
return (
<div style={{ maxWidth: '400px', margin: '0 auto' }}>
<SignupForm />
<br />
<span>
I already have an account (<Link to="/login">go to login</Link>).
</span>
</div>
)
}
最后,要保护路由,只需简单地添加更改即可main.wasp,authRequired: true因此,我们可以像这样添加:
// main.wasp
page MainPage {
component: import Main from "@src/pages/Main",
authRequired: true
}
如果您想更深入地了解这个示例,请访问此仓库:wasp/examples/todo-typescript at release · wasp-lang/wasp (github.com)。
另一个很好的参考资料是他们的文档,您可以在这里找到。文档涵盖了我在这里提到的大部分内容,甚至更多(例如Wasp v0.14 中新增的强大钩子)。
这样容易多了,不是吗?让我们回顾一下我们走到今天这一步所采取的步骤:
- 设置项目。
- 将用户实体添加到数据库中。
- 在 Wasp 主配置中定义身份验证。
- 获取 GitHub OAuth 凭据并配置环境变量。
- 使用预构建的、易于使用的组件,添加登录和注册的路由和页面。
authRequired通过在配置中指定来保护路由。
自定义 Wasp 身份验证
如果您需要对身份验证流程进行更多控制和自定义,Wasp 提供了身份验证钩子,允许您根据应用程序的特定需求定制体验。这些钩子使您能够在身份验证过程的各个阶段执行自定义代码,从而确保您可以实现任何所需的自定义行为。
有关使用 Wasp 的身份验证钩子的更多详细信息,请访问Wasp 文档。
附加章节:使用 Wasp 添加电子邮件/密码登录并自定义身份验证
现在假设我们要添加电子邮件和密码身份验证——以及我们期望遵循此登录方法的所有常用功能(例如重置密码、电子邮件验证等)。
使用 Wasp,我们只需要在 main.wasp 文件中添加几行代码,因此,只需更新 Wasp 配置以包含电子邮件/密码身份验证,即可立即使用!
Wasp 将处理其余部分,包括更新 UI 组件,并确保流畅、安全的身份验证流程。
//main.wasp
app myApp {
wasp: {
version: "^0.14.0"
},
title: "My App",
auth: {
// 1. Specify the User entity
userEntity: User,
methods: {
// 2. Enable Github Auth
gitHub: {},
email: {
// 3. Specify the email from field
fromField: {
name: "My App Postman",
email: "hello@itsme.com"
},
// 4. Specify the email verification and password reset options
emailVerification: {
clientRoute: EmailVerificationRoute, //this route/page should be created
},
passwordReset: {
clientRoute: PasswordResetRoute, //this route/page should be created
},
// Add an emailSender -- Dummy just logs to console for dev purposes
// but there are a ton of supported providers :D
emailSender: {
provider: Dummy,
},
},
},
onAuthFailedRedirectTo: "/login"
},
}
在 Next.js 中使用 Lucia 实现这个功能需要更多的工作,涉及从实际发送电子邮件到生成验证令牌等诸多方面。他们在这里提到了这一点,但再次强调,Wasp's Auth 让整个过程变得简单得多,它为我们处理了许多复杂性,同时还提供了许多其他可直接使用的 UI 组件,以简化 UI 细节(例如VerifyEmailForm,ForgotPasswordForm和ResetPasswordForm)。
关键在于实现相同场景所需的时间和开发者经验的差异。如果你独自使用 Lucia 开发 Next.js 项目,至少需要几个小时才能完成所有工作。而使用 Wasp,同样的流程只需不到 1 小时。剩下的时间该怎么利用呢?用来实现你业务真正需要的关键功能!
请您给予我们支持!
您是否对更多类似内容感兴趣?订阅我们的新闻邮件,并在 GitHub 上给我们点个赞吧!我们需要您的支持才能继续推进我们的项目 😀
结论
我认为,如果你是一名想要把事情做好的开发人员,你可能已经注意到这两种实现方式在复杂程度上的显著差异。
Wasp 通过减少样板代码和抽象重复性任务,使开发人员能够更专注于构建独特功能,而不是被身份验证细节所困扰。这对于旨在快速发布产品的小型团队或个人开发人员来说尤其有利。
当然,通常来说,当我们谈论抽象时,总会伴随着失去个性化实现所带来的精细度这一缺点。在这种情况下,Wasp 提供了一系列可供你围绕其实现的功能,并在后台使用 Lucia,因此内容实现不匹配的情况极少发生。
总而言之,虽然使用 Next.js 和 Lucia 自行实现身份验证可以提供完全的控制和自定义,但这可能既复杂又耗时。另一方面,使用像 Wasp 这样的解决方案可以简化流程、减少代码量并加快开发速度。
文章来源:https://dev.to/wasp/how-to-add-auth-with-lucia-to-your-reactnextjs-app-a-step-by-step-guide-114e

