Day14 - 在 Next.js 如何做 authentication

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


<<:  [DAY15]组图(2)

>>:  Day 17手势识别GestureDectector

[ Day 16 ] React Hooks 中的 useEffect

昨天介绍了在 Function Component 中该如何操作 state 的方法(附上传送门)...

全方位对比:SmartQuery VS FineReport来自报表工程师的经验

写在前面 相比起BI这些当红炸子鸡概念,报表工具大家可能不太熟悉,希望这篇文章能够给大家提供一些新的...

day 23 - 取号机 AUTO_INCREMENT(MYSQL) > INCR(Redis) > snowflake演算法

取号机制是专案中很常会使用到的项目。在我们的生活中小到饮料店的取餐单、银行的号码牌, 大到公文系统的...

[Day-6] R语言 - 怎麽选 分群群数 & 距离? ( Clustering Distance & Index )

您的订阅是我制作影片的动力 订阅点这里~ 若内容有误,还请留言指正,谢谢您的指教 ...

OpenID Connect

OAuth 2.0 指定了存取资源的存取令牌,但它没有提供提供身份信息(ID Token)的标准方...