Next.js 实现JWT身份验证:SSR与CSR的简单教程
Random pretty picture to get your attention
最近,我遇到了一个有趣的关于技术挑战的问题。如何在 NextJs 中使用存储于 HTTP-only cookie 中的 JWT 进行服务器端渲染,而不借助任何外部库?
在我的GitHub上,可以找到同时包含了后端和前端的单代码库,地址是我的GitHub。
方法我采取了一种非常简单的方法——生成JWT令牌的小后端和前端则将生成的令牌存储在cookie中。对于客户端的请求,我使用了rewrite
指令将基础URL替换为后端的URL。对于服务端的请求,我们可以直接使用URL进行获取而无需任何处理(因为没有跨源资源共享(CORS)的问题)。服务端的数据获取则得益于从next/headers
中获取的cookies
包。
我做了一个非常简单的NodeJs Express后端应用,它只有3个接口。为了简单起见,我使用了一个基于文件的数据库插件叫做file-system-db插件。
注册端此接口允许用户使用用户名和密码注册账户。我没有处理加盐和哈希这些细节,但在实际应用中,这些步骤是非常重要的。
app.post("/sign-up", (req, res) => {
console.log(req.body);
if (!req.body || !req.body.username || !req.body.password) {
res.status(400).send({ message: "请输入用户名和密码" });
return;
}
const { username, password } = req.body;
if (db.has(username)) {
res.status(400).send({ message: "该用户名已被注册" });
return;
}
db.set(username, password);
res.send({ message: "注册成功" });
});
登录端点接口
登录端点返回一个 JWT 令牌,该令牌会被设置为前端的 cookie,允许已经登录的用户发起请求。我用 jsonwebtoken 这个库生成了令牌。
app.post("/sign-in", (req, res) => {
if (!req.body || !req.body.username || !req.body.password) {
res.status(400).json({ message: "用户名和密码必填" });
return;
}
const { username, password } = req.body;
if (!db.has(username) || db.get(username) !== password) {
res.status(403).json({ message: "无效的凭据" });
return;
}
const token = jwt.sign({ username }, jwtSigningSecret, {
algorithm: "HS256",
});
res.cookie("jwt", token);
res.send({ message: "登录成功" });
});
已就绪的端点
此端点唯一的作用是验证与请求一同发送的JWT令牌。我使用了jsonwebtoken来验证令牌。
app.get("/ready", (req, res) => {
const token = req.cookies["jwt"];
console.log(token);
if (!token) {
res.status(403).send({ message: "未通过认证" });
return;
}
let decoded;
try {
decoded = jwt.verify(token, jwtSigningSecret);
} catch (error) {
res.status(403).send({ message: "请重新登录" });
return;
}
if (!db.has(decoded.username)) {
res.status(403).send({ message: "登录已过期" });
return;
}
res.send({ message: "搞定", username: decoded.username });
});
前端(指网站或应用的用户界面)
next.config.ts
这是 Next.js 应用程序的配置文件。
首先我需要更改的是 rewrites
指令项。为了让客户端请求能够正常加载,我们将路径以 /api/
开头的请求重写为后端的基础 URL(在本地运行的情况下为 http://localhost:8080
)。
const API_URL = process.env.BACKEND_BASE_URL; // API 的基础 URL
/** 类型为 {import('next').NextConfig} */ // 用于配置 Next.js 应用程序的行为
const nextConfig = {
async rewrites() { // 重写API路径
return [
{
source: "/api/:path*",
destination: `${API_URL}/:path*`,
},
];
},
};
export default nextConfig;
client-side-fetching.ts
// 客户端数据获取.ts (client-side data fetching.ts)
对于客户端数据获取,我通常使用axios。如果遇到unauthenticated
响应,我可以利用axios拦截器引导用户回到登录页面。
"use client";
import axios from "axios";
const clientFetch = axios.create();
clientFetch.interceptors.response.use(
(response) => {
if (response.status >= 400 && response.status <= 403) {
window.location.pathname = "/sign-in";
return Promise.reject("未授权");
}
return response;
},
(error) => {
return Promise.reject(error);
}
);
export default clientFetch;
你可以在这里看看它是怎么用的,点击此处:here:
const 调用登录 = async (data: { username: string; password: string }) => {
try {
await clientFetch.post("/api/sign-in", data);
router.push("/home");
} catch (错误) {
const err = 错误 as AxiosError;
const 数据 = err.response?.data as { message: string };
设置错误(数据.message);
}
};
服务器端数据获取.ts // 此 TypeScript 文件用于处理服务器端数据获取
我也写了一个帮助函数来在服务器端获取数据。幸运的是,我们能通过 next/headers
中的 cookies
包从 cookie 中提取 JWT。
这个辅助函数返回一个Promise,包含一个数组,其中包含O或null,以及Error或null。
import "server-only";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { env } from "@/env";
/**
* each parameter description and return value description
*/
export async function ssrFetch<O, I = undefined>(
pathname: string,
options: {
method: string;
body?: I;
next?: NextFetchRequestConfig;
}
): Promise<[O | null, Error | null]> {
let response;
try {
response = await fetch(`${env.BACKEND_BASE_URL}${pathname}`, {
method: options.method,
body: options.body ? JSON.stringify(options.body) : undefined,
headers: {
Cookie: `jwt=${(await cookies()).get("jwt")?.value || ""}`,
"Content-Type": "application/x-www-form-urlencoded",
},
next: {
revalidate: 0,
...options.next,
},
});
} catch (error) {
return [null, error as Error];
}
// 如果响应状态码为403(禁止访问)
if (response.status === 403) {
redirect("/sign-in");
// 指向登录页面
}
// 将响应内容解析为JSON
response = await response.json();
// 返回一个包含数据和错误的Promise
return [response, null];
}
你可以在这里点击看看它是怎么用的
async function 从JWT获取用户名(): Promise<string> {
const [result, error] = await ssrFetch<{
message: string;
username: string;
}>("/ready", { method: "GET" });
if (error !== null || result?.username === undefined) {
throw new Error("无法获取/ready");
}
return result.username;
}
middleware.ts
在中间件中,我们可以检查用户在每一页上的认证情况。这样可以避免用户在JWT令牌过期后看到缓存页面,比如。
import axios, { AxiosError } from "axios";
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_PATHS = ["/sign-in", "/sign-up"];
const ENCODED_CHAR_REGEX = /%[0-9A-Fa-f]{2}/;
async function validateJWT(jwt: string | undefined) {
if (!jwt) {
return false;
}
try {
await axios(`${process.env.BACKEND_BASE_URL}/ready`, {
method: "GET",
headers: {
Cookie: `jwt=${jwt}`,
},
});
return true;
} catch (error) {
return false;
}
}
export default async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
// 处理包含编码字符的URL路径
if (ENCODED_CHAR_REGEX.test(pathname)) {
console.log("middleware: before rewrite", pathname);
const decodedPathname = decodeURIComponent(pathname);
return NextResponse.rewrite(new URL(`/app${decodedPathname}`, req.url));
}
// 跳过 Next.js 内部路由
if (pathname.includes("/_next/")) {
return NextResponse.next();
}
const jwt = req.cookies.get("jwt");
const isPublicPath = PUBLIC_PATHS.some((path) => pathname.startsWith(path));
console.log("middleware path:", pathname, "isPublicPath", isPublicPath);
// 处理没有 JWT 的非公共路径
if (!jwt && !isPublicPath) {
console.log("running this");
return NextResponse.redirect(new URL("/sign-in", req.url));
}
// 对于没有JWT的公共路径,直接跳过JWT验证
if (!jwt && isPublicPath) {
return NextResponse.next();
}
const isValidJWT = await validateJWT(jwt?.value);
// 如果 JWT 无效,重定向到登录页面
if (!isValidJWT) {
return NextResponse.redirect(new URL("/sign-in", req.url));
}
// 将已认证的用户从公共路径重定向到首页
if (isPublicPath) {
return NextResponse.redirect(new URL("/home", req.url));
}
console.log("middleware PASS");
return NextResponse.next();
}
export const config = {
matcher: [
/*
* 匹配所有请求路径,但排除以下路径开头的请求:
* - web/api (API 路由)
* - web/_next/static (静态文件)
* - web/_next/image (图片优化文件)
* 排除所有以 .png 结尾的路径,例如web/somefile.png
*/
"/((?!api|_next/image|favicon.ico|.*\\.png$).*)",
],
};
最后的总结
这种方法受到了Max Schmitt在其文章的启发,并进行了调整和适应,针对NextJs的新版本,利用了middleware.ts
并简化了流程,使之更易于操作,通过移除代理API路由,使其更加简洁,同时提供了SSR和CSR数据获取的辅助函数,以简化开发过程。
共同学习,写下你的评论
评论加载中...
作者其他优质文章