会话守卫

会话守卫(Session guard)

会话守卫使用 @adonisjs/session 软件包在 HTTP 请求期间登录并认证用户。

会话和 cookie 已在互联网上存在很长时间,并且对大多数应用来说都表现良好。因此,对于服务端渲染的应用或位于同一顶级域下的 SPA Web 客户端,我们推荐使用会话守卫。

配置守卫

身份认证守卫定义在 config/auth.ts 文件中。你可以在该文件的 guards 对象下配置多个守卫。

config/auth.ts
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 类的实例。它接受一个用于在认证期间查找用户的用户提供器,以及一个用于配置记住我令牌(remember tokens)行为的可选配置对象。

sessionUserProvider 方法会创建 SessionLucidUserProvider 类的实例。它接受用于认证的模型引用。

执行登录

你可以使用守卫的 login 方法来登录用户。该方法接受一个 User 模型的实例,并为其创建一个登录会话。

在以下示例中:

  • 我们使用 AuthFinder mixin 中的 verifyCredentials 方法,通过 email 和密码查找用户。

  • auth.use('web') 返回在 config/auth.ts 文件中配置的 SessionGuard 实例(web 是你在配置中定义的守卫名称)。

  • 接下来,我们调用 auth.use('web').login(user) 方法为用户创建一个登录会话。

  • 最后,我们将用户重定向到 /dashboard 端点。你可以自由自定义重定向的端点。

import User from '#models/user'
import { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async store({ request, auth, response }: HttpContext) {
/**
* 第 1 步:从请求体中获取凭据
*/
const { email, password } = request.only(['email', 'password'])
/**
* 第 2 步:验证凭据
*/
const user = await User.verifyCredentials(email, password)
/**
* 第 3 步:登录用户
*/
await auth.use('web').login(user)
/**
* 第 4 步:将其发送到受保护的路由
*/
response.redirect('/dashboard')
}
}

保护路由

你可以使用 auth 中间件来保护路由,使其免受未认证用户的访问。该中间件在 start/kernel.ts 文件中以命名中间件集合的形式注册。

import router from '@adonisjs/core/services/router'
export const middleware = router.named({
auth: () => import('#middleware/auth_middleware')
})

auth 中间件应用到你希望保护、免受未认证用户访问的路由上。

import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', () => {})
.use(middleware.auth())

默认情况下,auth 中间件会根据 default 守卫(在配置文件中定义)对用户进行认证。不过,你可以在分配 auth 中间件时指定一个守卫数组。

在以下示例中,auth 中间件将尝试使用 webapi 守卫对请求进行认证。

import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', () => {})
.use(
middleware.auth({
guards: ['web', 'api']
})
)

处理身份认证异常

如果用户未通过认证,auth 中间件会抛出 E_UNAUTHORIZED_ACCESS 异常。该异常会使用以下内容协商规则自动处理。

  • 带有 Accept=application/json 请求头的请求会收到一个包含 message 属性的错误数组。

  • 带有 Accept=application/vnd.api+json 请求头的请求会收到一个符合 JSON API 规范的错误数组。

  • 对于服务端渲染的应用,用户将被重定向到 /login 页面。你可以在 auth 中间件类中配置重定向端点。

获取已登录用户

你可以使用 auth.user 属性访问已登录用户的实例。该值仅在使用了 authsilent_auth 中间件,或者你手动调用了 auth.authenticateauth.check 方法时才可用。

使用 auth 中间件
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
await auth.user!.getAllMetrics()
})
.use(middleware.auth())
手动调用 authenticate 方法
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
/**
* 首先,对用户进行认证
*/
await auth.authenticate()
/**
* 然后访问用户对象
*/
await auth.user!.getAllMetrics()
})

静默认证中间件(Silent auth middleware)

silent_auth 中间件与 auth 中间件类似,但在用户未通过认证时不会抛出异常。相反,请求仍会像往常一样继续。

