Session 守卫
本指南介绍 AdonisJS 中基于 session 的身份验证。你将学习:
- 如何配置 session 守卫
- 如何登录和登出用户
- 如何保护路由免受未身份验证用户的访问
- 如何访问已身份验证的用户
- 如何实现「记住我」功能
- 如何防止已登录用户访问仅限访客的页面
概述
session 守卫使用 @adonisjs/session 包来跟踪已登录用户。当用户登录时,其标识符会被存储在 session 中,并向浏览器发送一个 cookie。在后续请求中,session 中间件会读取该 cookie 并恢复身份验证状态。
几十年来,session 和 cookie 一直是 Web 身份验证的标准方案。当你构建服务端渲染的应用,或托管在与 API 同一顶级域名下的 SPA 时(例如你的应用位于 example.com,API 位于 api.example.com),请使用 session 守卫。
配置守卫
身份验证守卫在 config/auth.ts 中定义。以下示例展示了一个 session 守卫的配置:
import { defineConfig } from '@adonisjs/auth'
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'
const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: false,
provider: sessionUserProvider({
model: () => import('#models/user'),
}),
}),
},
})
export default authConfig
sessionGuard 方法创建了
SessionGuard
类的一个实例。它接受一个用户提供程序以及一个可选的「记住我」令牌配置对象。
sessionUserProvider 方法创建了
SessionLucidUserProvider
的实例,该类使用 Lucid 模型在身份验证过程中查找用户。
登录
使用 auth.use('web').login() 方法为用户创建一个 session。该方法接受一个 User 模型实例,并将其标识符存储在 session 中。
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, auth, response }: HttpContext) {
/**
* 从请求体中获取凭据
*/
const { email, password } = request.only(['email', 'password'])
/**
* 使用 AuthFinder mixin 验证凭据
*/
const user = await User.verifyCredentials(email, password)
/**
* 为用户创建 session
*/
await auth.use('web').login(user)
/**
* 重定向到一个受保护的页面
*/
return response.redirect('/dashboard')
}
}
auth.use('web') 方法返回在 config/auth.ts 文件中以 web 名称配置的守卫实例。
登出
使用 auth.use('web').logout() 方法销毁用户的 session。如果用户拥有活跃的「记住我」令牌,该令牌也会被删除。
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async destroy({ auth, response }: HttpContext) {
await auth.use('web').logout()
return response.redirect('/login')
}
}
保护路由
使用 auth 中间件来保护路由,防止未身份验证的用户访问。该中间件在 start/kernel.ts 的命名中间件集合中注册:
import router from '@adonisjs/core/services/router'
export const middleware = router.named({
auth: () => import('#middleware/auth_middleware'),
})
将中间件应用到需要身份验证的路由上:
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', ({ auth }) => {
return `Welcome ${auth.user!.fullName}`
})
.use(middleware.auth())
默认情况下,auth 中间件使用配置中的 default 守卫进行身份验证。若要显式指定守卫,可将它们作为参数传入:
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', ({ auth }) => {
return `Welcome ${auth.user!.fullName}`
})
.use(
middleware.auth({
guards: ['web', 'api'],
})
)
当指定多个守卫时,只要其中任意一个成功验证请求,身份验证即视为成功。
处理身份验证错误
当 auth 中间件无法对请求进行身份验证时,会抛出 E_UNAUTHORIZED_ACCESS 异常。该异常会通过内容协商转换为一个 HTTP 响应:
- 带有
Accept: application/json的请求会收到一个错误对象数组。 - 带有
Accept: application/vnd.api+json的请求会收到按 JSON API 规范格式化的错误。 - 服务端渲染的应用会重定向到
/login。你可以在app/middleware/auth_middleware.ts中自定义此路径。
访问已身份验证的用户
身份验证完成后,可以通过 auth.user 访问用户实例。当使用 auth 中间件、silent_auth 中间件,或手动调用 auth.authenticate() 或 auth.check() 时,该属性会被填充。
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
const user = auth.user!
return await user.getAllMetrics()
})
.use(middleware.auth())
避免使用非空断言
如果你不想使用非空断言操作符(!),可以改用 auth.getUserOrFail() 方法。它会返回用户,或在用户不存在时抛出
E_UNAUTHORIZED_ACCESS
异常:
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
const user = auth.getUserOrFail()
return await user.getAllMetrics()
})
.use(middleware.auth())
检查身份验证状态
使用 auth.isAuthenticated 属性来检查当前请求是否已通过身份验证:
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
if (auth.isAuthenticated) {
return await auth.user!.getAllMetrics()
}
})
.use(middleware.auth())
静默身份验证
silent_auth 中间件的工作方式与 auth 中间件类似,但在用户未通过身份验证时不会抛出异常。请求会正常继续,使你可以在有可用身份验证数据时选择性地使用它。
这对于同时适用于访客和已身份验证用户的页面非常有用,例如一个为已登录用户显示个性化内容的主页。
在路由中间件栈中注册该中间件:
import router from '@adonisjs/core/services/router'
router.use([
// ...其他中间件
() => import('#middleware/silent_auth_middleware'),
])
在 Edge 模板中访问用户
初始化 auth 中间件
会将 ctx.auth 对象共享给 Edge 模板。可通过 auth.user 访问已身份验证的用户:
@if(auth.isAuthenticated)
<p>Hello {{ auth.user.email }}</p>
@end
在未被 auth 中间件保护的公共页面上,在访问 auth.user 之前先调用 auth.check() 来尝试身份验证:
{{-- 在不要求身份验证的情况下检查用户是否已登录 --}}
@eval(await auth.check())
<header>
@if(auth.isAuthenticated)
<p>Hello {{ auth.user.email }}</p>
@else
<a href="/login">Sign in</a>
@end
</header>
记住我
「记住我」功能通过在 cookie 中存储一个长期有效的令牌,使会话过期后用户依然保持登录状态。当 session 过期时,AdonisJS 会使用该令牌自动重建 session。
创建令牌表
记住我令牌存储在数据库中。为令牌表创建一个迁移:
node ace make:migration remember_me_tokens
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'remember_me_tokens'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments()
table
.integer('tokenable_id')
.notNullable()
.unsigned()
.references('id')
.inTable('users')
.onDelete('CASCADE')
table.string('hash').notNullable().unique()
table.timestamp('created_at').notNullable()
table.timestamp('updated_at').notNullable()
table.timestamp('expires_at').notNullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
配置令牌提供者
将 DbRememberMeTokensProvider 分配给你的 User 模型:
import { BaseModel } from '@adonisjs/lucid/orm'
import { DbRememberMeTokensProvider } from '@adonisjs/auth/session'
export default class User extends BaseModel {
// ...其他模型属性
static rememberMeTokens = DbRememberMeTokensProvider.forModel(User)
}
启用记住我令牌
在守卫配置中启用该功能:
import { defineConfig } from '@adonisjs/auth'
import { sessionGuard, sessionUserProvider } from '@adonisjs/auth/session'
const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: true,
rememberMeTokensAge: '2 years',
provider: sessionUserProvider({
model: () => import('#models/user'),
}),
}),
},
})
export default authConfig
在登录时生成令牌
向 login() 传入第二个布尔参数,以生成「记住我」令牌:
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, auth, response }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
const user = await User.verifyCredentials(email, password)
await auth.use('web').login(
user,
!!request.input('remember_me')
)
return response.redirect('/dashboard')
}
}
重定向到目标 URL
当未身份验证的用户尝试访问受保护的页面时,auth 中间件会将其重定向到登录页面。登录后,你会希望将他们送回最初请求的页面,而不是一个通用的仪表盘。
强制登录流程
当 auth 中间件拒绝一个未身份验证的请求时,它会在重定向到登录页之前,自动将当前 URL 存储到 session 中。这适用于匹配到路由的 GET、导航类(非 AJAX)请求。你无需对 auth 中间件做任何修改。
在你的登录控制器中,使用 toIntendedRoute() 在身份验证成功后重定向。它会从 session 中读取存储的 URL,并在没有存储 URL 时回退到提供的默认值。
import User from '#models/user'
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, auth, response }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
const user = await User.verifyCredentials(email, password)
await auth.use('web').login(user)
/**
* 重定向到用户试图访问的页面,
* 如果没有存储目标 URL,则重定向到仪表盘
*/
return response.redirect().toIntendedRoute('dashboard')
}
}
自愿登录流程
当用户从公共页面主动点击登录链接时,可以将当前 URL 作为查询参数传递。登录页处理器使用 session.setIntendedUrl() 将其存储在 session 中,该方法会校验 URL 以防止开放重定向(open redirect)。
<a href="{{ route('auth.login', {}, { qs: { intended: request.url(true) } }) }}">
Login
</a>
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async show({ request, session, view }: HttpContext) {
const intended = request.input('intended')
if (intended) {
session.setIntendedUrl(intended)
}
return view.render('auth/login')
}
}
登录后,toIntended() 的工作方式相同,无论目标 URL 是如何存储的。
访客中间件
访客中间件会将已身份验证的用户从 /login 或 /register 这类页面重定向离开。这可以防止用户在单一设备上创建多个 session。
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('/login', () => {
// 显示登录表单
})
.use(middleware.guest())
默认情况下,该中间件使用 default 守卫检查身份验证。若要显式指定守卫:
router
.get('/login', () => {
// 显示登录表单
})
.use(
middleware.guest({
guards: ['web', 'admin_web'],
})
)
你可以在 app/middleware/guest_middleware.ts 中配置已身份验证用户的重定向路径。
事件
session 守卫会在身份验证过程中发出事件。完整列表请参阅 事件参考指南 。