跳转至

Next.js

使用 @authgear/nextjs SDK,将 Authany 身份验证集成到 Next.js App Router 应用中。您将完成登录、登出、显示用户信息,以及保护一个服务端 API 路由。

完整的示例应用可在 authgear/authgear-example-nextjs 获取。

您将构建的内容:

  • 一个首页:未登录时显示登录按钮,登录后显示用户身份信息和登出按钮
  • 一个受保护的 API 路由(/api/me),返回当前用户的信息

在 Authany 中设置您的应用

第 1 步:在管理台中创建应用

  1. 登录 Authany 管理台
  2. 选择或创建一个项目
  3. 在左侧菜单中进入应用
  4. 点击 ⊕ Add Application
  5. 输入应用名称,并选择 Single Page Application 作为应用类型
  6. 点击保存

第 2 步:配置应用

  1. 在 URIs 下,找到 Authorized Redirect URIs 字段
  2. 添加 http://localhost:3000/api/auth/callback
  3. 记下您的 Client ID 和 Endpoint(例如 https://your-project.authanyid.com),稍后会用到
  4. 点击保存

构建您的 Next.js 应用

第 1 步:创建 Next.js 项目

npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app

第 2 步:安装 Authany SDK

npm install @authgear/nextjs

第 3 步:配置环境变量

创建 .env.local,内容如下:

AUTHGEAR_ENDPOINT=https://your-project.authanyid.com
AUTHGEAR_CLIENT_ID=your-client-id
AUTHGEAR_REDIRECT_URI=http://localhost:3000/api/auth/callback
SESSION_SECRET=a-random-string-of-at-least-32-characters

说明

SESSION_SECRET 用于加密存储在浏览器中的会话 Cookie。它必须至少为 32 个字符。请使用随机字符串生成器生成。

第 4 步:创建共享的 Authany 配置

创建 src/lib/authgear.ts。该文件保存您的 Authany 配置,供服务端和客户端代码共同引用:

// src/lib/authgear.ts
import type { AuthgearConfig } from "@authgear/nextjs";

export const authgearConfig: AuthgearConfig = {
  endpoint: process.env.AUTHGEAR_ENDPOINT!,
  clientID: process.env.AUTHGEAR_CLIENT_ID!,
  redirectURI: process.env.AUTHGEAR_REDIRECT_URI!,
  sessionSecret: process.env.SESSION_SECRET!,
};

第 5 步:添加 OAuth 路由处理器

创建 src/app/api/auth/[...authgear]/route.ts。这个通配路由会自动处理所有 Authany 身份验证端点:

// src/app/api/auth/[...authgear]/route.ts
import { createAuthgearHandlers } from "@authgear/nextjs";
import { authgearConfig } from "@/lib/authgear";

export const { GET, POST } = createAuthgearHandlers(authgearConfig);

createAuthgearHandlers 会为您注册以下路由:

Method Path Purpose
GET /api/auth/login 开始 OAuth 登录流程
GET /api/auth/callback 处理 OAuth 回调并设置会话 Cookie
GET /api/auth/logout 清除会话并吊销令牌
POST /api/auth/refresh 刷新已过期的访问令牌
GET /api/auth/userinfo 返回当前用户的信息
GET /api/auth/open 打开一个已预先完成身份验证的 Authany 托管页面

第 6 步:将 AuthgearProvider 添加到布局中

AuthgearProvider 通过 React context 让所有客户端组件都能获取用户的会话状态。由于它使用了浏览器 API,因此必须是一个客户端组件。

创建 src/app/providers.tsx:

// src/app/providers.tsx
"use client";

import { AuthgearProvider } from "@authgear/nextjs/client";

export default function Providers({ children }: { children: React.ReactNode }) {
  return <AuthgearProvider>{children}</AuthgearProvider>;
}

然后在 src/app/layout.tsx 中包裹您的根布局:

// src/app/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Providers from "./providers";

const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Next.js + Authgear",
  description: "Example app demonstrating Authgear authentication with Next.js",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

挂载时,AuthgearProvider 会请求 /api/auth/userinfo 来检查是否存在现有会话。结果可以通过 useAuthgear() 这个 hook 获取。

第 7 步:实现首页

替换 src/app/page.tsx。该页面是一个客户端组件,因此可以使用 useAuthgear() 读取会话状态并响应其变化:

// src/app/page.tsx
"use client";

import { useState } from "react";
import { useAuthgear, SignInButton, SignOutButton } from "@authgear/nextjs/client";

export default function Home() {
  const { isAuthenticated, user } = useAuthgear();
  const [apiResult, setApiResult] = useState<string | null>(null);

  async function testProtectedApi() {
    const res = await fetch("/api/me");
    const data = await res.json();
    setApiResult(JSON.stringify(data, null, 2));
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-center gap-6">
      <h1 className="text-3xl font-bold">Next.js + Authgear</h1>

      {isAuthenticated ? (
        <>
          <p className="text-gray-600">
            Logged in as: <span className="font-mono">{user?.sub}</span>
          </p>
          {(user?.email ?? user?.phoneNumber) && (
            <p className="text-gray-600">
              {user?.email ?? user?.phoneNumber}
            </p>
          )}
          <button
            onClick={testProtectedApi}
            className="rounded-md bg-green-600 px-6 py-2 text-white hover:bg-green-700"
          >
            Test Protected API
          </button>
          {apiResult && (
            <pre className="rounded-md bg-gray-100 dark:bg-gray-800 p-4 text-sm">{apiResult}</pre>
          )}
          <SignOutButton className="rounded-md bg-red-600 px-6 py-2 text-white hover:bg-red-700">
            Logout
          </SignOutButton>
        </>
      ) : (
        <SignInButton className="rounded-md bg-blue-600 px-6 py-2 text-white hover:bg-blue-700">
          Login
        </SignInButton>
      )}
    </main>
  );
}

关键的 hook 和组件:

  • useAuthgear() — 返回 { isAuthenticated, user, state, isLoaded, signIn, signOut }。user 是一个 UserInfo 对象,包含 sub、email、phoneNumber 及其他资料字段。
  • <SignInButton> — 点击后跳转到 /api/auth/login,开始 OAuth 流程。
  • <SignOutButton> — 点击后跳转到 /api/auth/logout,清除会话。

第 8 步:创建受保护的 API 路由

服务端路由处理器可以使用 @authgear/nextjs/server 中的 currentUser() 来验证身份。它会读取加密的会话 Cookie,并返回当前用户;如果未通过身份验证,则返回 null。

创建 src/app/api/me/route.ts:

// src/app/api/me/route.ts
import { currentUser } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
import { NextResponse } from "next/server";

export async function GET() {
  const user = await currentUser(authgearConfig);

  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  return NextResponse.json({ user });
}

在从 Authany 的 userinfo 端点获取用户信息之前,currentUser() 会在访问令牌过期时自动刷新它。


运行应用

npm run dev

在浏览器中打开 http://localhost:3000。


测试集成

登录流程

  1. 访问 http://localhost:3000 —— 您应该会看到一个登录按钮
  2. 点击登录 —— 您会被重定向到 Authany 托管的登录页面
  3. 使用您的凭据登录
  4. 您会被重定向回首页,此时页面会显示您的用户 ID 和邮箱/手机号

登出流程

  1. 点击登出 —— 会话被清除,您会返回到登录界面

受保护的 API

  1. 登录后,点击Test Protected API
  2. 页面会调用 GET /api/me,该接口会验证您的会话并返回您的用户信息:
{
  "user": {
    "sub": "...",
    "email": "...",
    "emailVerified": true
  }
}
  1. 要验证该路由确实受到保护,请打开一个隐身窗口并运行:
curl http://localhost:3000/api/me

预期返回状态码 401 的响应:

{ "error": "Unauthorized" }

项目结构

src/
├── app/
│   ├── api/
│   │   ├── auth/
│   │   │   └── [...authgear]/
│   │   │       └── route.ts      # OAuth route handler
│   │   └── me/
│   │       └── route.ts          # Protected API route
│   ├── layout.tsx                # Root layout with AuthgearProvider
│   ├── page.tsx                  # Home page with login/logout UI
│   └── providers.tsx             # Client component wrapping AuthgearProvider
└── lib/
    └── authgear.ts               # Shared Authgear config

其他主题

在服务端组件中读取会话

使用 auth() 可以在不发起网络请求的情况下读取原始会话(状态 + 令牌):

import { auth } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";

export default async function MyServerComponent() {
  const session = await auth(authgearConfig);

  if (session.state !== "AUTHENTICATED") {
    return <p>Not logged in</p>;
  }

  return <p>Access token expires at: {session.expiresAt}</p>;
}

当您需要完整的用户资料时,请使用 currentUser() —— 它会向 userinfo 端点发起额外请求,并自动处理令牌刷新。

在外部 API 中验证 Bearer 令牌

如果您有一个独立的 API 服务器接收由 Authany 签发的访问令牌(例如来自移动端应用),可以使用 verifyAccessToken() 来验证这些令牌:

import { verifyAccessToken } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const authHeader = request.headers.get("authorization");
  if (!authHeader?.startsWith("Bearer ")) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const token = authHeader.slice(7);

  try {
    const payload = await verifyAccessToken(token, authgearConfig);
    return NextResponse.json({ sub: payload.sub });
  } catch {
    return NextResponse.json({ error: "Invalid token" }, { status: 401 });
  }
}

