如何使用 JWT 在 Next.js 中实现令牌身份验证

使用 JWT 在 Next.js 中实现令牌身份验证

令牌身份验证是保护 Web 和移动应用程序免受未经授权访问的常用方法。在 Next.js 中,你可以利用 Next-auth 提供的身份验证功能。

或者,你也可以选择使用 JSON Web 令牌(JWT)来开发基于自定义令牌的身份验证系统。通过这样做,你可以更好地控制身份验证逻辑;本质上,你可以定制系统以精确匹配你的项目需求。

设置 Next.js 项目

首先,通过在终端上运行以下命令来安装 Next.js:

npx create-next-app@latest next-auth-jwt --experimental-app

本指南将使用 Next.js 13,其中包含应用程序目录。

接下来,使用 npm(Node 包管理器)在项目中安装这些依赖项:

npm install jose universal-cookie

jose 是一个 JavaScript 模块,它提供了一组用于使用 JSON Web 令牌的实用程序,而 universal-cookie 依赖提供了一种在客户端和服务器端环境中使用浏览器 cookie 的简便方法。

创建登录表单用户界面

打开 src/app 目录,创建一个新文件夹并将其命名为 login。在此文件夹中,添加一个新的 page.js 文件并包含以下代码:

"use client";
import { useRouter } from "next/navigation";

export default function LoginPage() {
return (
<form onSubmit={handleSubmit}>
<label>
Username:
<input type="text" name="username" />
</label>
<label>
Password:
<input type="password" name="password" />
</label>
<button type="submit">Login</button>
</form>
);
}

上面的代码创建了一个名为 LoginPage 的功能组件,它将在浏览器上呈现一个简单的登录表单,允许用户输入用户名和密码。

代码中的 “use client” 语句确保在应用程序目录中的仅服务器代码和仅客户端代码之间声明边界。

本例中用于声明登录页面的代码,特别是 handleSubmit 函数只在客户端执行;否则,Next.js 将抛出错误。

现在,让我们定义 handleSubmit 函数的代码。在功能组件内,添加以下代码:

const router = useRouter();

const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const username = formData.get("username");
const password = formData.get("password");
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ username, password }),
});
const { success } = await res.json();
if (success) {
router.push("/protected");
router.refresh();
} else {
alert("Login failed");
}
};

为了管理登录身份验证逻辑,此函数从登录表单中捕获用户凭据。然后,它向 API 端点发送 POST 请求,并传递用户详细信息以进行验证。

如果凭据有效,则表明登录过程成功 – API 在响应中返回成功状态。然后,处理函数将使用 Next.js 的路由器将用户导航到指定的 URL,在本例中为受保护的路由。

定义登录 API 端点

在 src/app 目录中,创建一个新文件夹并将其命名为 api。在此文件夹中,添加一个新的 login/route.js 文件并包含以下代码:

import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";

export async function POST(request) {
const body = await request.json();
if (body.username === "admin" && body.password === "admin") {
const token = await new SignJWT({
username: body.username,
})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("30s")
.sign(getJwtSecretKey());
const response = NextResponse.json(
{ success: true },
{ status: 200, headers: { "content-type": "application/json" } }
);
response.cookies.set({
name: "token",
value: token,
path: "https://www.makeuseof.com/",
});
return response;
}
return NextResponse.json({ success: false });
}

此 API 的主要任务是使用模拟数据验证 POST 请求中传递的登录凭据。

验证成功后,它会生成与经过身份验证的用户详细信息关联的加密 JWT 令牌。最后,它向客户端发送成功的响应,包括响应 cookie 中的令牌;否则,返回失败状态响应。

实施令牌验证逻辑

令牌身份验证的第一步是在成功登录过程后生成令牌。下一步是实现令牌验证的逻辑。

本质上,你将使用 jose 模块提供的 jwtVerify 函数来验证后续 HTTP 请求传递的 JWT 令牌。

在 src 目录中,创建一个新的 libs/auth.js 文件并包含以下代码:

import { jwtVerify } from "jose";

export function getJwtSecretKey() {
const secret = process.env.NEXT_PUBLIC_JWT_SECRET_KEY;
if (!secret) {
throw new Error("JWT Secret key is not matched");
}
return new TextEncoder().encode(secret);
}

export async function verifyJwtToken(token) {
try {
const { payload } = await jwtVerify(token, getJwtSecretKey());
return payload;
} catch (error) {
return null;
}
}

