Next-Auth 源码解析

Next-Auth 源码解析

简单介绍一下Next-Auth 源码的结构

目录简介

我们看packages/next-auth/src,这个目录下面是根目录,我们会看到下面的结构

--src
  -- client  // 这个里面主要是封装了fetch 这个方法
  -- core    // 这个是主要的方法,/api/auth/xxx 的api 及页面都是在这个里面定义的
  -- jwt    // 这个里面主要提供了jwt token 加密解密的方法
  -- next   // 这个主要是定义了nextjs中的middleware 的定义
  -- providers  // 提供了各种认证方法的默认配置
  -- react    // 这个是给react 使用的,提供了useSession/getToken等前端的获取更新session 的方法
  -- utils   // 这个是定义了一些辅助方法,解析路由,合并数据等。
  index.ts
  middleware.ts

我们先来看client 这个目录,这个目录里面主要提供了两个东西,一个是对Fetch 的封装,凡是由网络请求的都会调用这个封装。另一个是封装了一个广播事件的东西。监听了localstorage里面的改动。

export function BroadcastChannel(name = "nextauth.message") {
  return {
    /** Get notified by other tabs/windows. */
    receive(onReceive: (message: BroadcastMessage) => void) {
      const handler = (event: StorageEvent) => {
        if (event.key !== name) return
        const message: BroadcastMessage = JSON.parse(event.newValue ?? "{}")
        if (message?.event !== "session" || !message?.data) return

        onReceive(message)
      }
      window.addEventListener("storage", handler)
      return () => window.removeEventListener("storage", handler)
    },
    /** Notify other tabs/windows. */
    post(message: Record<string, unknown>) {
      if (typeof window === "undefined") return
      try {
        localStorage.setItem(
          name,
          JSON.stringify({ ...message, timestamp: now() })
        )
      } catch {
        /**
         * The localStorage API isn't always available.
         * It won't work in private mode prior to Safari 11 for example.
         * Notifications are simply dropped if an error is encountered.
         */
      }
    },
  }
}

export interface BroadcastMessage {
  event?: "session"
  data?: { trigger?: "signout" | "getSession" }
  clientId: string
  timestamp: number
}

这个广播事件,目前的监听者就是在React里面的SessionProvider,它会触发在SessionProvider里面定义的__NEXTAUTH._getSession()方法,这个方法的调用参数是__NEXTAUTH._getSession({ event: "storage" })。而这个方法是为了请求/api/auth/session这个API,来获取session对象。就整个代码而言,这个方法由如下几种可能的调用方式

  • __NEXTAUTH._getSession() 这个是第一次调用
  • __NEXTAUTH._getSession({ event: "storage" }) 这个分支是为了避免循环,或者浏览器其他tab 发消息,来更新
  • __NEXTAUTH._getSession({ event: "visibilitychange" }) 这个是tab 被激活的时候触发
  • __NEXTAUTH._getSession({ event: "poll" }) 这个是轮询刷新session
    源码片段如下
React.useEffect(() => {
    __NEXTAUTH._getSession = async ({ event } = {}) => {
      try {
        const storageEvent = event === "storage"
        // We should always update if we don't have a client session yet
        // or if there are events from other tabs/windows
        if (storageEvent || __NEXTAUTH._session === undefined) {
          __NEXTAUTH._lastSync = now()
          __NEXTAUTH._session = await getSession({
            broadcast: !storageEvent,
          })
          setSession(__NEXTAUTH._session)
          return
        }

        if (
          // If there is no time defined for when a session should be considered
          // stale, then it's okay to use the value we have until an event is
          // triggered which updates it
          !event ||
          // If the client doesn't have a session then we don't need to call
          // the server to check if it does (if they have signed in via another
          // tab or window that will come through as a "stroage" event
          // event anyway)
          __NEXTAUTH._session === null ||
          // Bail out early if the client session is not stale yet
          now() < __NEXTAUTH._lastSync
        ) {
          return
        }

        // An event or session staleness occurred, update the client session.
        __NEXTAUTH._lastSync = now()
        __NEXTAUTH._session = await getSession()
        setSession(__NEXTAUTH._session)
      } catch (error) {
        logger.error("CLIENT_SESSION_ERROR", error as Error)
      } finally {
        setLoading(false)
      }
    }

    __NEXTAUTH._getSession()

    return () => {
      __NEXTAUTH._lastSync = 0
      __NEXTAUTH._session = undefined
      __NEXTAUTH._getSession = () => {}
    }
  }, [])

我们再看看react 这个目录,这个目录里提供了一些列的方法供前端React 项目使用,具体包括如下

SessionProvider组件,一般套在整个App 的最外面,用于给整个应用提供Session 对象

useSession()hook 函数,这个是用来消费SessionProvider 的Session对象,Session对象的类型如下

