終于理解了 Next.js 中的 Cookie
Cookie 是瀏覽器中默默存在的數(shù)據(jù)塊。雖然有些 Cookie 會(huì)侵犯用戶隱私,但其他一些則試圖通過跟蹤用戶的瀏覽習(xí)慣、偏好等來改善瀏覽體驗(yàn)。Cookie 在許多場景都很有用,包括身份驗(yàn)證、改善用戶體驗(yàn)和加快響應(yīng)時(shí)間。
在本文中,我們將探討如何在 Next.js 的服務(wù)器組件和客戶端組件以及中間件中管理 Cookie。我們還將介紹兩個(gè)可以在 Next.js Pages Router 中設(shè)置 Cookie 的包,并將它們應(yīng)用到實(shí)際用例中。
要跟隨本教程學(xué)習(xí),可以前往此 GitHub 倉庫。
https://github.com/GeoBrodas/cookies-with-nextjs
Cookie:福祉還是禍害?
Cookie 是 Web 應(yīng)用程序放置在用戶計(jì)算機(jī)上的小數(shù)據(jù)塊,用于存儲(chǔ)狀態(tài)信息,如用戶偏好或會(huì)話管理,也用于跟蹤目的。
近年來,關(guān)于 Cookie 的討論引發(fā)了很多爭議,因?yàn)樗鼈兗扔袃?yōu)點(diǎn)也有缺點(diǎn)。
一方面,Cookie 可以幫助你輕松存儲(chǔ)用戶的個(gè)性化數(shù)據(jù),讓你能更好地為每個(gè)用戶定制用戶體驗(yàn)。另一方面,Cookie 跟蹤用戶在線行為的能力引發(fā)了隱私擔(dān)憂。
因此,現(xiàn)在有了一些標(biāo)準(zhǔn)和法規(guī),要求 Web 應(yīng)用程序披露其 Cookie 使用情況,并讓用戶選擇是否退出。
鑒于 Next.js 的多功能性,有多種方法可以管理 Cookie。在此之前,我們只需要在頁面、API 路由和中間件中管理 Cookie。然而,Next 13 中添加的服務(wù)器組件引入了更多新技術(shù)。
客戶端 vs 服務(wù)器端 Cookie
很多人問的一個(gè)問題是客戶端和服務(wù)器端 Cookie 是否有區(qū)別。
Cookie 可以通過客戶端和服務(wù)器端操作創(chuàng)建。服務(wù)器端 Cookie 通常是通過 HTTP 標(biāo)頭創(chuàng)建和訪問的。無論如何創(chuàng)建,Cookie 都存儲(chǔ)在用戶的瀏覽器中,可以直接在客戶端訪問。
但是,httpOnly Cookie 是個(gè)例外。如果你創(chuàng)建啟用了 httpOnly 屬性的 Cookie,這些 Cookie 就不能通過客戶端操作直接訪問,從而降低XSS攻擊的風(fēng)險(xiǎn)。
在服務(wù)器組件中處理 Cookie:Next.js App Router
我們首先探討如何在使用 Next.js App Router 的服務(wù)器組件中訪問和修改 Cookie。要繼續(xù),請運(yùn)行以下命令創(chuàng)建一個(gè)新的 Next 應(yīng)用:
npx create-next-app@latest next-cookie
在安裝過程中,選擇你偏好的配置。不過,別忘了為"use App router?"選項(xiàng)選擇"Yes"。完成基本設(shè)置后
如何獲取 Cookie
要在服務(wù)器組件中讀取傳入的請求 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;
在本例中,如果沒有存儲(chǔ)帶有user_id標(biāo)簽的 cookie,我們將看到一個(gè)空白屏幕。但是,如果有多個(gè) cookie 與此標(biāo)記匹配,userId將被設(shè)置為第一個(gè)匹配,并顯示在瀏覽器上。
要獲取與某個(gè)名稱匹配的所有 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;
在這個(gè)更新的示例中,如果有多個(gè)帶有user_id標(biāo)記的 cookie,我們就會(huì)遍歷它們,并顯示每個(gè) cookie 的名稱和值。
如何設(shè)置 cookie
我們可以通過 cookies().set() 方法設(shè)置新的 Cookie。但是,由于 HTTP 不允許在流式傳輸開始后設(shè)置 Cookie,我們只能在服務(wù)器操作(Server Actions)或 API 路由中修改 Cookie 值(設(shè)置和刪除)。以下是一個(gè)例子:
// 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;
在這個(gè)例子中,我們用"use server"語句標(biāo)記了設(shè)置 Cookie 的函數(shù),將其指定為服務(wù)器操作。然后我們渲染了一個(gè)表單,允許用戶選擇首選主題。用戶提交表單后,我們獲取他們選擇的主題值并將其設(shè)置到 theme cookie 中。
此外,我們可以使用以下語法在設(shè)置新 Cookie 時(shí)傳入額外選項(xiàng):
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),
});
這樣,我們可以創(chuàng)建一個(gè) httpOnly cookie 并設(shè)置 cookie 的路徑、最大年齡和過期日期。
如果你使用的 Next.js 版本低于v14,在使用服務(wù)器操作時(shí)可能會(huì)遇到以下錯(cuò)誤:
Error:
× To use Server Actions, please enable the feature flag in your Next.js config.
要修復(fù)這個(gè)問題,只需更新你的 next.config.js 文件以啟用實(shí)驗(yàn)性的 serverActions,如下所示:
const nextConfig = {
experimental: {
serverActions: true,
},
// . . .
};
module.exports = nextConfig;
但是,如果你使用的是 Next.js v14 或更新版本,則不會(huì)遇到此類錯(cuò)誤。
如何刪除 Cookie
我們可以使用 cookies().delete(name) 方法刪除 Cookie。但是,和設(shè)置 Cookie 一樣,我們只能在服務(wù)器操作或 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;
通過上面這個(gè)例子,當(dāng)用戶點(diǎn)擊按鈕提交表單時(shí),theme cookie 將從瀏覽器中刪除。
Next.js 路由處理程序和 API 路由中的 Cookie
在 Next.js 路由處理程序(即 /app 目錄的 API 路由等價(jià)物)中,我們可以自由使用我們剛剛介紹的所有 cookie 方法,而無需創(chuàng)建服務(wù)器操作。你可以在下面看到一個(gè)例子:
// 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");
// 設(shè)置 Cookie
cookies().set("cookieName", "value");
// 刪除 Cookie
cookies().delete("cookieName");
return new Response("Hello, World!", {
status: 200,
});
}
另外,你也可以在路由處理程序和 API 路由中使用傳統(tǒng)的 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));
}
在這個(gè)例子中,我們直接從請求對象中獲取 theme cookie 并將其作為 API 響應(yīng)返回。
在 Next.js Pages Router 中處理 Cookie
現(xiàn)在,讓我們看看如何在經(jīng)典的 Next.js Pages Router 中管理 Cookie。首先,創(chuàng)建一個(gè)新的 Next.js 應(yīng)用程序,并確保在配置過程中選擇 pages router。
在 Next.js 中使用 react-cookie
我們要探索的第一個(gè)包是 react-cookie。這個(gè)包旨在幫助你在 React 應(yīng)用程序中加載和保存 cookie。為了嘗試一下,我們將創(chuàng)建一個(gè)簡單的應(yīng)用程序來跟蹤注冊用戶。
使用以下代碼安裝 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;
我們首先添加一個(gè) useEffect Hook 來在加載時(shí)獲取所有 cookie:
import { useCookies } from 'react-cookie';
const Home: NextPage = () => {
const [cookies, setCookie, removeCookie] = useCookies(['user']);
useEffect(() => {
console.log('Cookies: ', cookies);
}, [cookies]);
return (
<div>...</div>
)}
目前,你應(yīng)該還看不到任何 cookie。所以,我們創(chuàng)建一個(gè)使用 setCookie() 函數(shù)來設(shè)置 cookie 的函數(shù):
import { useRouter } from 'next/router';
//...在默認(rèn)函數(shù)內(nèi)部
const router = useRouter();
const setCookieHandler = () => {
setCookie('new-user', true, {
path: '/',
});
router.replace("/");
};
setCookie() 函數(shù)接受三個(gè)參數(shù):鍵名、鍵值和一些配置選項(xiàng)。這些選項(xiàng)包括 MaxAge、Path、Domain、expires 等。在這種情況下使用了 path 選項(xiàng),允許程序從任何位置訪問該 cookie。
如你所見,我們還使用了 useRouter() Hook 通過 replace() 方法重新加載頁面,以避免在歷史堆棧中添加 URL 條目。這樣看起來就像頁面重新渲染了一樣!
隨著我們繼續(xù)前進(jìn),請記住本教程僅專注于演示特定包的功能。因此,我們將假設(shè)你理解認(rèn)證流程等概念。
要了解更多關(guān)于 Next.js 中的認(rèn)證信息,請參考這個(gè)關(guān)于 SuperTokens 的指南。你也可以在這篇文章中回顧認(rèn)證流程。
將函數(shù)綁定到按鈕
接下來,讓我們將這個(gè)函數(shù)綁定到一個(gè)按鈕上。輸入以下代碼:
{!cookies['user'] && (
<button onClick={setCookieHandler} className={styles.button}>
Complete new user registration!
</button>
)}
在這種情況下,只有當(dāng) cookie 存在時(shí)按鈕才會(huì)渲染。運(yùn)行開發(fā)服務(wù)器來看看這個(gè)效果。你可以通過按 Control+Shift+J 打開開發(fā)者工具,然后選擇 Application 部分來直觀地看到這個(gè) cookie。
現(xiàn)在,我們刪除 cookie 以允許用戶退出。首先,編寫另一個(gè)函數(shù):
const removeCookieHandler = () => {
removeCookie('new-user');
router.replace("/");
};
接下來,將它綁定到另一個(gè)只有在 cookie 可用時(shí)才會(huì)渲染的按鈕上。這意味著什么?如果用戶已注冊,cookie 就會(huì)可用。代碼看起來是這樣的:
{cookies['new-user'] && (
<button onClick={removeCookieHandler} className={styles.resetbutton}>
Reset new user registration
</button>
)}
下面是它在應(yīng)用程序中的樣子:
使用 cookies-next 包
接下來,我們來看看如何使用 cookies-next 包。這個(gè)包更適合 Next.js 生態(tài)系統(tǒng),因?yàn)樗梢栽谌魏蔚胤绞褂?- 無論是在 Pages Router 還是 App Router 中,在客戶端,通過 getServerSideProps 在服務(wù)器端,甚至在 Next.js API 路由中。
以下是這兩個(gè)包的對比:
- react-cookie 更加流行,提供簡單易用的 API 并與 React 框架高度兼容
- cookies-next 是專門為 Next.js 創(chuàng)建的相對較新的包,提供服務(wù)器端渲染功能和改進(jìn)的安全措施
另一個(gè)關(guān)于 cookies-next 令人驚喜的事實(shí)(這個(gè)是給那些關(guān)心包大小的開發(fā)者) - 它比 react-cookie 的包體積更小。這使得它在你的下一個(gè)項(xiàng)目中更具吸引力! ??
按照慣例,讓我們首先用以下命令安裝 cookies-next:
npm install cookies-next
cookies-next 包內(nèi)置了類似于 react-cookie 包的功能。這些功能可用于設(shè)置和刪除 cookie。讓我們用以下代碼創(chuàng)建用于設(shè)置和刪除 cookie 的處理函數(shù):
// 添加 cookie
const addCookie = () => {
setCookie('user', true, {
path: '/',
});
router.replace('/');
};
// 刪除 cookie
const removeCookie = () => {
deleteCookie('user', {
path: '/',
});
router.replace('/');
};
完成后,你可以通過將其綁定到在 cookie 存在時(shí)渲染的不同按鈕來測試它。除了 getServerSideProps 和 API 路由外,你還可以在應(yīng)用程序的服務(wù)器端使用 cookies-next 包。
讓我們看一個(gè)例子,用戶收到一些信息,對其進(jìn)行驗(yàn)證,然后設(shè)置一個(gè) cookie 來表示信息的合法性,所有這些都在 API 路由上完成。
實(shí)現(xiàn) API 路由
繼續(xù)在 ./pages/api/verify-otp.ts 中創(chuàng)建一個(gè)新的 API 路由。在文件中,用以下代碼創(chuàng)建一個(gè)基本的處理函數(shù):
export default function handler (
req: NextApiRequest,
res: NextApiResponse
) {
return;
}
我們將設(shè)置 cookie 來表示用戶的可信度,并在特定時(shí)間后過期。更具體地說,如果有某種驗(yàn)證(比如用于檢查憑證的數(shù)據(jù)庫或某些 OTP 邏輯),它就會(huì)過期。處理函數(shù)如下:
if (
req.method === 'POST' // 只允許 POST 請求
) {
// 從請求體中獲取用于驗(yàn)證的憑證
const { name } = req.body;
// OTP 驗(yàn)證邏輯
// 設(shè)置 cookie
setCookie('authorize', true, {
req,
res,
maxAge: 60 * 60 * 24 * 7, // 1 周
path: '/',
});
// 響應(yīng)狀態(tài)和消息
return res.status(200).json({
message: `${name} is authorized to access`,
authorize: true,
code: '20-0101-2092',
});
}
在這里,cookie 會(huì)在一周后過期,并要求用戶重新驗(yàn)證。在驗(yàn)證成功后,API 會(huì)返回一個(gè)狀態(tài)為 200 的消息,其中包含可以在前端顯示的相關(guān)數(shù)據(jù)。
從前端訪問 API 路由
現(xiàn)在,我們嘗試從前端訪問這個(gè)路由。該函數(shù)只能在用戶首次注冊時(shí)觸發(fā)。使用以下代碼創(chuàng)建函數(shù):
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 來存儲(chǔ)來自 API 路由的數(shù)據(jù),并基于 isAuthorized 變量有條件地渲染按鈕。使用以下代碼:
const [isAuthorized, setAuthorization] = useState(false);
const [launchCode, setLaunchCode] = useState('');
完成這些后,試試到目前為止寫的代碼。你可以通過打開開發(fā)者工具并選擇 Application 部分來檢查 cookie 是否存在。
Next.js 中間件中的 Cookie
Next.js 中間件設(shè)計(jì)在 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");
}
要在中間件中設(shè)置新的 cookie,我們還可以利用 NextResponse API,如下所示:
// middleware.js
import { NextResponse } from "next/server";
export function middleware(request) {
const response = NextResponse.next();
// 設(shè)置 cookies
response.cookies.set("foo", "bar");
// 或者
response.cookies.set({
name: "foo",
value: "bar",
path: "/",
// . . .
});
return response;
}
這樣,cookie 就會(huì)全局設(shè)置在用戶的瀏覽器中,并可以在我們之前演示的 Next.js 頁面中訪問。
常見的 Next.js Cookie 問題及解決方案
sameSite 功能是 cookie 的一個(gè)重要屬性,但它也可能在生產(chǎn)級應(yīng)用程序中造成問題:
sameSite 功能僅表明是否可以通過具有不同源的其他網(wǎng)站檢索 cookie。理想情況下,這應(yīng)該是準(zhǔn)確的,因?yàn)樗惶峁┝艘粚臃烙缯竟舻谋Wo(hù)。
為了確定方案和域名的最后部分是否匹配,sameSite 瀏覽器機(jī)制會(huì)分析目標(biāo) URI 和來自客戶端的請求:
由于 sameSite 參數(shù)默認(rèn)設(shè)置為 true,如果你是一個(gè)經(jīng)常使用其他智能手機(jī)并通過開發(fā)服務(wù)器托管在本地主機(jī)上的私有 IP 地址連接的開發(fā)者,cookie 將不會(huì)被注冊。
domain 屬性是 cookie 的一個(gè)關(guān)鍵但有時(shí)容易混淆的元素。這個(gè)屬性決定了哪些域可以訪問該 cookie。如果沒有指定域,cookie 的默認(rèn)域分配將是最初生成它的域。
這就是為什么在多個(gè)子域試圖訪問相同 cookie 的情況下,建議設(shè)置 domain 屬性:
結(jié)論
Cookie 對于 Web 開發(fā)至關(guān)重要。react-cookie 和 cookies-next 包由于其獨(dú)特的特性和優(yōu)勢,非常適合各種使用場景。
react-cookie 更受歡迎,提供簡單易用的 API 和與 React 框架的良好兼容性。相比之下,cookies-next 是一個(gè)專門為 Next.js 創(chuàng)建的相對較新的包,提供服務(wù)器端渲染功能和改進(jìn)的安全措施。
另一個(gè)令人驚喜的事實(shí) - 這是給所有注重包大小的開發(fā)者的 - 就是與 react-cookie 相比,cookies-next 的包體積更小。這本質(zhì)上使它在你的下一個(gè)項(xiàng)目中更具吸引力! ??