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": "[email protected]",
  "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: "[email protected]",
  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


<<:  TypeOrm | Repository APIs 用法纪录 2

>>:  安装MinIO并从notebook储存model到MinIO

【後转前要多久】# Day02 HTML - 基本观念复习

基本上网页与HTML、CSS、JavaScript息息相关, 虽然有办法、但很难去完整地分别拆开来讲...

[Day1] 一切从0开始

起点 因为痛过,所以改变 故事是这样开始的,2019年在炎热的7月,我正式加入一个新创团队,一个热腾...

D28 / Compose 可以用来做 Desktop App? - Compose JB

写到最後三天了,想要聊聊和 Android 不完全相关的东西。感谢JetBrains 的开发,和 K...

DAY 19 Big Data 5Vs – Variety(速度) EMR (2)

接续介绍昨天建立的EMR丛集: 建立的丛集可以在左方工具栏的丛集分页找到 步骤的状态可以到「步骤」分...

DAY 18 制作 Nav Bar - dropdown

针对 dropdown 的部分,我们要来细节微调他的 style ,让他符合 vogue 上的设计,...