发布于 2026-01-06 2 阅读
0

如何使用 Lucia 为 React/Next.js 应用添加身份验证 - 分步指南

如何使用 Lucia 为 React/Next.js 应用添加身份验证 - 分步指南

尽管身份验证是 Web 应用中最常见的功能之一,但实现方式却多种多样,这使得它成为一项非常复杂的任务。在本文中,我将分享我使用 Lucia 的个人经验——Lucia 是一个现代化的、与框架无关的身份验证库,近几个月来,它受到了社区的广泛好评,实至名归。

首先,我将通过一个循序渐进的指南,演示如何在你的 Next.js 应用程序中实现它。这需要编写相当多的代码并进行一些配置,但过程本身非常简单明了。

其次,我们将看看如何使用Wasp仅用几行代码实现同样的功能。Wasp 是一个功能齐全的 React 和 Node.js 全栈框架,它底层使用 Lucia 来实现身份验证。它完全运行在您的基础设施上,并且 100% 开源免费。

Wasp 认证

为什么是露西亚?

在为应用程序添加身份验证时,有几种流行的解决方案可供选择。例如,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
Enter fullscreen mode Exit fullscreen mode

步骤二:安装 Lucia

接下来,安装 Lucia:

npm install lucia
Enter fullscreen mode Exit fullscreen mode

步骤 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

步骤 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;
}
Enter fullscreen mode Exit fullscreen mode

第五步:实现登录和注册功能

要实现这一点,我们首先需要创建一个 GitHub OAuth 应用。这相对简单,创建完成后,将必要的环境变量和回调 URL 添加到您的应用中即可。您可以参考 GitHub 文档了解具体操作方法。

//.env.local
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
Enter fullscreen mode Exit fullscreen mode

接下来,只需在页面上添加登录和注册功能即可,我们这就快速完成:

// 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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

添加页面后,我们还需要添加到 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);
}
Enter fullscreen mode Exit fullscreen mode

最后,是回调函数(也就是我们在 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;
}
Enter fullscreen mode Exit fullscreen mode

这里还有一件重要的事情是,现在我们使用的是 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!);
Enter fullscreen mode Exit fullscreen mode

步骤 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;
}
Enter fullscreen mode Exit fullscreen mode

小菜一碟,对吧?嗯,其实不然。

让我们回顾一下实现这一目标所必需的步骤:

  • 设置您的应用。
  • 加入露西亚。
  • 设置身份验证。
  • 将用户添加到数据库。
  • 获取 GitHub OAuth 凭据并配置环境变量。
  • 创建一些实用函数。
  • 添加登录和注册路由,并使用自定义组件。
  • 最后,创建一条受保护的路由。

https://media2.giphy.com/media/3ofSBnYbEPePeigIMg/giphy.gif?cid=7941fdc6x77sivlvr6hs2yu5aztvwjvhgugv6b718mjanr2h&ep=v1_gifs_search&rid=giphy.gif&ct=g

说实话,当想要快速开发出一些很棒的功能时,反复执行这些步骤,还要调试时不时出现的一些逻辑问题,确实会让人有点沮丧。很快,我们将了解 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
Enter fullscreen mode Exit fullscreen mode

步骤 2:将用户实体添加到我们的数据库中

只需在文件app.auth.userEntity中定义实体schema.prisma并运行一些迁移即可:

model User {
  id Int @id @default(autoincrement())
  email   String   @unique
  name    String?
  // Add your own fields below
  // ...
}
Enter fullscreen mode Exit fullscreen mode

步骤 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"
  },
}
Enter fullscreen mode Exit fullscreen mode

之后,只需在终端中运行:

wasp db migrate-dev
Enter fullscreen mode Exit fullscreen mode

步骤 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
Enter fullscreen mode Exit fullscreen mode

步骤 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"
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

最后,要保护路由,只需简单地添加更改即可main.waspauthRequired: true因此,我们可以像这样添加:

// main.wasp
page MainPage {
  component: import Main from "@src/pages/Main",
  authRequired: true
}
Enter fullscreen mode Exit fullscreen mode

如果您想更深入地了解这个示例,请访问此仓库:wasp/examples/todo-typescript at release · wasp-lang/wasp (github.com)
另一个很好的参考资料是他们的文档,您可以在这里找到。文档涵盖了我在这里提到的大部分内容,甚至更多(例如Wasp v0.14 中新增的强大钩子)。

https://media4.giphy.com/media/nDSlfqf0gn5g4/giphy.gif?cid=7941fdc6oxsddr7p8rjsuavcyq7ugiad8iqdu1ei25urcge4&ep=v1_gifs_search&rid=giphy.gif&ct=g

这样容易多了,不是吗?让我们回顾一下我们走到今天这一步所采取的步骤:

  • 设置项目。
  • 将用户实体添加到数据库中。
  • 在 Wasp 主配置中定义身份验证。
  • 获取 GitHub OAuth 凭据并配置环境变量。
  • 使用预构建的、易于使用的组件,添加登录和注册的路由和页面。
  • authRequired通过在配置中指定来保护路由。

自定义 Wasp 身份验证

如果您需要对身份验证流程进行更多控制和自定义,Wasp 提供了身份验证钩子,允许您根据应用程序的特定需求定制体验。这些钩子使您能够在身份验证过程的各个阶段执行自定义代码,从而确保您可以实现任何所需的自定义行为。

有关使用 Wasp 的身份验证钩子的更多详细信息,请访问Wasp 文档

附加章节:使用 Wasp 添加电子邮件/密码登录并自定义身份验证

现在假设我们要添加电子邮件和密码身份验证——以及我们期望遵循此登录方法的所有常用功能(例如重置密码、电子邮件验证等)。

使用 Wasp,我们只需要在 main.wasp 文件中添加几行代码,因此,只需更新 Wasp 配置以包含电子邮件/密码身份验证,即可立即使用!

https://wasp-lang.dev/img/auth-ui/auth-demo-compiler.gif

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"
  },
}
Enter fullscreen mode Exit fullscreen mode

在 Next.js 中使用 Lucia 实现这个功能需要更多的工作,涉及从实际发送电子邮件到生成验证令牌等诸多方面。他们在这里提到了这一点,但再次强调,Wasp's Auth 让整个过程变得简单得多,它为我们处理了许多复杂性,同时还提供了许多其他可直接使用的 UI 组件,以简化 UI 细节(例如VerifyEmailFormForgotPasswordFormResetPasswordForm)。

关键在于实现相同场景所需的时间和开发者经验的差异。如果你独自使用 Lucia 开发 Next.js 项目,至少需要几个小时才能完成所有工作。而使用 Wasp,同样的流程只需不到 1 小时。剩下的时间该怎么利用呢?用来实现你业务真正需要的关键功能!

请您给予我们支持!

https://media2.giphy.com/media/l0MYAs5E2oIDCq9So/giphy.gif?cid=7941fdc6l6i66eq1dc7i5rz05nkl4mgjltyv206syb0o304g&ep=v1_gifs_search&rid=giphy.gif&ct=g

您是否对更多类似内容感兴趣?订阅我们的新闻邮件,并在 GitHub 上给我们点个赞吧!我们需要您的支持才能继续推进我们的项目 😀

结论

https://media2.giphy.com/media/l1AsKaVNyNXHKUkUw/giphy.gif?cid=7941fdc6u6vp4j2gpjfuizupxlvfdzskl03ncci2e7jq17zr&ep=v1_gifs_search&rid=giphy.gif&ct=g

我认为,如果你是一名想要把事情做好的开发人员,你可能已经注意到这两种实现方式在复杂程度上的显著差异。

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