说明

verifyAccessToken 使用 Authany 的 JWKS 端点在本地校验 JWT 签名和有效期,不涉及会话 Cookie。

检查会话状态

useAuthgear() 会返回 isLoaded,用于表示初始会话检查是否已完成。使用它可以避免出现一闪而过的未登录界面:

const { isAuthenticated, isLoaded } = useAuthgear();

if (!isLoaded) return <p>Loading...</p>;

打开用户设置

<UserSettingsButton> 组件会在新标签页中打开 Authany 托管的设置页面,且当前用户已经完成身份验证,无需 Server Action。

将该按钮添加到您的组件中:

import { UserSettingsButton } from "@authgear/nextjs/client";

export default function Dashboard() {
  return (
    <UserSettingsButton>Account Settings</UserSettingsButton>
  );
}

该按钮必须渲染在 <AuthgearProvider> 内部。它会将用户的会话交换为一个短期有效的令牌,并在新标签页中打开已预先完成身份验证的设置页面 URL。

像普通按钮一样设置样式 —— <UserSettingsButton> 支持所有标准的 HTML button 属性:

<UserSettingsButton
  className="btn btn-primary"
  style={{ padding: "0.5rem 1rem" }}
  disabled={!isAuthenticated}
>
  Account Settings
</UserSettingsButton>

