雖然 Next.js 的定位是一個全端框架,能夠撰寫 API route 並且在裡面串接資料庫,如果是一個小專案可能綽綽有餘。但是 Next.js 畢竟是一個近期才後來居上的框架,未來還有許多的發展空間,你可能不會選擇把 Next.js 當作全端框架使用,而是截長補短使用 SSR、SSG 的功能,或是其圖片載入優化的功能,後端可能會有其他的選擇,像是 golang、python、ruby 等等語言的後端框架。
況且在轉移到 Next.js 之前,許多的專案已經有了後端的程式,API 服務、金流、商業邏輯的程式碼已經有一定程度的規模,沒必要使用 Next.js 把全部的 API 重寫,甚至重寫的時間成本、效能都必須要納入考量,繼續使用既有的後端服務可能會是團隊的首要選擇。
在這篇文章中,我們假定已經存在後端的服務,並且我們想要使用後端服務存取資料,但是如果想要打 API 獲取資料,必須能夠通過使用者驗證。
在這篇文章中,將示範如何將 Next.js 跟既有的 API 服務串接,並且每次打 API 時,都必須通過 JWT 的驗證,否則將無法成功獲得資料。
筆者將可以運行的 JWT JSON server 放在 GitHub 上, 想要跟著一起動手做的讀者們可以到 GitHub 上下載,並且執行以下指令啟動 JSON server:
// 安裝套件
yarn
// 啟動 fake jwt server
yarn start-auth
這個 JSON server 包含了兩個主要的功能,其一是包含了在前幾章中我們使用過的「產品資料」,這些資料讀者可以在 https://fakestoreapi.com/ 這個網站中找到。如同前面使用的 API endpoints, JSON server 讓我們能夠快速地建立 REST API,這個 JSON server 提供了以下兩個產品的 API endpoints:
GET /products
GET /products/:id
另一個功能則是使用以上兩個 API endpoints 時都必須通過 JWT 的驗證,接著,我們將了解怎麼使用 JWT 驗證,並且成功獲得產品資料。
一個必須通過 JWT 驗證的後端服務,在每次打 API 實則必須帶上 JWT 的資訊,伺服器端在收到 JWT 後才能夠驗證其使用者的權限。而獲得 JWT 的方式一般來說都是透過登入,並驗證使用者的帳號與密碼,成功後回傳一組 accessToken
,以下為 JSON server 提供的登入 API endpoints:
POST http://localhost:8000/auth/login
在打上述的 /auth/login
API 登入時, body
必須帶入下格式的資料,包含使用者的 email 與 password:
{
"email": "admin@email.com",
"password": "password1234"
}
在登入成功後,API 會反回一組 access_token
:
{
"access_token": "<ACCESS_TOKEN>"
}
在請求 /products
資料時,必須在 header 加入以下資訊才能通過 JWT 驗證:
Authorization: Bearer <ACCESS_TOKEN>
大家對於 postman 應該不陌生,我們經常會用它來測試 REST API,今天我們將使用它來測試 JSON server。在打產品資料的 API — /products
之前,我們必須要先登入驗證使用者的資訊,因此要先打 POST /auth/login
這支 API,並在 body
中帶入 email 與 password 的資訊。而在驗證成功後,伺服器會回傳一組 access_token
:
我們將 access_token
組合成 Bearer <ACCESS_TOKEN>
的形式,帶入到 headers
的 Authorization
這個欄位中,如此一來,就可以順利使用 GET /products
取得產品列表資料:
但是,如果帶入了錯誤的 Authorization
的資訊,伺服器將會回傳 401,告知沒有存取的權限:
在 Next.js 中搭配 NextAuth 進行 JWT 驗證的流程會與常見的直接跟伺服器交互驗證的方式不太一樣, NextAuth 需要建構在 API routes 中,這意味者用戶端的 JWT 流程會是先打 API routes,然後 API routes 再跟後端交互驗證。
之所以與一般 React SPA 的應用不一樣的原因是 Next.js 提供了 SSR 的選項,有些頁面會使用到 SSR,會在渲染伺服器端先獲取資料,而獲取資料必須經過 JWT 驗證,此時必須拿到使用者驗證的 session,所以 NextAuth 會做在 API routes 中是有原因的,如此一來, SSR 就可以有方法能夠取得使用者的驗證資訊。
我們先來看看在 API routes 會如何跟後端驗證使用者,以下假定為使用者登入的流程,API routes 打一支由後端提供驗證的 API,在我們範例中為 POST /auth/login
,其 body 必須帶有使用者的帳號與密碼,在驗證成功後會獲得 accessToken
。
以下為範例程式,使用原生的 fetch
打後端的 API,最後會傳的 data
即是一個包含 accessToken
的物件:
const body = {
email: "admin@email.com",
password: "password1234",
};
fetch("http://localhost:8000/auth/login", {
method: "POST",
body: JSON.stringify(body),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((data) => console.log(data));
因此,現在我們知道了怎麼跟後端進行驗證,這些程式將會是在 API routes 的一部分,接著繼續看怎麼設定 NextAuth。
NextAuth 的 API routes 是定義在 pages/api/auth/[...nextauth].ts
,以下則是 NextAuth 的基本設定,與一般 API routes 的寫法不太一樣,會直接 export default
NextAuth 的建構物件,我們依序來了解這些設定包含哪些內容。
先看到一開始設置了 jwt: true
的選項,因為 NextAuth 有提供兩種驗證的方式,一種是 database session 的驗證,另一種則是 JWT 驗證,而如果沒有設定 database
的選項,則 jwt
會直接被定為 true
,但是明確的定義這個參數則是可以有效地增加可維護性。
而 providers
則是使用者驗證的方式,它是一個陣列的型別,可以傳入 Google、Facebook、Apple 等等的驗證方式,也可以是客製化驗證方法,如下方的範例。在這個範例中可以看到用到了上方與後端交互驗證的 fetch
,這些驗證的流程都會被定義在 Providers.Credentials
的非同步 authorize
方法中,假設如果驗證成功了,在這個方法中則必須回傳一個代表使用者已經驗證的物件,例如 { name: 'admin', accessToken: 'blablabla...' }
,反之,如果回傳的是 false/null
則代表該使用者無法通過驗證。
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
export default NextAuth({
session: {
jwt: true,
},
providers: [
Providers.Credentials({
async authorize(credentials) {
const res = await fetch("http://localhost:8000/auth/login", {
method: "POST",
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json",
},
});
const user = await res.json();
if (res.ok && user) {
return user;
}
return null;
},
}),
],
// callback
});
在驗證成功後,不論是在渲染伺服器端可以在 SSR 時獲得資料,或是在用戶端的 component 動態地獲得資料,每次跟伺服器的 API 請求都必須帶有驗證的訊息,所以我們會透過 NextAuth 的 getSession
或 useSession
拿到驗證的訊息,但是由於預設的 session 包含的資訊是固定的,我們可以從型別定義中找到預設的物件:
export interface DefaultSession extends Record<string, unknown> {
user?: {
name?: string | null;
email?: string | null;
image?: string | null;
};
expires?: string;
}
因此,如果沒有其他的設定就無法在 session 中取得 accessToken
,所以我們要在 NextAuth 中另外設定兩個 callback,才能讓 getSession()
或 useSession()
都能夠取得 accessToken
。
以下要設定的兩個 callback 分別為 session()
與 jwt()
,它們執行的順序是先 jwt()
然後才是 session()
,而 jwt()
是在 jwt: true
時才會被執行。
jwt()
的執行時機為使用者 signIn
或是在客戶端呼叫 useSession
或 getSession
時會被執行,但是 useSession
只有在無法透過 context 取得 session 時才會執行,因為 SSR 可以透過 props
傳遞 session
到 component 中,再由 NextAuth 提供的 Context Provider 設定數值, useSession
便可以直接從 context 中取值,此時就不會需要跟伺服器交互驗證, jwt()
也就不會被執行。
我們可以在 jwt()
中的第一個參數 token
設定 accessToken
,這個 token
會在其他的 callback 中被用到。
session()
的執行時機點會在 jwt()
之後, 它包含了兩個參數,第一個參數 session
表示的是 getSession
或 useSession
可以取得的物件,像是我們希望可以取得 accessToken
,因此我們必須在 session
中另外設定這個屬性;第二的參數 token
則是由 jwt()
回傳的物件,在 jwt()
中設定的屬性則可以在這裡取得,因此我們就可以透過這兩個參數設定我們期望可以取得的 session 的物件長什麼樣子。
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
export default NextAuth({
// ... session and providers
callbacks: {
async session(session, token) {
session.accessToken = token.accessToken;
return session;
},
async jwt(token, user) {
if (user) {
token.accessToken = user.access_token;
}
return token;
},
},
});