export type SessionContextValue<R extends boolean = false> = R extends true
  ?
      | { update: UpdateSession; data: Session; status: "authenticated" }
      | { update: UpdateSession; data: null; status: "loading" }
  :
      | { update: UpdateSession; data: Session; status: "authenticated" }
      | {
          update: UpdateSession
          data: null
          status: "unauthenticated" | "loading"
        }

signIn(),这个函数会出发signIn的流程,如果是OAuth,它实际上会去post 到/auth/signin/{provider},

signOut() 这个函数会访问/auth/signout,同时会广播事件,通知其他浏览器的tab,从而实现同时登出。

getSession() 这个是用于获取session对象的

getCsrfToken() 这个是获取xss 防跨站token的,在signIn,signOut,SessionProvider里面必须添加到body里面,

next这个包是专门为nextjs 而准备的,这个目录里面包含了整个包的入口 也就是NextAuth()这个方法。我们主要关注NextAuthApiHandler()这个分支的代码

  • 首先将nextjs 的请求转换成内部的请求数据结构,主要是解析出action, cookie, httpmethod 等,使用的是 toInternalRequest()
  • 接着调用init(),这一步包含初始化options,处理csrfToken(创建或者验证),处理callbackurl(从查询参数里面读取,或者从cookie里面解析),处理callbackurl 的时候会调用我们定义的callbacks.redirect()。这个方法是针对第一次进入,或者回调回来进入的场景,所以有了从查询参数读取,然后保存到cookie, 或者从cookie里面读取,这两种场景,cookie 里面的值是之前写进去的。
  • 构建SessionStore这个对象,主要是用来管理SessionToken这个cookie 的,它支持划分为多个cookie 的情况。由于cookie 的体积过大,会分多个cookie 来存储,会有后缀的 xx.0, xx.1, xx.2 ...
  • 依据httpmethod, 分为get 跟post 两个分支
    • 对于get 请求,分别定义了signIn,signOut,error,veryrequest这些静态页面,如果这些页面提供了自定义的页面,则会重定向到提供的页面上,除了这几个action 之外,还有一些其他的接口,providers,session,csrf,callback,这些主要是用来获取session/token等信息到前端js 中,或者更新token/cookie
    • 对于post 请求,首先一定会验证csrftoken,通过比较body里面的跟cookie里面的csrf token, 来预防跨站。这个里面有个前提是js 在跨站的情况下是拿不到csrf token 的,只有在同域名的情况下才可以拿到。对于signIn,signOut,这个主要是准备cookie 然后跳转到OAuth 站点,callback这个则是为了OAuth 认证通过后会跳回来准备的,Session,这个是给前端js 用于获取session对象或者更新session 对象准备的。其他的就不重要了。

再聊聊这个包对于cookie 的加密,相应的代码是在jwt这个目录下

  • 密钥是来自于process.env.NEXTAUTH_SECRET这个变量,然后密钥是通过如下的加密方法进行加密,其中盐是空字符串
import hkdf from "@panva/hkdf"
async function getDerivedEncryptionKey(
  keyMaterial: string | Buffer,
  salt: string
) {
  return await hkdf(
    "sha256",
    keyMaterial,
    salt,
    `NextAuth.js Generated Encryption Key${salt ? ` (${salt})` : ""}`,
    32
  )
}
  • 通过加密后的密钥用于对token 进行加密签名从而生成cookie.
import { EncryptJWT, jwtDecrypt } from "jose";
export async function encode(params: JWTEncodeParams) {
  /** @note empty `salt` means a session token. See {@link JWTEncodeParams.salt}. */
  const { token = {}, secret, maxAge = DEFAULT_MAX_AGE, salt = "" } = params
  const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
  return await new EncryptJWT(token)
    .setProtectedHeader({ alg: "dir", enc: "A256GCM" })
    .setIssuedAt()
    .setExpirationTime(now() + maxAge)
    .setJti(uuid())
    .encrypt(encryptionSecret)
}

/** Decodes a NextAuth.js issued JWT. */
export async function decode(params: JWTDecodeParams): Promise<JWT | null> {
  /** @note empty `salt` means a session token. See {@link JWTDecodeParams.salt}. */
  const { token, secret, salt = "" } = params
  if (!token) return null
  const encryptionSecret = await getDerivedEncryptionKey(secret, salt)
  const { payload } = await jwtDecrypt(token, encryptionSecret, {
    clockTolerance: 15,
  })
  return payload
}

曾经遇到过clock_Toleranc的问题,可以尝试将源码中的10ms 增大,源码core>lib>oAuth>client.ts


  // allow a 10 second skew
  // See https://github.com/nextauthjs/next-auth/issues/3032
  // and https://github.com/nextauthjs/next-auth/issues/3067
  client[custom.clock_tolerance] = 10
posted @ 2024-04-22 23:04  kongshu  阅读(181)  评论(0编辑  收藏  举报