NextJs 中使用Next-Auth

发布时间 2023-07-21 16:32:13作者: kongshu

NextJs 中使用Next-Auth

本篇讨论的范畴是Azureb2c 做为provider, token的类型是jwt token.我们讨论在Azureb2c 认证完后,由Next-Auth 负责认证的过程。

Basic Concept

  • Token
    这个就是cookie,它的名字是非https是next-auth.session-token,如果是https则是__Secure-next-auth.session-token,
  • Session
    这个是js中const {data,status}=useSession();中使用的数据。它代表的是js 中的对象。

Session 的获取过程

当在client端调用getSession(),它其实是发请求去/api/auth/session,这个API会从你的cookie 中读取Token,然后解析,接着分别调用AuthOption里面callback 里的jwt(),session()两个回调函数。然后将返回的新的session 对象放到body里面,将更新过的Token塞回cookie,交给客户端。

js 中的session 对象跟 cookie 里面的Token 有什么关系。

我们可以模糊的认为,cookie 里面的Token 就是jwt()回调函数的返回值,加密后存储到cookie里面。而session()回调函数的返回值就返回到前端的session JS 对象中。

Token 与 Session 的同步问题

由于同一个Token在前端有两种形态的存在,也就是cookie 与Session 对象。那么就有很大的可能会出现不同步的情况(例如,cookie 过期了,session 对象还在)。大多数的情况下,前端的Session 是借助与SessionProvider 来实现的。借助这个我们可以使用useSession()的方式来实现session 的共享。

  • next-auth\react里面的SessionProvider, 内部其实是引用了SessionContext,它来给这个Context 提供了数据源。在这个组件首次加载的时候,它会通过getSession()方法获取到最新的session,同时这个过程也会更新cookie。为了更新这个Session对象。其实const {data,status,update}=useSession(),在客户端它放出了第三个参数,当我们调用这个方法是update(newSession|null),我们就可以完成一次session对象的更新与获取过程。
  • 除此之外,它还提供了另外两种途径,
    • 第一,监听了document 的visibilitychange事件,每次窗口被激活后,都会激发session 的刷新。这个是通过refetchOnWindowFocus控制
    • 第二,轮询,refetchInterval 通过这个参数提供时间间隔,定期去刷session。
    • 第三,这个就比较歪门邪道了,通过修改localStorage里面的nextauth.message这个值,也可以触发。例如
    const message={event:'session'}
    localStorage.setItem('nextauth.message',JSON.stringify({ ...message, timestamp: now() }));
    

SessionProvider

这个组件所在的next-auth\react中还额外提供了一些有用的方法,

useSession

const {data,status,update}=useSession(),这个方法其实基本上就是把useContext(SessionContext)

getSession

这个方法是客户端发请求去/api/auth/session这个api 获取session, 注意,这个会返回最新的Session,但是不会更新SessionContext 里面的值。

getCsrfToken

这个方法是用于获取CsrfToken,它其实是访问/api/auth/csrf这个API,获取csrftoken,除了在body中返回csrftoken,它也会降该值写入到cookie中。我一开始有个疑惑,如果csrfToken都写入到cookie中了,那么就失去了csrf的意义了。后来看了源码后发现,对于/api/auth下面的所有POST请求,必须在body中也提供这个csrfToken,后端会依据cookie里面的跟body里面的进行比对。这边简单介绍一下原理。Cookie里面的csrf有两部分组成csrfToken|hash,body里面或者getCsrfToken返回的则是前面的csrfToken这部分,这个csrfToken就是一个简单的32位的随机数。当验证csrfToken的有效性的时候,它会先从cookie里面读取这个cookie,用自己的密钥计算一下csrfToken产生的hash,如果csrfToken有效,则用它与body里面的csrfToken进行比对。这么做的好处是,后端不用管理这个csrfToken。

export function createCSRFToken({
  options,
  cookieValue,
  isPost,
  bodyValue,
}: CreateCSRFTokenParams) {
  if (cookieValue) {
    const [csrfToken, csrfTokenHash] = cookieValue.split("|")
    const expectedCsrfTokenHash = createHash("sha256")
      .update(`${csrfToken}${options.secret}`)
      .digest("hex")
    if (csrfTokenHash === expectedCsrfTokenHash) {
      // If hash matches then we trust the CSRF token value
      // If this is a POST request and the CSRF Token in the POST request matches
      // the cookie we have already verified is the one we have set, then the token is verified!
      const csrfTokenVerified = isPost && csrfToken === bodyValue

      return { csrfTokenVerified, csrfToken }
    }
  }

  // New CSRF token
  const csrfToken = randomBytes(32).toString("hex")
  const csrfTokenHash = createHash("sha256")
    .update(`${csrfToken}${options.secret}`)
    .digest("hex")
  const cookie = `${csrfToken}|${csrfTokenHash}`

  return { cookie, csrfToken }
}

signIn

这个方法是用于登录的,如果我们提供了provider,作为参数,那么它会开启一个登录的流程。例如azureb2c, 它会开启b2c 登录的流程,登录完成后,它会同时触发getSession的逻辑。完成后,前端cookie里面拥有了cookie,js 里面拿到了session 对象

signOut

这个方法主要是用于清除完前端的cookie.清除完后可以跳转其他的页面。

顺带聊一下Cookie 参考

虽然经常被人诟病,但是这个真心是个好东西,用起来省心。之所以被诟病的原因,主要还是安全的问题。CSRF的问题。但是如果不考虑兼容性,最新版的浏览器对于它的安全保证还是非常高的。例如

  • HttpOnly
    这个属性可以确保js 无法读到这个cookie,通过document.cookies
  • Security
    这个确保只有https的连接才能读取
  • Samesite
    有效值 Strict, Lax, None
    Strict: 最严格的模式,只有当前域名,当前的tab的请求才会带上cookie.
    Lax: 比Strict 要宽松一点,除了这些例外, 其他的跟Strict 一样。Form 的Get 请求,Link标签里面的路由,或者预加载。会带上cookie,其他的就跟Strict 一样了。
    None:没有同源的限制。