iT邦幫忙

2021 iThome 鐵人賽

DAY 15
0
Modern Web

從零開始學習 Next.js系列 第 15

Day15 - 在 Next.js 做 JWT 驗證,使用既有的 Backend API - PART 1

NextAuth + JWT authentication

雖然 Next.js 的定位是一個全端框架,能夠撰寫 API route 並且在裡面串接資料庫,如果是一個小專案可能綽綽有餘。但是 Next.js 畢竟是一個近期才後來居上的框架,未來還有許多的發展空間,你可能不會選擇把 Next.js 當作全端框架使用,而是截長補短使用 SSR、SSG 的功能,或是其圖片載入優化的功能,後端可能會有其他的選擇,像是 golang、python、ruby 等等語言的後端框架。

況且在轉移到 Next.js 之前,許多的專案已經有了後端的程式,API 服務、金流、商業邏輯的程式碼已經有一定程度的規模,沒必要使用 Next.js 把全部的 API 重寫,甚至重寫的時間成本、效能都必須要納入考量,繼續使用既有的後端服務可能會是團隊的首要選擇。

在這篇文章中,我們假定已經存在後端的服務,並且我們想要使用後端服務存取資料,但是如果想要打 API 獲取資料,必須能夠通過使用者驗證。

啟動一個 JWT JSON server

https://github.com/leochiu-a/fake-api-jwt-json-server

在這篇文章中,將示範如何將 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 + JSON server

一個必須通過 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 測試 JSON server

大家對於 postman 應該不陌生,我們經常會用它來測試 REST API,今天我們將使用它來測試 JSON server。在打產品資料的 API — /products 之前,我們必須要先登入驗證使用者的資訊,因此要先打 POST /auth/login 這支 API,並在 body 中帶入 email 與 password 的資訊。而在驗證成功後,伺服器會回傳一組 access_token

截圖 2021-08-30 下午10.28.44.png

我們將 access_token 組合成 Bearer <ACCESS_TOKEN> 的形式,帶入到 headersAuthorization 這個欄位中,如此一來,就可以順利使用 GET /products 取得產品列表資料:

POST /auth/login

但是,如果帶入了錯誤的 Authorization 的資訊,伺服器將會回傳 401,告知沒有存取的權限:

GET /products with JWT

在 NextAuth 定義 JWT 驗證的 API routes

在 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 的 getSessionuseSession 拿到驗證的訊息,但是由於預設的 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 或是在客戶端呼叫 useSessiongetSession 時會被執行,但是 useSession 只有在無法透過 context 取得 session 時才會執行,因為 SSR 可以透過 props 傳遞 session 到 component 中,再由 NextAuth 提供的 Context Provider 設定數值, useSession 便可以直接從 context 中取值,此時就不會需要跟伺服器交互驗證, jwt() 也就不會被執行。

我們可以在 jwt() 中的第一個參數 token 設定 accessToken ,這個 token 會在其他的 callback 中被用到。

session() 的執行時機點會在 jwt() 之後, 它包含了兩個參數,第一個參數 session 表示的是 getSessionuseSession 可以取得的物件,像是我們希望可以取得 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;
    },
  },
});

Reference


上一篇
Day14 - 在 Next.js 如何做 authentication
下一篇
Day16 - 在 Next.js 做 JWT 驗證,使用既有的 Backend API - PART 2
系列文
從零開始學習 Next.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言