当你希望始终对用户进行认证以执行某些操作,但不想在用户未通过认证时阻塞请求时,这个中间件非常有用。

如果你打算使用这个中间件,那么必须在路由中间件 列表中注册它。

start/kernel.ts
import router from '@adonisjs/core/services/router'
router.use([
// ...
() => import('#middleware/silent_auth_middleware')
])

检查请求是否已认证

你可以使用 auth.isAuthenticated 标志检查某个请求是否已经过认证。对于一个已认证的请求,auth.user 的值始终是有定义的。

import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.get('dashboard', async ({ auth }) => {
if (auth.isAuthenticated) {
await auth.user!.getAllMetrics()
}
})
.use(middleware.auth())

获取已认证用户,否则失败

如果你不喜欢在 auth.user 属性上使用非空断言运算符(non-null assertion operator),你可以使用 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()
await user.getAllMetrics()
})
.use(middleware.auth())

在 Edge 模板中访问用户

InitializeAuthMiddleware 也会将 ctx.auth 属性共享给 Edge 模板。因此,你可以通过 auth.user 属性访问当前已登录的用户。

@if(auth.isAuthenticated)
<p> Hello {{ auth.user.email }} </p>
@end

如果你想在非受保护路由上获取已登录用户的信息,可以使用 auth.check 方法检查用户是否已登录,然后再访问 auth.user 属性。一个很好的用例是在公共页面的网站页眉中显示已登录用户的信息。

{{--
这是一个公共页面,因此它不受 auth
中间件保护。不过,我们仍然希望在网站
页眉中显示已登录用户的信息。
为此,我们使用 `auth.check` 方法静默地
检查用户是否已登录,然后在其页眉中
显示他们的 email。
你明白这个思路了!
--}}
@eval(await auth.check())
<header>
@if(auth.isAuthenticated)
<p> Hello {{ auth.user.email }} </p>
@end
</header>

执行登出

你可以使用 guard.logout 方法让用户登出。在登出期间,用户状态会从会话存储中删除。当前活跃的记住我令牌也会被删除(如果使用记住我令牌的话)。

import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router
.post('logout', async ({ auth, response }) => {
await auth.use('web').logout()
return response.redirect('/login')
})
.use(middleware.auth())

使用记住我(Remember Me)功能

记住我功能会在用户会话过期后自动登录用户。这是通过生成一个加密安全的令牌并将其作为 cookie 保存在用户的浏览器中来实现的。

在用户会话过期后,AdonisJS 会使用记住我 cookie,验证令牌的有效性,并自动为该用户重新创建已登录会话。

创建记住我令牌表

记住我令牌保存在数据库中,因此你必须创建一个新的迁移来创建 remember_me_tokens 表。

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)
}

在配置中启用记住我令牌

最后,让我们在 config/auth.ts 文件中的会话守卫配置上启用 useRememberTokens 标志。

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

在登录期间记住用户

设置完成后,你可以使用 guard.login 方法生成记住我令牌和 cookie,如下所示。

import User from '#models/user'
import { 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,
/**
* 当存在 "remember_me" 输入时生成令牌
*/
!!request.input('remember_me')
)
response.redirect('/dashboard')
}
}

使用访客中间件(guest middleware)

auth 软件包附带了一个访客(guest)中间件,你可以用它来拦截已登录用户访问 /login 页。这样做是为了避免单个用户在同一设备上创建多个会话。

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.get('/login', () => {})
.use(middleware.guest())

默认情况下,访客中间件会使用 default 守卫(在配置文件中定义)来检查用户的登录状态。不过,你可以在分配 guest 中间件时指定一个守卫数组。

router
.get('/login', () => {})
.use(middleware.guest({
guards: ['web', 'admin_web']
}))

最后,你可以在 ./app/middleware/guest_middleware.ts 文件中配置已登录用户的重定向路由。

事件

请查阅事件参考指南,查看 Auth 软件包发出的可用事件列表。