如果未提供子元素,默认标签文字为 "Account Settings"。

自定义路由路径

默认情况下,<UserSettingsButton> 会向 /api/auth/open 发送请求。如果您的身份验证路由位于不同路径,请在 <AuthgearProvider> 上设置 openPagePath:

<AuthgearProvider openPagePath="/api/my-auth/open">
  {children}
</AuthgearProvider>

错误行为

如果用户未通过身份验证,或其会话中没有刷新令牌,新标签页将显示错误而不是设置页面。请为按钮加上身份验证检查以避免这种情况:

const { isAuthenticated } = useAuthgear();

<UserSettingsButton disabled={!isAuthenticated}>
  Account Settings
</UserSettingsButton>

控制单点登录行为

默认情况下,Authany 会在多次登录之间复用其服务端会话(即启用 SSO)。您可以通过 createAuthgearHandlers 中的 isSSOEnabled 全局修改此行为,也可以在单次登录调用中通过 prompt 选项覆盖该设置。

全局禁用 SSO —— 始终显示登录表单:

// src/app/api/auth/[...authgear]/route.ts
import { createAuthgearHandlers } from "@authgear/nextjs";
import { authgearConfig } from "@/lib/authgear";

export const { GET, POST } = createAuthgearHandlers({
  ...authgearConfig,
  isSSOEnabled: false,
});

通过 PromptOption 在单次登录调用中覆盖设置:

import { PromptOption } from "@authgear/nextjs";

// via hook
const { signIn } = useAuthgear();
signIn({ prompt: PromptOption.Login }); // always show login form

// via component
<SignInButton signInOptions={{ prompt: PromptOption.Login }}>Sign In</SignInButton>
PromptOption Effect
PromptOption.Login 始终显示登录表单
PromptOption.None 从不显示登录表单;如果用户未通过身份验证则报错

单次调用的 prompt 优先级高于全局的 isSSOEnabled 设置。

后续步骤