密钥用于签名和验证令牌。通过将解码后的令牌签名与预期签名进行比较,服务器可以有效地验证所提供的令牌是否有效,并最终授权用户的请求。

在根目录下创建 .env 文件并添加唯一密钥,如下:

NEXT_PUBLIC_JWT_SECRET_KEY=your_secret_key

创建受保护的路由

现在,你需要创建一条只有经过身份验证的用户才能访问的路由。为此,请在 src/app 目录中创建一个新的 protected/page.js 文件。在此文件中,添加以下代码:

export default function ProtectedPage() {
return <h1>Very protected page</h1>;
}

创建一个钩子来管理身份验证状态

在 src 目录中创建一个新文件夹并将其命名为 hooks。在此文件夹中添加一个新的 useAuth/index.js 文件并包含以下代码:

"use client" ;
import React from "react";
import Cookies from "universal-cookie";
import { verifyJwtToken } from "@/libs/auth";

export function useAuth() {
const [auth, setAuth] = React.useState(null);

const getVerifiedtoken = async () => {
const cookies = new Cookies();
const token = cookies.get("token") ?? null;
const verifiedToken = await verifyJwtToken(token);
setAuth(verifiedToken);
};
React.useEffect(() => {
getVerifiedtoken();
}, []);
return auth;
}

该钩子管理客户端的身份验证状态。它使用 verifyJwtToken 函数获取并验证 cookie 中存在的 JWT 令牌的有效性,然后将经过身份验证的用户详细信息设置为 auth 状态。

通过这样做,它允许其他组件访问和使用经过身份验证的用户的信息。这对于根据身份验证状态更新 UI、发出后续 API 请求或根据用户角色呈现不同内容等场景至关重要。

在本例中,你将使用钩子根据用户的身份验证状态在主路由上呈现不同的内容。

你可能考虑的另一种方法是使用 Redux Toolkit 处理状态管理或使用 Jotai 等状态管理工具。这种方法保证组件可以全局访问身份验证状态或任何其他定义的状态。

继续打开 app/page.js 文件,删除样板 Next.js 代码,然后添加以下代码:

"use client" ;

import { useAuth } from "@/hooks/useAuth";
import Link from "next/link";
export default function Home() {
const auth = useAuth();
return <>
<h1>Public Home Page</h1>
<header>
<nav>
{auth ? (
<p>logged in</p>
) : (
<Link href="https://wilku.top/login">Login</Link>
)}
</nav>
</header>
</>
}

上面的代码利用 useAuth 钩子来管理身份验证状态。在此过程中,当用户未经身份验证时,它有条件地呈现一个公共主页,其中包含指向登录页面路由的链接,并为经过身份验证的用户显示一个段落。

添加中间件以强制对受保护的路由进行授权访问

在 src 目录中,创建一个新的 middleware.js 文件,并添加以下代码:

import { NextResponse } from "next/server";
import { verifyJwtToken } from "@/libs/auth";

const AUTH_PAGES = ["https://wilku.top/login"];

const isAuthPages = (url) => AUTH_PAGES.some((page) => page.startsWith(url));

export async function middleware(request) {

const { url, nextUrl, cookies } = request;
const { value: token } = cookies.get("token") ?? { value: null };
const hasVerifiedToken = token && (await verifyJwtToken(token));
const isAuthPageRequested = isAuthPages(nextUrl.pathname);

if (isAuthPageRequested) {
if (!hasVerifiedToken) {
const response = NextResponse.next();
response.cookies.delete("token");
return response;
}
const response = NextResponse.redirect(new URL(`/`, url));
return response;
}

if (!hasVerifiedToken) {
const searchParams = new URLSearchParams(nextUrl.searchParams);
searchParams.set("next", nextUrl.pathname);
const response = NextResponse.redirect(
new URL(`/login?${searchParams}`, url)
);
response.cookies.delete("token");
return response;
}

return NextResponse.next();

}
export const config = { matcher: ["https://wilku.top/login", "/protected/:path*"] };

该中间件代码充当守卫。它进行检查以确保当用户想要访问受保护的页面时,他们经过身份验证并有权访问路由,此外还将未经授权的用户重定向到登录页面。

保护 Next.js 应用程序的安全

令牌认证是一种有效的安全机制。然而,它并不是保护你的应用程序免受未经授权访问的唯一可用策略。

为了增强应用程序应对动态网络安全形势的能力,重要的是采用全面的安全方法,全面解决潜在的安全漏洞和漏洞,以确保彻底的保护。