Cookie 是浏览器中默默存在的数据块。虽然有些 Cookie 会侵犯用户隐私,但其他一些则试图通过跟踪用户的浏览习惯、偏好等来改善浏览体验。Cookie 在许多场景都很有用,包括身份验证、改善用户体验和加快响应时间。
在本文中,我们将探讨如何在 Next.js 的服务器组件和客户端组件以及中间件中管理 Cookie。我们还将介绍两个可以在 Next.js Pages Router 中设置 Cookie 的包,并将它们应用到实际用例中。
要跟随本教程学习,可以前往此 GitHub 仓库。
https://github.com/GeoBrodas/cookies-with-nextjs
Cookie:福祉还是祸害?
Cookie 是 Web 应用程序放置在用户计算机上的小数据块,用于存储状态信息,如用户偏好或会话管理,也用于跟踪目的。
近年来,关于 Cookie 的讨论引发了很多争议,因为它们既有优点也有缺点。
一方面,Cookie 可以帮助你轻松存储用户的个性化数据,让你能更好地为每个用户定制用户体验。另一方面,Cookie 跟踪用户在线行为的能力引发了隐私担忧。
因此,现在有了一些标准和法规,要求 Web 应用程序披露其 Cookie 使用情况,并让用户选择是否退出。
鉴于 Next.js 的多功能性,有多种方法可以管理 Cookie。在此之前,我们只需要在页面、API 路由和中间件中管理 Cookie。然而,Next 13 中添加的服务器组件引入了更多新技术。
客户端 vs 服务器端 Cookie
很多人问的一个问题是客户端和服务器端 Cookie 是否有区别。
Cookie 可以通过客户端和服务器端操作创建。服务器端 Cookie 通常是通过 HTTP 标头创建和访问的。无论如何创建,Cookie 都存储在用户的浏览器中,可以直接在客户端访问。
但是,httpOnly Cookie 是个例外。如果你创建启用了 httpOnly 属性的 Cookie,这些 Cookie 就不能通过客户端操作直接访问,从而降低XSS攻击的风险。
在服务器组件中处理 Cookie:Next.js App Router
我们首先探讨如何在使用 Next.js App Router 的服务器组件中访问和修改 Cookie。要继续,请运行以下命令创建一个新的 Next 应用:
npx create-next-app@latest next-cookie
在安装过程中,选择你偏好的配置。不过,别忘了为"use App router?"选项选择"Yes"。完成基本设置后
如何获取 Cookie
要在服务器组件中读取传入的请求 Cookie 值,我们使用 cookies().get(cookieName) 方法,如下所示:
// app/page.js
import { cookies } from "next/headers";
const Home = () => {
const cookieStore = cookies();
const userId = cookieStore.get("user_id");
return <>{userId && <p>User ID: {userId}</p>}</>;
};
export default Home;
在本例中,如果没有存储带有user_id标签的 cookie,我们将看到一个空白屏幕。但是,如果有多个 cookie 与此标记匹配,userId将被设置为第一个匹配,并显示在浏览器上。
要获取与某个名称匹配的所有 cookie,我们可以使用cookies().getAll()方法,如下所示:
// app/page.js
import { cookies } from "next/headers";
const Home = () => {
const cookieStore = cookies();
const userId = cookieStore.getAll("user_id");
return (
<>
{userId.length > 0 &&
userId.map((cookie) => (
<div key={cookie.name}>
<p>Name: {cookie.name}</p>
<p>Value: {cookie.value}</p>
</div>
))}
</>
);
};
export default Home;
在这个更新的示例中,如果有多个带有user_id标记的 cookie,我们就会遍历它们,并显示每个 cookie 的名称和值。
如何设置 cookie
我们可以通过 cookies().set() 方法设置新的 Cookie。但是,由于 HTTP 不允许在流式传输开始后设置 Cookie,我们只能在服务器操作(Server Actions)或 API 路由中修改 Cookie 值(设置和删除)。以下是一个例子:
// app/page.js
import { cookies } from "next/headers";
const Home = () => {
async function createThemeCookie(formData) {
"use server";
const selectedTheme = formData.get("theme");
cookies().set("theme", selectedTheme);
}
return (
<>
<form action={createThemeCookie}>
<select name="theme">
<option value="dark">Dark Theme</option>
<option value="light">Light Theme</option>
</select>
<button type="submit">Create Theme Cookie</button>
</form>
</>
);
};
export default Home;
在这个例子中,我们用"use server"语句标记了设置 Cookie 的函数,将其指定为服务器操作。然后我们渲染了一个表单,允许用户选择首选主题。用户提交表单后,我们获取他们选择的主题值并将其设置到 theme cookie 中。
此外,我们可以使用以下语法在设置新 Cookie 时传入额外选项:
cookies().set({
name: "theme",
value: "light || dark",
httpOnly: true,
path: "/",
maxAge: 60 * 60 * 24 * 365 * 1000,
expires: new Date(Date.now() + 60 * 60 * 24 * 365 * 1000),
});
这样,我们可以创建一个 httpOnly cookie 并设置 cookie 的路径、最大年龄和过期日期。
如果你使用的 Next.js 版本低于v14,在使用服务器操作时可能会遇到以下错误:
Error:
× To use Server Actions, please enable the feature flag in your Next.js config.
要修复这个问题,只需更新你的 next.config.js 文件以启用实验性的 serverActions,如下所示:
const nextConfig = {
experimental: {
serverActions: true,
},
// . . .
};
module.exports = nextConfig;
但是,如果你使用的是 Next.js v14 或更新版本,则不会遇到此类错误。
如何删除 Cookie
我们可以使用 cookies().delete(name) 方法删除 Cookie。但是,和设置 Cookie 一样,我们只能在服务器操作或 API 路由中使用此方法,如下所示:
// app/page.js
import { cookies } from "next/headers";
const Home = () => {
async function deleteThemeCookie(formData) {
"use server";
cookies().delete("theme");
}
return (
<>
<form action={deleteThemeCookie}>
<button type="submit">Delete Theme Cookie</button>
</form>
</>
);
};
export default Home;
通过上面这个例子,当用户点击按钮提交表单时,theme cookie 将从浏览器中删除。
Next.js 路由处理程序和 API 路由中的 Cookie
在 Next.js 路由处理程序(即 /app 目录的 API 路由等价物)中,我们可以自由使用我们刚刚介绍的所有 cookie 方法,而无需创建服务器操作。你可以在下面看到一个例子:
// app/api/route.js
import { cookies } from "next/headers";
export async function GET(request) {
const cookieStore = cookies();
// 获取 Cookie
const myCookie = cookieStore.get("cookieName");
// 获取所有 Cookie
const myCookies = cookieStore.getAll("cookieName");
// 设置 Cookie
cookies().set("cookieName", "value");
// 删除 Cookie
cookies().delete("cookieName");
return new Response("Hello, World!", {
status: 200,
});
}
另外,你也可以在路由处理程序和 API 路由中使用传统的 Web API 从请求中读取 cookie,如下所示:
// app/api/route.js 或 pages/api/index.js
export async function GET(request) {
let theme = request.cookies.get("theme");
return new Response(JSON.stringify(theme));
}
在这个例子中,我们直接从请求对象中获取 theme cookie 并将其作为 API 响应返回。
在 Next.js Pages Router 中处理 Cookie
现在,让我们看看如何在经典的 Next.js Pages Router 中管理 Cookie。首先,创建一个新的 Next.js 应用程序,并确保在配置过程中选择 pages router。
在 Next.js 中使用 react-cookie
我们要探索的第一个包是 react-cookie。这个包旨在帮助你在 React 应用程序中加载和保存 cookie。为了尝试一下,我们将创建一个简单的应用程序来跟踪注册用户。
使用以下代码安装 react-cookie:
npm install react-cookie
要开始使用这些 Hook,在 _app.tsx 文件中添加 CookiesProvider 组件,如下所示:
import type { AppProps } from 'next/app';
import { CookiesProvider } from 'react-cookie';
function MyApp({ Component, pageProps }: AppProps) {
return (
<CookiesProvider>
<Component {...pageProps} />
</CookiesProvider>
);
}
export default MyApp;
我们首先添加一个 useEffect Hook 来在加载时获取所有 cookie:
import { useCookies } from 'react-cookie';
const Home: NextPage = () => {
const [cookies, setCookie, removeCookie] = useCookies(['user']);
useEffect(() => {
console.log('Cookies: ', cookies);
}, [cookies]);
return (
<div>...</div>
)}
目前,你应该还看不到任何 cookie。所以,我们创建一个使用 setCookie() 函数来设置 cookie 的函数:
import { useRouter } from 'next/router';
//...在默认函数内部
const router = useRouter();
const setCookieHandler = () => {
setCookie('new-user', true, {
path: '/',
});
router.replace("/");
};
setCookie() 函数接受三个参数:键名、键值和一些配置选项。这些选项包括 MaxAge、Path、Domain、expires 等。在这种情况下使用了 path 选项,允许程序从任何位置访问该 cookie。
如你所见,我们还使用了 useRouter() Hook 通过 replace() 方法重新加载页面,以避免在历史堆栈中添加 URL 条目。这样看起来就像页面重新渲染了一样!
随着我们继续前进,请记住本教程仅专注于演示特定包的功能。因此,我们将假设你理解认证流程等概念。
要了解更多关于 Next.js 中的认证信息,请参考这个关于 SuperTokens 的指南。你也可以在这篇文章中回顾认证流程。
将函数绑定到按钮
接下来,让我们将这个函数绑定到一个按钮上。输入以下代码:
{!cookies['user'] && (
<button onClick={setCookieHandler} className={styles.button}>
Complete new user registration!
</button>
)}
在这种情况下,只有当 cookie 存在时按钮才会渲染。运行开发服务器来看看这个效果。你可以通过按 Control+Shift+J 打开开发者工具,然后选择 Application 部分来直观地看到这个 cookie。
现在,我们删除 cookie 以允许用户退出。首先,编写另一个函数:
const removeCookieHandler = () => {
removeCookie('new-user');
router.replace("/");
};
接下来,将它绑定到另一个只有在 cookie 可用时才会渲染的按钮上。这意味着什么?如果用户已注册,cookie 就会可用。代码看起来是这样的:
{cookies['new-user'] && (
<button onClick={removeCookieHandler} className={styles.resetbutton}>
Reset new user registration
</button>
)}
下面是它在应用程序中的样子:
使用 cookies-next 包
接下来,我们来看看如何使用 cookies-next 包。这个包更适合 Next.js 生态系统,因为它可以在任何地方使用 - 无论是在 Pages Router 还是 App Router 中,在客户端,通过 getServerSideProps 在服务器端,甚至在 Next.js API 路由中。
以下是这两个包的对比:
- react-cookie 更加流行,提供简单易用的 API 并与 React 框架高度兼容
- cookies-next 是专门为 Next.js 创建的相对较新的包,提供服务器端渲染功能和改进的安全措施
另一个关于 cookies-next 令人惊喜的事实(这个是给那些关心包大小的开发者) - 它比 react-cookie 的包体积更小。这使得它在你的下一个项目中更具吸引力! 🎉
按照惯例,让我们首先用以下命令安装 cookies-next:
npm install cookies-next
cookies-next 包内置了类似于 react-cookie 包的功能。这些功能可用于设置和删除 cookie。让我们用以下代码创建用于设置和删除 cookie 的处理函数:
// 添加 cookie
const addCookie = () => {
setCookie('user', true, {
path: '/',
});
router.replace('/');
};
// 删除 cookie
const removeCookie = () => {
deleteCookie('user', {
path: '/',
});
router.replace('/');
};
完成后,你可以通过将其绑定到在 cookie 存在时渲染的不同按钮来测试它。除了 getServerSideProps 和 API 路由外,你还可以在应用程序的服务器端使用 cookies-next 包。
让我们看一个例子,用户收到一些信息,对其进行验证,然后设置一个 cookie 来表示信息的合法性,所有这些都在 API 路由上完成。
实现 API 路由
继续在 ./pages/api/verify-otp.ts 中创建一个新的 API 路由。在文件中,用以下代码创建一个基本的处理函数:
export default function handler (
req: NextApiRequest,
res: NextApiResponse
) {
return;
}
我们将设置 cookie 来表示用户的可信度,并在特定时间后过期。更具体地说,如果有某种验证(比如用于检查凭证的数据库或某些 OTP 逻辑),它就会过期。处理函数如下:
if (
req.method === 'POST' // 只允许 POST 请求
) {
// 从请求体中获取用于验证的凭证
const { name } = req.body;
// OTP 验证逻辑
// 设置 cookie
setCookie('authorize', true, {
req,
res,
maxAge: 60 * 60 * 24 * 7, // 1 周
path: '/',
});
// 响应状态和消息
return res.status(200).json({
message: `${name} is authorized to access`,
authorize: true,
code: '20-0101-2092',
});
}
在这里,cookie 会在一周后过期,并要求用户重新验证。在验证成功后,API 会返回一个状态为 200 的消息,其中包含可以在前端显示的相关数据。
从前端访问 API 路由
现在,我们尝试从前端访问这个路由。该函数只能在用户首次注册时触发。使用以下代码创建函数:
const verifyOTP = async (name: string) => {
const response = await fetch('/api/verify-otp', {
method: 'POST',
body: JSON.stringify({ name }),
});
const data = await response.json();
if (data.authorize) {
setAuthorization(true);
setLaunchCode(data.code);
} else {
setAuthorization(false);
alert('Invalid OTP');
}
};
我们可以使用 useState Hook 来存储来自 API 路由的数据,并基于 isAuthorized 变量有条件地渲染按钮。使用以下代码:
const [isAuthorized, setAuthorization] = useState(false);
const [launchCode, setLaunchCode] = useState('');
完成这些后,试试到目前为止写的代码。你可以通过打开开发者工具并选择 Application 部分来检查 cookie 是否存在。
Next.js 中间件中的 Cookie
Next.js 中间件设计在 Pages Router 和 App Router 中是一致的。因此,在中间件中处理 cookie 的方式对两者都是相同的。例如,我们可以通过中间件请求获取和删除 cookie,如下所示:
// middleware.js
export function middleware(request) {
// 获取 Cookie
let cookie = request.cookies.get("cookieName");
console.log(cookie);
// 获取所有 Cookie
const allCookies = request.cookies.getAll();
console.log(allCookies);
// 删除 Cookie
request.cookies.delete("cookieName");
}
要在中间件中设置新的 cookie,我们还可以利用 NextResponse API,如下所示:
// middleware.js
import { NextResponse } from "next/server";
export function middleware(request) {
const response = NextResponse.next();
// 设置 cookies
response.cookies.set("foo", "bar");
// 或者
response.cookies.set({
name: "foo",
value: "bar",
path: "/",
// . . .
});
return response;
}
这样,cookie 就会全局设置在用户的浏览器中,并可以在我们之前演示的 Next.js 页面中访问。
常见的 Next.js Cookie 问题及解决方案
sameSite 功能是 cookie 的一个重要属性,但它也可能在生产级应用程序中造成问题:
sameSite 功能仅表明是否可以通过具有不同源的其他网站检索 cookie。理想情况下,这应该是准确的,因为它只提供了一层防御跨站攻击的保护。
为了确定方案和域名的最后部分是否匹配,sameSite 浏览器机制会分析目标 URI 和来自客户端的请求:
由于 sameSite 参数默认设置为 true,如果你是一个经常使用其他智能手机并通过开发服务器托管在本地主机上的私有 IP 地址连接的开发者,cookie 将不会被注册。
domain 属性是 cookie 的一个关键但有时容易混淆的元素。这个属性决定了哪些域可以访问该 cookie。如果没有指定域,cookie 的默认域分配将是最初生成它的域。
这就是为什么在多个子域试图访问相同 cookie 的情况下,建议设置 domain 属性:
结论
Cookie 对于 Web 开发至关重要。react-cookie 和 cookies-next 包由于其独特的特性和优势,非常适合各种使用场景。
react-cookie 更受欢迎,提供简单易用的 API 和与 React 框架的良好兼容性。相比之下,cookies-next 是一个专门为 Next.js 创建的相对较新的包,提供服务器端渲染功能和改进的安全措施。
另一个令人惊喜的事实 - 这是给所有注重包大小的开发者的 - 就是与 react-cookie 相比,cookies-next 的包体积更小。这本质上使它在你的下一个项目中更具吸引力! 🎉