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:没有同源的限制。