iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

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

Day14 - 在 Next.js 如何做 authentication

  • 分享至 

  • xImage
  •  

Authentication

在 web 應用中經常需要驗證使用者的權限,例如登入與未登入能看到的頁面可能會不同,需要透過一些方式驗證使用者是否能夠進入該頁面。在 Next.js 中對於 SSG 與 SSR 有不同的驗證方式,以下將會以 next-auth 這個套件為主軸,介紹在 SSG 與 SSR 的頁面中如何做驗證。

在官方文件中還有提到 with-iron-session 這一個套件,先從下載量來看, npm trend 中可以看見 next-auth 的下載量是高於 next-iron-session 的。雖然 next-iron-session 被開源的時間較晚,但如果從 2020 年來看,下載量在當時並沒有差距太多,都落在每週幾千的次的下載量,但是隨著時間推進,到了 2021 年 next-auth 的下載量是高過很多的。

next-auth v.s. with-iron-session

而筆者認為 next-iron-session 的下載量較低的原因是它的寫法可能較不容易被接受,如果使用這個套件,將需要大幅度更動 getServerSideProps 的寫法:

import withSession from '../lib/session'

export const getServerSideProps = withSession(async function ({ req, res }) {
    const user = req.session.get('user')
    // ...
}

如上面的例子,想要取得 session 的訊息,必須在 function 外用 HOC 的方式包一層 withSession ,才能透過 req.session 取得驗證權限的訊息,這種方法肯定很難讓人被接受。

next-auth 的寫法更像似遵照 hook 的思維,可以使用 useSessiongetSession 取得驗證權限的訊息,而不用破壞原本 Next.js 的程式碼結構。

在 SSR、SSG 與 CSR 中驗證權限的方式

在 Next.js 由於可以混用 SSR、SSG 與 CSR 三種不同的頁面,所以針對不同的頁面,也會有不同的驗證方式。 SSG 與 CSR 的驗證方式很接近,都是在用戶端驗證,如果驗證失敗則可以使用 next/router 轉址到其他頁面;而 SSR 則是在伺服器端驗證,驗證失敗則可以直接在 getServerSideProps 轉址到其他頁面。

從 web vitals 的角度來看,由於 SSR 會在伺服器端做驗證,所以驗證階段必須越快越好,否則會影響 TTI (Time to Interactive) 與 TTFB (Time to First Byte)。果驗證較慢,不如可以考慮將頁面轉換成 SSG 的形式,有利於提升使用者體驗。

NextAuth 簡介

next-auth 是一個提供完整解決方案的套件,包括支援 Google、 Facebook、 Twitter 等第三方登入的方法,也可以使用自定義的驗證方式,支援 OAuth 1.0、1.0A 與 2.0 的驗證,也同時支援 JSON Web Token 與 database session 等的驗證模式,是完整性很高的套件。

next-auth 的 API 與前面提到的 next-iron-session 相較起來更容易閱讀,不僅提供 hook 的 API,而且還有提供在用戶端與伺服器端端都能使用的 getSession ,讓使用者驗證更容易實作。

如果你需要的是用戶端的驗證解決方案, next-auth 可能無法滿足你的需求,因為 next-auth 使用的是伺服器端驗證,在一個頁面中必須使用 next-auth 的 client API - signIn 進行登入驗證,而登入驗證會導入到 next-auth 所屬的 API routes - api/auth/[...nextauth].ts ,然後再根據不同的情況使用第三方登入驗或自定義的驗證。

NextAuth 的範例程式

NextAuth 的官方 GitHub 有 next-auth-example ,如果需要一個模板,可以從跟著 README 玩玩看。

以下我們來看看在 Next.js 中幾個 next-auth 基本的使用方法,包括設定 API routes、在 component 中使用 hook API ,以及使用 context 讓 Next.js 全域都能取得使用者的 session。

新增 API routes

next-auth 是一個在伺服器端的驗證套件,首先,我們必須在定義一個 API routes 於 pages/api/auth/[...nextauth].ts ,這個 API routes 與常見的寫法不太一樣,使用的是 next-auth 的 API ,其中包含驗證的 providers ,例如 Google、Facebook、Twitter 等等,如果需要將使用者的資料儲存在資料庫中,可以在 database 這個參數加上 url。

實際上 NextAuth 這個 API 的能夠傳入參數有很多,如果需要知道更多資訊的讀者,可以直接到官方文件中瀏覽。

import NextAuth from "next-auth";
import Providers from "next-auth/providers";

export default NextAuth({
  // 使用 Google 驗證
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
  ],

  // 如果需要將使用者的資料儲存在資料庫中,可以在 database 這個參數加上 url
  database: process.env.DATABASE_URL,
});

在 React component 中使用 Hooks API

pages/login/index.ts

NextAuth 有提供 hooks API 讓我們使用,在使用上非常方便,只要從 next-auth/client 引入 useSession 後不用做任何的設定就可以使用。

從下面中的範例中,我們還可以看到 NextAuth 也提供了 signInsignOut 的 function,登入時可以呼叫 signIn ,NextAuth 會幫我們打 API routes ,並經過一連串驗證權限的流程,如果驗證成功,我們就可已透過 useSession 拿到驗證後的資料,例如 accessToken

而登出的流程也可以透過 NextAuth 的 signOut 觸發,如此一來,session 中資料就會被清空,使用者便會回到登入前的狀態。

import { signIn, signOut, useSession } from "next-auth/client";

function Login() {
  const [session, loading] = useSession();

  return (
    <>
      {!session && <button onClick={() => signIn()}>登入</button>}
      {session && (
        <>
          <p>使用者 email: {session.user.email}</p>
          <button onClick={() => signOut()}>登出</button>
        </>
      )}
    </>
  );
}

export default Login;

優化 session 的使用

page/_app.ts

由於 NextAuth 是透過 Next.js 的 API routes 驗證使用者,透過上述的 signIn 觸發後, session 會被記錄下來,當我們使用到 Next.js 的 SSR 功能時,會透過 getSession 取得驗證後的一些訊息,例如 accessToken ,我們在一些情況需要在打 API 時帶入 accessToken 才能夠成功獲取資料。

在獲取資料,並且成功由伺服器渲染完頁面後,使用者後續還會跟頁面互動,而在切換頁面時,如果需要不斷地跟伺服器交換使用者是否已認證的訊息,難免會讓一些互動會有些延遲,所以為了解決這個情況,NextAuth 提供了 Context API,讓我們可以在 getServerSideProps 中傳入 session ,如此一來,在頁面切換時,取得使用者的驗證訊息就會更有效率,像是在 accessToken 過期之前,不必再次透過伺服器驗證使用者,而是直接從 Context 取得 accessToken ,藉此提升使用者體驗。

import { AppProps } from "next/app";
import { Provider } from "next-auth/client";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  );
}

export default MyApp;

Reference


上一篇
Day13 - 重構產品頁面 API,使用 API routes - feat. MongoDB
下一篇
Day15 - 在 Next.js 做 JWT 驗證,使用既有的 Backend API - PART 1
系列文
從零開始學習 Next.js30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言