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 对象准备的。其他的就不重要了。
- 对于get 请求,分别定义了
再聊聊这个包对于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