身份验证
会话策略是指在用户访问应用程序时,应用程序如何管理用户会话的方式。在 Auth.js 中,支持两种主要的会话策略:基于 JWT 的会话和基于数据库的会话。
两种会话策略都有各自的优缺点,你需要根据你的应用程序需求来评估。
JWT 会话
Auth.js 可以使用 JSON Web Tokens(JWT)创建会话,这是 Auth.js 的默认会话策略。用户登录后,会在 HttpOnly cookie 中创建一个 JWT。将 cookie 设置为 HttpOnly 可以防止 JavaScript 通过客户端访问它(例如通过 document.cookie),从而使攻击者更难窃取值。此外,JWT 使用服务器端唯一知道的密钥进行加密。因此,即使攻击者窃取了 cookie 中的 JWT,他们也无法解密它。结合短期的过期时间,这使 JWT 成为创建会话的安全方式。
用户退出时,JWT 将从 cookies 中删除,并销毁会话。
优点
- JWT 作为会话不需要数据库存储会话,这可以更快、更便宜地运行,并且更容易扩展。
- JWT 会话与新的运行时(如 Edge)的兼容性最好。
- 使用这种策略需要的资源较少,因为不需要管理额外的数据库/服务。
- 您可以使用创建的令牌在同一域上的服务和 API 之间传递信息,而无需联系数据库以验证所包含的信息。
- 您可以使用 JWT 安全地存储信息,而不会将其暴露给在您的网站上运行的第三方 JavaScript。
缺点
- 在编码到期之前,无法使 JSON Web Token 失效 - 这样做需要维护一个已失效令牌的服务器端黑名单(至少直到它们真正过期),并在每次提供令牌时检查每个令牌是否在列表中。 Auth.js 将销毁 cookie,但是如果用户在其他地方保存了 JWT,则它将有效(服务器将接受它)直到过期。(在使用 JSON Web Tokens 作为会话令牌时,使用较短的会话过期时间,以便更早地使会话失效并简化此问题。)
- Auth.js 启用高级功能以减轻使用较短会话到期时间对用户体验的不利影响,包括自动会话令牌轮换,可选发送保持活动消息(会话轮询)以防止短暂会话过期(如果有窗口或选项卡打开),后台重新验证,以及自动选项卡/窗口同步,会在会话状态更改时或窗口或选项卡获得或失去焦点时保持窗口同步。
- 与数据库会话令牌一样,JSON Web Tokens 在您可以存储的数据量上是有限制的。通常,每个 cookie 的限制约为 4096 字节,尽管确切的限制在不同浏览器之间会有所不同。您尝试在令牌中存储的数据越多,您设置的其他 cookie 越多,您将越接近此限制。 Auth.js 实现了会话 cookie 分块,以便将超过 4kb 限制的 cookie 进行分割和重新组装。然而,由于这些数据需要在每个请求上传输,您需要注意您想使用这种技术传输多少数据。
- 即使配置正确,存储在加密 JWT 中的信息也不应假设在某个时候是不可能解密的 - 例如由于发现缺陷或技术进步而导致。存储在加密 JSON Web Token(JWE)中的数据可能会在某个时候被泄露。建议是生成具有高熵的秘密。
数据库会话
作为 JWT 会话策略的替代方案,Auth.js 还支持数据库会话。在这种情况下,Auth.js 在登录后不会保存带有用户数据的 JWT,而是在数据库中创建一个会话。然后将会话 ID 保存在 HttpOnly cookie 中。这与 JWT 会话策略类似,但是不在 cookie 中保存用户数据,而是仅将一个指向数据库中会话的模糊值存储在 cookie 中。因此,每当您尝试访问用户会话时,都会查询数据库以获取数据。
用户退出时,会话将从数据库中删除,并且会话 ID 将从 cookies 中删除。
优点
- 数据库会话可以随时在服务器端进行修改,因此您可以实现一些可能更难(但不是不可能)使用 JWT 策略的功能,例如:“在所有地方退出”,或限制并发登录。
- Auth.js 不关心您使用的数据库类型,我们有一个官方数据库适配器的大型列表,但您也可以自行实现。
缺点
- 数据库会话需要与数据库的往返,因此除非您的连接/数据库已为此做好准备,否则它们可能会在规模上较慢。
- 许多数据库适配器目前尚不与 Edge 兼容,这将允许更快,更便宜的会话检索。
- 设置数据库需要更多的工作量,并且需要额外的服务来管理,相对于无状态的 JWT 策略而言。
效验 Session
ts
async function validateSession(
sessionId: string
): Promise<{ user: User, session: Session } | { user: null, session: null }> {
const [databaseSession, databaseUser] = await this.adapter.getSessionAndUser(sessionId)
if (!databaseSession)
return { session: null, user: null }
if (!databaseUser) {
await this.adapter.deleteSession(databaseSession.id)
return { session: null, user: null }
}
// 效验会话是否过期
if (!isWithinExpirationDate(databaseSession.expiresAt)) {
await this.adapter.deleteSession(databaseSession.id)
return { session: null, user: null }
}
// 检查会话是否需要刷新
const activePeriodExpirationDate = new Date(
// 会话过期时间的一半
databaseSession.expiresAt.getTime() - this.sessionExpiresIn.milliseconds() / 2
)
const session: Session = {
...this.getSessionAttributes(databaseSession.attributes),
id: databaseSession.id,
userId: databaseSession.userId,
fresh: false,
expiresAt: databaseSession.expiresAt
}
if (!isWithinExpirationDate(activePeriodExpirationDate)) {
session.fresh = true
session.expiresAt = createDate(this.sessionExpiresIn)
await this.adapter.updateSessionExpiration(databaseSession.id, session.expiresAt)
}
const user: User = {
...this.getUserAttributes(databaseUser.attributes),
id: databaseUser.id
}
return { user, session }
}