Session
你可以使用 @adonisjs/session 包在 AdonisJS 应用中管理用户会话。session 包提供了一个统一的 API,用于在不同的存储提供商之间存储会话数据。
以下是内置存储的列表。
-
cookie:会话数据存储在加密的 cookie 中。cookie 存储非常适合多服务器部署,因为数据存储在客户端。 -
file:会话数据保存在服务器上的一个文件中。只有当你通过负载均衡器实现了粘性会话时,file 存储才能扩展到多服务器部署。 -
redis:会话数据存储在 Redis 数据库中。redis 存储推荐用于会话数据量大的应用,并且可以扩展到多服务器部署。 -
dynamodb:会话数据存储在 Amazon DynamoDB 表中。DynamoDB 存储适用于需要高度可扩展、分布式会话存储的应用,尤其是当基础设施构建在 AWS 之上时。 -
database:会话数据使用 Lucid 存储在 SQL 数据库中。如果你已经在应用中使用 SQL 数据库,并且想避免添加 Redis 这样的额外依赖,database 存储是一个不错的选择。 -
memory:会话数据存储在全局内存存储中。memory 存储在测试期间使用。
除了内置的后端存储之外,你还可以创建并注册自定义会话存储。
安装
使用以下命令安装并配置该包:
node ace add @adonisjs/session
-
使用检测到的包管理器安装
@adonisjs/session包。 -
在
adonisrc.ts文件中注册以下 service provider 和命令。{commands: [// ...其他命令() => import('@adonisjs/session/commands')],providers: [// ...其他 providers() => import('@adonisjs/session/session_provider')]} -
创建
config/session.ts文件。 -
定义以下环境变量及其校验。
SESSION_DRIVER=cookie -
在
start/kernel.ts文件中注册以下中间件。router.use([() => import('@adonisjs/session/session_middleware')])
配置
session 包的配置存储在 config/session.ts 文件中。
另请参阅:Session 配置桩
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, stores } from '@adonisjs/session'
export default defineConfig({
age: '2h',
enabled: true,
cookieName: 'adonis-session',
clearWithBrowser: false,
cookie: {
path: '/',
httpOnly: true,
secure: app.inProduction,
sameSite: 'lax',
},
store: env.get('SESSION_DRIVER'),
stores: {
cookie: stores.cookie(),
}
})
-
enabled
-
在不从中间件栈中移除中间件的情况下,临时启用或禁用它。
-
cookieName
-
用于存储会话 ID 的 cookie 名称。欢迎随意重命名。
-
clearWithBrowser
-
设为 true 时,用户关闭浏览器窗口后会移除会话 ID cookie。这个 cookie 在技术上被称为 会话 cookie。
-
age
-
age属性控制会话数据在没有用户活动的情况下的有效时长。超过给定时长后,会话数据被视为已过期。 -
cookie
-
控制会话 ID cookie 的属性。另请参阅 cookie 配置。
-
store
-
定义你用于存储会话数据的存储。它可以是一个固定值,也可以从环境变量读取。
-
stores
-
stores对象用于配置一个或多个后端存储。大多数应用会使用单个存储。不过,你可以配置多个存储,并根据应用运行的环境在它们之间切换。
存储配置
以下是 @adonisjs/session 包内置的后端存储列表。
import app from '@adonisjs/core/services/app'
import { defineConfig, stores } from '@adonisjs/session'
export default defineConfig({
store: env.get('SESSION_DRIVER'),
stores: {
cookie: stores.cookie(),
file: stores.file({
location: app.tmpPath('sessions')
}),
redis: stores.redis({
connection: 'main'
})
dynamodb: stores.dynamodb({
clientConfig: {}
}),
database: stores.database({
connectionName: 'postgres',
tableName: 'sessions',
}),
}
})
-
stores.cookie
-
cookie存储加密会话数据,并将其存储在 cookie 中。 -
stores.file
-
定义
file存储的配置。该方法接受用于存储会话文件的location路径。 -
stores.redis
-
定义
redis存储的配置。该方法接受用于存储会话数据的connection名称。在使用
redis存储之前,务必先安装并配置 @adonisjs/redis 包。 -
stores.dynamodb
-
定义
dynamodb存储的配置。你可以通过clientConfig属性传入 DynamoDB 配置,或者传入一个 DynamoDB 实例作为client属性。With client configstores.dynamodb({clientConfig: {region: 'us-east-1',endpoint: '<database-endpoint>',credentials: {accessKeyId: '',secretAccessKey: '',}},})With client instanceimport { DynamoDBClient } from '@aws-sdk/client-dynamodb'const client = new DynamoDBClient({})stores.dynamodb({client,})此外,你可以定义一个自定义表名和键属性名。
stores.dynamodb({tableName: 'Session'keyAttribute: 'key'}) -
stores.database
-
定义
database存储的配置。该方法可选地接受要使用的connectionName和用于存储会话数据的tableName。在使用
database存储之前,务必先安装并配置 @adonisjs/lucid 包。你还必须在数据库中创建 sessions 表。你可以使用以下命令创建迁移文件。
node ace make:session-tablestores.database({connectionName: 'postgres',tableName: 'sessions',gcProbability: 2,})与使用 Redis(自动处理过期)不同,SQL 数据库需要手动清理过期的会话。database 存储实现了一种概率性垃圾回收:在每个请求上,有
gcProbability百分比的概率(默认:2%)会从数据库中删除过期的会话。将
gcProbability设为0可完全禁用自动垃圾回收。在这种情况下,你应该设置一个定时任务来定期清理过期的会话。为已有数据库添加标签支持
如果你有一个已存在的 sessions 表,并想使用 会话标签,你需要添加
user_id列。创建一个新的迁移:node ace make:migration add_user_id_to_sessionsimport { BaseSchema } from '@adonisjs/lucid/schema'export default class extends BaseSchema {protected tableName = 'sessions'async up() {this.schema.alterTable(this.tableName, (table) => {table.string('user_id').nullable().index()})}async down() {this.schema.alterTable(this.tableName, (table) => {table.dropColumn('user_id')})}}
更新环境变量校验
如果你决定使用除默认存储之外的会话存储,请确保同时更新 SESSION_DRIVER 环境变量的校验。
在下面的示例中,我们配置了 cookie、redis 和 dynamodb 存储。因此,我们也应该允许 SESSION_DRIVER 环境变量是其中之一。
import { defineConfig, stores } from '@adonisjs/session'
export default defineConfig({
store: env.get('SESSION_DRIVER'),
stores: {
cookie: stores.cookie(),
redis: stores.redis({
connection: 'main'
})
}
})
{
SESSION_DRIVER: Env.schema.enum(['cookie', 'redis', 'memory'] as const)
}
基本示例
一旦 session 包注册完成,你就可以从 HTTP 上下文 访问 session 属性。session 属性暴露了用于向会话存储读写数据的 API。
import router from '@adonisjs/core/services/router'
router.get('/theme/:color', async ({ params, session, response }) => {
session.put('theme', params.color)
response.redirect('/')
})
router.get('/', async ({ session }) => {
const colorTheme = session.get('theme')
return `You are using ${colorTheme} color theme`
})
会话数据在请求开始时从会话存储读取,并在请求结束时写回存储。因此,所有更改都保存在内存中,直到请求结束。
支持的数据类型
会话数据使用 JSON.stringify 序列化为字符串;因此,你可以使用以下 JavaScript 数据类型作为会话值。
- string
- number
- bigInt
- boolean
- null
- object
- array
// 对象
session.put('user', {
id: 1,
fullName: 'virk',
})
// 数组
session.put('product_ids', [1, 2, 3, 4])
// 布尔值
session.put('is_logged_in', true)
// 数字
session.put('visits', 10)
// BigInt
session.put('visits', BigInt(10))
// 日期对象会被转换为 ISO 字符串
session.put('visited_at', new Date())
读写数据
以下是你可以用于与 session 对象交互的方法列表。
get
从存储中返回某个键的值。你可以使用点表示法读取嵌套值。
session.get('key')
session.get('user.email')
你也可以将默认值定义为第二个参数。如果键在存储中不存在,则返回默认值。
session.get('visits', 0)
has
检查存储中是否存在某个键。
if (session.has('visits')) {
}
all
返回会话存储中的所有数据。返回值始终是一个对象。
session.all()
put
向会话存储添加一个键值对。你可以使用点表示法创建带有嵌套值的对象。
session.put('user', { email: 'foo@bar.com' })
// 与上面相同
session.put('user.email', 'foo@bar.com')
forget
从会话存储中移除一个键值对。
session.forget('user')
// 从 user 对象中移除 email
session.forget('user.email')
pull
pull 方法返回某个键的值,并同时从存储中移除它。
const user = session.pull('user')
session.has('user') // false
increment
increment 方法递增某个键的值。如果它尚不存在,则定义一个新键值。
session.increment('visits')
// 递增 4
session.increment('visits', 4)
decrement
decrement 方法递减某个键的值。如果它尚不存在,则定义一个新键值。
session.decrement('visits')
// 递减 4
session.decrement('visits', 4)
clear
clear 方法移除会话存储中的一切。
session.clear()
会话生命周期
即使在请求/响应生命周期未与会话交互的情况下,AdonisJS 也会在第一个 HTTP 请求上创建一个空会话存储,并将其分配给一个唯一的会话 ID。
在随后的每个请求上,我们会更新会话 ID cookie 的 maxAge 属性,以确保它不会过期。会话存储也会收到更改通知(如果有),以便更新并持久化它们。
你可以使用 sessionId 属性访问唯一的会话 ID。访问者的会话 ID 在过期之前保持不变。
console.log(session.sessionId)
重新生成会话 ID
重新生成会话 ID 有助于防止应用遭受会话固定 攻击。在将匿名会话与已登录用户关联时,你必须重新生成会话 ID。
@adonisjs/auth 包会自动重新生成会话 ID,因此你不必手动执行。
/**
* 新的会话 ID 将在
* 请求结束时被分配
*/
session.regenerate()
会话标签
会话标签允许你将会话与用户 ID 关联。这对于实现以下功能很有用:
- 从所有设备登出:销毁与用户关联的所有会话。
- 活动会话列表:在用户的账户设置中向用户展示所有活动会话。
会话标签仅受 redis、database 和 memory 存储支持。尝试对 cookie、file 或 dynamodb 存储使用标签会抛出错误。
为当前会话打标签
你可以使用 session.tag 方法用用户 ID 为当前会话打标签。这通常是在用户登录后完成的。
import router from '@adonisjs/core/services/router'
router.post('/login', async ({ auth, session }) => {
const user = await auth.use('web').login(email, password)
// 用用户 ID 为会话打标签
await session.tag(String(user.id))
})
SessionCollection
SessionCollection 类提供了在 HTTP 请求上下文之外进行编程式会话管理的 API。它允许你按 ID 读取、销毁会话,并为其打标签。
创建实例
你必须通过容器获取 SessionCollection 实例,以确保正确的配置注入。
import app from '@adonisjs/core/services/app'
import { SessionCollection } from '@adonisjs/session'
const sessionCollection = await app.container.make(SessionCollection)
可用方法
// 按 ID 获取会话数据
const data = await sessionCollection.get(sessionId)
// 按 ID 销毁会话
await sessionCollection.destroy(sessionId)
// 用用户 ID 为会话打标签
await sessionCollection.tag(sessionId, userId)
// 获取用户的所有会话
const sessions = await sessionCollection.tagged(userId)
// 返回:Array<{ id: string, data: SessionData }>
// 检查当前存储是否支持标签
const supportsTagging = sessionCollection.supportsTagging()
列出活动会话
以下是一个在用户账户设置页面列出其所有活动会话的示例。
import app from '@adonisjs/core/services/app'
import router from '@adonisjs/core/services/router'
import { SessionCollection } from '@adonisjs/session'
router.get('/account/sessions', async ({ auth, session, view }) => {
const user = auth.user!
const sessionCollection = await app.container.make(SessionCollection)
const sessions = await sessionCollection.tagged(String(user.id))
// 标记当前会话
const sessionsWithCurrent = sessions.map((tagged) => ({
...tagged,
isCurrent: tagged.id === session.sessionId,
}))
return view.render('account/sessions', { sessions: sessionsWithCurrent })
})
从所有其他设备登出
以下是一个实现"从所有其他设备登出"功能的完整示例。
import app from '@adonisjs/core/services/app'
import router from '@adonisjs/core/services/router'
import { SessionCollection } from '@adonisjs/session'
router.post('/logout-other-devices', async ({ auth, session, response }) => {
const user = auth.user!
const sessionCollection = await app.container.make(SessionCollection)
const sessions = await sessionCollection.tagged(String(user.id))
for (const s of sessions) {
if (s.id !== session.sessionId) {
await sessionCollection.destroy(s.id)
}
}
return response.redirect().back()
})
Flash 消息
Flash 消息用于在两个 HTTP 请求之间传递数据。它们通常用于在特定操作之后向用户提供反馈。例如,在表单提交后显示成功消息,或显示校验错误消息。
在下面的示例中,我们定义了显示联系表单和将表单详情提交到数据库的路由。表单提交后,我们使用 flash 消息将用户重定向回表单,并显示一条成功通知。
import router from '@adonisjs/core/services/router'
router.post('/contact', ({ session, request, response }) => {
const data = request.all()
// 保存联系数据
session.flash('notification', {
type: 'success',
message: 'Thanks for contacting. We will get back to you'
})
response.redirect().back()
})
router.get('/contact', ({ view }) => {
return view.render('contact')
})
你可以在 edge 模板中使用 flashMessage 标签或 flashMessages 属性访问 flash 消息。
@flashMessage('notification')
<div class="notification {{ $message.type }}">
{{ $message.message }}
</div>
@end
<form method="POST" action="/contact">
<!-- 表单的其余部分 -->
</form>
你可以在控制器中使用 session.flashMessages 属性访问 flash 消息。
router.get('/contact', ({ view, session }) => {
console.log(session.flashMessages.all())
return view.render('contact')
})
校验错误与 flash 消息
Session 中间件会自动捕获校验异常,并将用户重定向回表单。校验错误和表单输入数据保存在 flash 消息中,你可以在 Edge 模板中访问它们。
在下面的示例中:
- 我们使用
old方法 访问title输入字段的值。 - 并使用
@inputError标签 访问错误消息。
<form method="POST" action="/posts">
<div>
<label for="title"> Title </label>
<input
type="text"
id="title"
name="title"
value="{{ old('title') || '' }}"
/>
@inputError('title')
@each(message in $messages)
<p> {{ message }} </p>
@end
@end
</div>
</form>
写入 flash 消息
以下是向 flash 消息存储写入数据的方法。session.flash 方法接受一个键值对,并将其写入会话存储内的 flash 消息属性中。
session.flash('key', value)
session.flash({
key: value
})
除了手动读取请求数据并将其存入 flash 消息外,你还可以使用以下方法之一来 flash 表单数据。
/**
* 用于 flash 请求数据的简写
*/
session.flashAll()
/**
* 与 "flashAll" 相同
*/
session.flash(request.all())
/**
* 用于 flash 请求数据中选定属性的简写
*/
session.flashOnly(['username', 'email'])
/**
* 与 "flashOnly" 相同
*/
session.flash(request.only(['username', 'email']))
/**
* 用于 flash 请求数据中选定属性(取反)的简写
*/
session.flashExcept(['password'])
/**
* 与 "flashExcept" 相同
*/
session.flash(request.except(['password']))
最后,你可以使用 session.reflash 方法重新 flash 当前的 flash 消息。
session.reflash()
session.reflashOnly(['notification', 'errors'])
session.reflashExcept(['errors'])
读取 flash 消息
flash 消息仅在重定向之后的后续请求中可用。你可以使用 session.flashMessages 属性访问它们。
console.log(session.flashMessages.all())
console.log(session.flashMessages.get('key'))
console.log(session.flashMessages.has('key'))
同一个 flashMessages 属性也会与 Edge 模板共享,你可以像下面这样访问它。
另请参阅:Edge 辅助方法参考
{{ flashMessages.all() }}
{{ flashMessages.get('key') }}
{{ flashMessages.has('key') }}
最后,你可以使用以下 Edge 标签访问特定的 flash 消息或校验错误。
{{-- 按 key 读取任意 flash 消息 --}}
@flashMessage('key')
{{ inspect($message) }}
@end
{{-- 读取通用错误 --}}
@error('key')
{{ inspect($message) }}
@end
{{-- 读取校验错误 --}}
@inputError('key')
{{ inspect($messages) }}
@end
事件
请查看 事件参考指南,了解 @adonisjs/session 包分发的事件列表。
创建自定义会话存储
会话存储必须实现 SessionStoreContract 接口,并定义以下方法。
import {
SessionData,
SessionStoreFactory,
SessionStoreContract,
} from '@adonisjs/session/types'
/**
* 你想接受的配置
*/
export type MongoDBConfig = {}
/**
* 驱动实现
*/
export class MongoDBStore implements SessionStoreContract {
constructor(public config: MongoDBConfig) {
}
/**
* 返回会话 ID 对应的会话数据。该方法
* 必须返回 null 或键值对对象
*/
async read(sessionId: string): Promise<SessionData | null> {
}
/**
* 针对提供的会话 ID 保存会话数据
*/
async write(sessionId: string, data: SessionData): Promise<void> {
}
/**
* 删除给定会话 ID 的会话数据
*/
async destroy(sessionId: string): Promise<void> {
}
/**
* 重置会话过期时间
*/
async touch(sessionId: string): Promise<void> {
}
}
/**
* 用于在配置文件中引用该存储的
* 工厂函数。
*/
export function mongoDbStore (config: MongoDbConfig): SessionStoreFactory {
return (ctx, sessionConfig) => {
return new MongoDBStore(config)
}
}
在上面的代码示例中,我们导出了以下值。
-
MongoDBConfig:你想要接受的配置的 TypeScript 类型。 -
MongoDBStore:作为类实现的存储。它必须遵守SessionStoreContract接口。 -
mongoDbStore:最后,一个为每个 HTTP 请求创建存储实例的工厂函数。
使用该存储
存储创建完成后,你可以在配置文件中使用 mongoDbStore 工厂函数引用它。
import { defineConfig } from '@adonisjs/session'
import { mongDbStore } from 'my-custom-package'
export default defineConfig({
stores: {
mongodb: mongoDbStore({
// 配置写在这里
})
}
})
关于序列化数据的说明
write 方法接收的会话数据是一个对象,在保存之前你可能需要将其转换为字符串。你可以使用任何序列化包来实现,也可以使用 AdonisJS helpers 模块提供的 MessageBuilder 辅助函数。作为参考,请查阅官方的 session 存储。