Next.js¶
使用 @authgear/nextjs SDK,将 Authany 身份验证集成到 Next.js App Router 应用中。您将完成登录、登出、显示用户信息,以及保护一个服务端 API 路由。
完整的示例应用可在 authgear/authgear-example-nextjs 获取。
您将构建的内容:
- 一个首页:未登录时显示登录按钮,登录后显示用户身份信息和登出按钮
- 一个受保护的 API 路由(
/api/me),返回当前用户的信息
在 Authany 中设置您的应用¶
第 1 步:在管理台中创建应用¶
- 登录 Authany 管理台
- 选择或创建一个项目
- 在左侧菜单中进入应用
- 点击 ⊕ Add Application
- 输入应用名称,并选择 Single Page Application 作为应用类型
- 点击保存
第 2 步:配置应用¶
- 在 URIs 下,找到 Authorized Redirect URIs 字段
- 添加
http://localhost:3000/api/auth/callback - 记下您的 Client ID 和 Endpoint(例如
https://your-project.authanyid.com),稍后会用到 - 点击保存
构建您的 Next.js 应用¶
第 1 步:创建 Next.js 项目¶
第 2 步:安装 Authany SDK¶
第 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() 会在访问令牌过期时自动刷新它。
运行应用¶
在浏览器中打开 http://localhost:3000。
测试集成¶
登录流程¶
- 访问
http://localhost:3000—— 您应该会看到一个登录按钮 - 点击登录 —— 您会被重定向到 Authany 托管的登录页面
- 使用您的凭据登录
- 您会被重定向回首页,此时页面会显示您的用户 ID 和邮箱/手机号
登出流程¶
- 点击登出 —— 会话被清除,您会返回到登录界面
受保护的 API¶
- 登录后,点击Test Protected API
- 页面会调用
GET /api/me,该接口会验证您的会话并返回您的用户信息:
- 要验证该路由确实受到保护,请打开一个隐身窗口并运行:
预期返回状态码 401 的响应:
项目结构¶
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,用于表示初始会话检查是否已完成。使用它可以避免出现一闪而过的未登录界面:
打开用户设置¶
<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:
错误行为
如果用户未通过身份验证,或其会话中没有刷新令牌,新标签页将显示错误而不是设置页面。请为按钮加上身份验证检查以避免这种情况:
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 设置。