会话
本指南介绍在 AdonisJS 应用中处理 HTTP 会话。你将了解:
- 安装并配置 session 包
- 存储和检索会话数据
- 处理闪现消息
- 选择正确的存储驱动
- 实现自定义会话存储
概述
HTTP 是一个无状态协议,意味着每个请求都是独立的,服务器不会在请求之间保留信息。会话通过提供一种跨多个 HTTP 请求持久化状态并将该状态与唯一的会话标识符关联起来,解决了这个问题。
在 AdonisJS 中,会话主要用于 Hypermedia 和 Inertia 应用,以维持用户身份验证状态,并在请求之间传递临时数据(闪现消息)。例如,用户登录后,他们的身份验证状态会存储在会话中,以便他们在后续请求中保持登录状态。类似地,当你在表单提交后重定向时,存储在会话中的闪现消息可以在下一页显示成功或错误通知。
如果你对会话的概念还不熟悉,建议在继续之前阅读这篇 HTTP 会话介绍 。
安装
运行以下 Ace 命令安装并配置 session 包:
node ace add @adonisjs/session
查看 add 命令执行的步骤
- 使用检测到的包管理器安装
@adonisjs/session包。 - 在
adonisrc.ts文件中注册@adonisjs/session/session_provider服务提供者。 - 创建
config/session.ts配置文件。 - 定义
SESSION_DRIVER环境变量。 - 在
start/kernel.ts文件中注册@adonisjs/session/session_middleware中间件。
选择存储驱动
session 驱动决定了你的会话数据存储在何处。每个驱动都有不同的特性,使其适合特定的用例:
基于 Cookie 的会话会静默截断超过 4KB 的数据。对于具有较大会话数据的生产应用,请切换到 Redis。
| 驱动 | 描述 | 最适合 |
|---|---|---|
cookie | 将存储在加密的 Cookie 中(最大约 4KB) | 简单应用、小数据、无后端存储 |
file | 存储在本地文件系统中 | 开发、单服务器部署 |
redis | 存储在 Redis 数据库中 | 生产、多服务器、较大数据 |
dynamodb | 存储在 AWS DynamoDB 中 | AWS 基础设施、无服务器应用 |
database | 存储在 SQL 数据库中 | 使用 SQL、已有数据库基础设施的生产应用 |
memory | 存储在内存中(重启后丢失) | 仅用于测试 |
配置
会话配置存储在 config/session.ts 中,该文件在安装期间创建。以下是默认配置:
import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, stores } from '@adonisjs/session'
export default defineConfig({
enabled: true,
cookieName: 'adonis-session',
clearWithBrowser: false,
age: '2h',
cookie: {
path: '/',
httpOnly: true,
secure: app.inProduction,
sameSite: 'lax',
},
store: env.get('SESSION_DRIVER'),
stores: {
cookie: stores.cookie(),
file: stores.file({
location: app.tmpPath('sessions')
}),
redis: stores.redis({
connection: 'main'
}),
database: stores.database({
connection: 'postgres',
tableName: 'sessions',
}),
dynamodb: stores.dynamodb({
region: env.get('AWS_REGION'),
endpoint: env.get('AWS_ENDPOINT'),
tableName: 'sessions',
}),
}
})
age
会话过期前的生命周期。接受字符串持续时间如 '2 hours' 或 '7 days',或毫秒数。此期限过后,会话过期并删除数据。
age: '2 hours'
// 或
age: 7200000 // 2 小时(毫秒)
clearWithBrowser
当为 true 时,无论配置的 age 是多少,用户关闭浏览器时都会删除会话 Cookie。当为 false(默认值)时,即使浏览器关闭,会话也会在配置的 age 持续时间内持续存在。
clearWithBrowser: false
store
确定使用哪个会话驱动。在 .env 文件中使用 SESSION_DRIVER 环境变量设置此项。该值必须与 stores 对象中定义的键之一匹配。
SESSION_DRIVER=cookie
cookie
控制会话 Cookie 行为的 Cookie 配置对象。这包括 Cookie 名称、域名、路径和安全选项的设置。详细选项请参阅 Cookie 配置 部分。
stores
一个定义所有可用会话存储的对象。每个键代表一个驱动名称,值是该存储的配置。store 属性中指定的驱动必须存在于此对象中。
Cookie 配置
会话使用 Cookie 来存储会话 ID(或对于 cookie 驱动则是整个会话数据)。使用以下选项配置 Cookie 行为:
cookie.name
存储会话 ID 的 Cookie 名称。仅当它应用中的其他 Cookie 冲突时才更改。
cookie: {
name: 'adonis-session'
}
cookie.domain
Cookie 有效的域名。留空则默认为当前域名。设置为 '.example.com'(带前导点)可在子域名间共享 Cookie,如 app.example.com 和 api.example.com。
cookie: {
domain: '' // 仅当前域名
// 或
domain: '.example.com' // 所有子域名
}
cookie.path
Cookie 有效的 URL 路径。将其设置为 '/' 可使 Cookie 在你的整个应用中可用。
cookie: {
path: '/'
}
cookie.httpOnly
当为 true 时,阻止 JavaScript 通过 document.cookie 访问 Cookie,防止恶意脚本试图窃取会话 ID 的 XSS 攻击。为了安全,请保持为 true。
cookie: {
httpOnly: true
}
cookie.secure
当为 true 时,确保 Cookie 仅通过 HTTPS 连接发送,防止在不安全的网络上进行会话劫持。起步套件使用 app.inProduction 在生产环境中自动启用此选项,而在本地通过 HTTP 开发期间保持禁用。
cookie: {
secure: app.inProduction
}
cookie.sameSite
控制浏览器何时随跨站请求发送 Cookie,防止 CSRF 攻击。
'lax':Cookie 在顶级导航(点击链接)时发送。默认且推荐用于大多数应用。'strict':Cookie 从不在跨站请求时发送。最安全,但可能破坏合法流程。'none':Cookie 总是发送。需要secure: true,很少需要。
cookie: {
sameSite: 'lax'
}
Redis 驱动
Redis 驱动将会话数据存储在 Redis 数据库中,使其非常适合具有多服务器或较大会话数据的生产应用。
你可以使用以下命令添加 redis 包。完整的设置说明请参阅 Redis 指南 。
node ace add @adonisjs/redis
stores.redis.connection
用于会话存储的 Redis 连接名称。该连接必须在你的 config/redis.ts 文件中配置。
stores: {
redis: stores.redis({
connection: 'main'
})
}
Database 驱动
Database 驱动将会话数据存储在 SQL 数据库中,非常适合已经使用 SQL 数据库基础设施的生产应用。
stores.database.connection
用于会话存储的数据库连接名称。该连接必须在你的 config/database.ts 文件中配置。
stores: {
database: stores.database({
connection: 'postgres'
})
}
stores.database.tableName
存储会话数据的数据库表名。默认为 'sessions'。请务必使用 node ace make:session-table 命令为会话表创建迁移。
stores: {
database: stores.database({
tableName: 'sessions'
})
}
DynamoDB 驱动
DynamoDB 驱动将会话数据存储在 AWS DynamoDB 中,非常适合无服务器应用和 AWS 基础设施。
安装:首先安装 AWS SDK:
npm install @aws-sdk/client-dynamodb
stores.dynamodb.region
你的 DynamoDB 表所在的 AWS 区域。
stores: {
dynamodb: stores.dynamodb({
region: env.get('AWS_REGION')
})
}
stores.dynamodb.endpoint
DynamoDB 的可选自定义端点 URL。在使用 DynamoDB Local 进行本地开发或使用自定义端点时很有用。
stores: {
dynamodb: stores.dynamodb({
endpoint: env.get('AWS_ENDPOINT')
})
}
stores.dynamodb.tableName
存储会话数据的 DynamoDB 表名。
stores: {
dynamodb: stores.dynamodb({
tableName: 'sessions'
})
}
File 驱动
File 驱动将会话数据存储在本地文件系统中,适合开发和单服务器部署。
stores.file.location
存储会话文件的目录路径。默认是你的应用根目录下的 tmp/sessions。
stores: {
file: stores.file({
location: app.tmpPath('sessions')
})
}
基础用法
安装后,你可以通过 HTTP 上下文的 session 属性访问会话。会话存储提供了一个简单的键值 API 用于读写数据。
让我们构建一个简单的购物车示例,演示核心的会话方法:
import router from '@adonisjs/core/services/router'
import Product from '#models/product'
/**
* 显示购物车中的商品。
* 使用 get() 并设置默认值为空数组。
*/
router.get('/cart', ({ session }) => {
const cartItems = session.get('cart', [])
return { items: cartItems, total: cartItems.length }
})
/**
* 将商品添加到购物车。
* 演示使用 put() 存储更新后的购物车数据。
*/
router.post('/cart', async ({ request, session }) => {
const productId = request.input('product_id')
const product = await Product.findOrFail(productId)
const cartItems = session.get('cart', [])
cartItems.push({
id: product.id,
name: product.name,
quantity: 1
})
session.put('cart', cartItems)
return { message: 'Item added', totalItems: cartItems.length }
})
/**
* 从购物车中移除特定商品。
* 展示如何更新并存储已修改的数据。
*/
router.delete('/cart/:productId', ({ params, session }) => {
const cartItems = session.get('cart', [])
const updatedCart = cartItems.filter(item => item.id !== params.productId)
session.put('cart', updatedCart)
return { message: 'Item removed' }
})
/**
* 清空整个购物车。
* 使用 forget() 移除特定键。
*/
router.delete('/cart', ({ session }) => {
session.forget('cart')
return { message: 'Cart cleared' }
})
检查值是否存在
在尝试检索之前,你可以检查会话中是否存在某个值。
router.get('/checkout', ({ session, response }) => {
/**
* 在继续之前检查购物车是否存在且有商品。
* has() 方法在键存在时返回 true。
*/
if (!session.has('cart')) {
return response.redirect('/cart')
}
const cartItems = session.get('cart')
return { items: cartItems }
})
检索并移除值
有时你需要获取一个值并立即将其从会话中移除。pull() 方法将两种操作合二为一。
router.post('/process-payment', ({ session }) => {
/**
* 在一个操作中获取购物车数据并将其移除。
* 这在处理一次性数据时很有用。
*/
const cartItems = session.pull('cart', [])
// 使用 cartItems 处理支付
// 购物车现在自动从会话中移除
return { processed: cartItems.length }
})
处理数值
会话提供了方便的方法来递增和递减数值,对计数器或跟踪数值状态很有用。
router.post('/visit', ({ session }) => {
/**
* 访问计数器加 1。
* 如果 'visits' 不存在,则初始化为 1。
*/
session.increment('visits')
const totalVisits = session.get('visits')
return { visits: totalVisits }
})
router.post('/undo', ({ session }) => {
/**
* 递减计数器。
* 如果 'actions' 不存在,则初始化为 -1。
*/
session.decrement('actions')
return { actionsRemaining: session.get('actions', 0) }
})
检索所有会话数据
你可以使用 all() 方法将所有会话数据作为对象检索。
router.get('/debug/session', ({ session }) => {
/**
* 将所有会话数据作为对象返回。
* 对调试或显示会话状态很有用。
*/
const allData = session.all()
return allData
})
清空整个会话
要移除会话存储中的所有数据,使用 clear() 方法。
router.post('/logout', ({ session, auth, response }) => {
// 清除身份验证
await auth.logout()
/**
* 移除所有会话数据。
* 这在退出登录时清理所有用户状态很有用。
*/
session.clear()
return response.redirect('/login')
})
闪现消息
闪现消息是存储在会话中的临时数据,仅对下一个 HTTP 请求可用。它们在被访问一次后自动删除,非常适合在重定向后显示一次性通知。
基础闪现消息
使用 flash() 方法为下一个请求存储一条消息。第一个参数是消息类型(用于对消息进行分类的约定),第二个是消息内容。
router.post('/cart', async ({ session, response }) => {
// 将商品添加到购物车...
/**
* 为下一个请求闪现一条成功消息。
* 可在模板中通过 flashMessages.get('success') 访问。
*/
session.flash('success', 'Item added to the cart')
return response.redirect().back()
})
AdonisJS 使用 success 和 error 作为标准消息类型约定。虽然你可以使用任何字符串作为消息类型,但这些约定有助于组织不同类型的通知。
export default class OrdersController {
async store({ session, response }: HttpContext) {
try {
// 处理订单...
session.flash('success', 'Order placed successfully!')
return response.redirect('/orders')
} catch (error) {
session.flash('error', 'Payment failed. Please try again.')
return response.redirect().back()
}
}
}
显示闪现消息
如果你使用的是 Hypermedia 或 Inertia 起步套件,闪现消息已经在布局文件中自动显示。起步套件会检查 success 和 error 消息,并以适当的样式渲染它们。
对于自定义布局或应用,在你的 Edge 模板中使用全局 flashMessages 辅助函数来显示闪现消息。
@if(flashMessages.has('success'))
<div class="alert alert-success">
{{ flashMessages.get('success') }}
</div>
@end
@if(flashMessages.has('error'))
<div class="alert alert-error">
{{ flashMessages.get('error') }}
</div>
@end
自定义闪现消息
除了标准消息类型之外,你还可以为特定用例创建自定义消息类型。
router.post('/newsletter/subscribe', ({ session, response }) => {
// 订阅用户...
session.flash('newsletter', 'Check your email to confirm subscription')
return response.redirect().back()
})
在你的模板中显示自定义消息。
@if(flashMessages.has('newsletter'))
<div class="alert alert-newsletter">
{{ flashMessages.get('newsletter') }}
</div>
@end
闪现表单数据
当表单处理期间发生错误并且你需要将用户重定向回表单时,你可以闪现已提交的表单数据以预填表单字段。
export default class PostsController {
async store({ request, session, response }: HttpContext) {
try {
// 尝试创建文章...
throw new Error('Unable to publish post')
} catch (error) {
/**
* 闪现表单数据以重新填充表单。
* 用户无需重新输入他们的数据。
*/
session.flashAll()
session.flash('error', 'Failed to create post. Please try again.')
return response.redirect().back()
}
}
}
你也可以只闪现特定字段或排除敏感字段。
// 只闪现特定字段
session.flashOnly(['title', 'content', 'category'])
// 闪现除敏感字段之外的所有内容
session.flashExcept(['password', 'credit_card'])
在模板中使用 old() 辅助函数访问闪现的表单数据。
<input type="text" name="title" value="{{ old('title') }}" />
<textarea name="content">{{ old('content') }}</textarea>
使用 AdonisJS 验证器时,验证错误和表单数据会在验证失败时自动闪现。你无需手动闪现它们。
重新闪现消息
有时你需要为额外的请求保留闪现消息。使用 reflash() 为再一个周期保留来自上一个请求的所有消息。
export default class CheckSubscriptionMiddleware {
async handle({ auth, session, response }: HttpContext, next: NextFn) {
const user = auth.getUserOrFail()
if (!user.hasActiveSubscription) {
/**
* 为再一个请求保留所有闪现消息。
*/
session.reflash()
session.flash('error', 'Please subscribe to continue')
return response.redirect('/subscribe')
}
await next()
}
}
你也可以选择性地只重新闪现特定的消息类型。
// 只重新闪现错误消息
session.reflashOnly(['error'])
// 重新闪现除 success 之外的所有消息
session.reflashExcept(['success'])
预期 URL
预期 URL 功能允许你存储一个用户完成中间步骤后应该被重定向到的 URL。例如,你可以在将用户重定向到登录页面之前存储当前页面 URL,登录后,将他们重定向回他们所在的页面。
URL 在存储前会经过验证,以防止开放重定向攻击。无效或不安全的 URL(如协议相对的 //evil.com)会被静默忽略。
import type { HttpContext } from '@adonisjs/core/http'
export default class SessionController {
async show({ request, session, view }: HttpContext) {
/**
* 如果登录页面是通过 ?intended 查询参数访问的,
* 将预期 URL 存储在会话中,以便登录后使用
*/
const intended = request.input('intended')
if (intended) {
session.setIntendedUrl(intended)
}
return view.render('auth/login')
}
async store({ request, auth, response }: HttpContext) {
const user = await User.verifyCredentials(
request.input('email'),
request.input('password')
)
await auth.use('web').login(user)
/**
* 如果存储了预期 URL,则重定向到该 URL,
* 否则回退到 dashboard
*/
return response.redirect().toIntendedRoute('dashboard')
}
}
你也可以直接读取、消费或清除预期 URL。
import router from '@adonisjs/core/services/router'
router.get('/example', ({ session }) => {
/**
* 读取但不消费
*/
const url = session.getIntendedUrl()
/**
* 读取并一步移除
*/
const consumed = session.pullIntendedUrl()
/**
* 不读取直接移除
*/
session.clearIntendedUrl()
})
另请参阅: 登录后重定向到预期 URL
会话再生
会话再生会创建一个新的会话 ID,同时保留所有现有的会话数据。这是防止 会话固定攻击(session fixation attacks) 的关键安全措施,在这种攻击中,攻击者诱骗用户使用由攻击者控制的会话 ID。
当用户登录时,他们的身份验证状态从未认证变为已认证。如果保持相同的会话 ID,在登录前就知道该会话 ID 的攻击者可能会劫持已认证的会话。登录后再生会话 ID 可以防止这种攻击。
AdonisJS 在使用官方 Auth 包时会自动处理会话再生。但是,如果你正在实现自定义身份验证,必须在登录成功后手动调用 session.regenerate()。
export default class AuthController {
async login({ request, session, response }: HttpContext) {
const { email, password } = request.only(['email', 'password'])
// 验证凭据...
const user = await User.verifyCredentials(email, password)
/**
* 在保留数据的同时生成新的会话 ID。
* 这可以防止会话固定攻击。
*/
await session.regenerate()
// 在新的会话中存储用户信息
session.put('user_id', user.id)
return response.redirect('/dashboard')
}
}
创建自定义会话存储
如果内置驱动都无法满足你的需求,你可以通过实现 SessionStoreContract 接口来创建自定义会话存储。这对于集成 MongoDB 等数据库或自定义存储解决方案很有用。
实现存储契约
创建一个实现了四个必需方法的类。
import {
SessionData,
SessionStoreFactory,
SessionStoreContract,
} from '@adonisjs/session/types'
/**
* 你想要接受的配置
*/
export type MongoDBConfig = {
collection: string;
database: string
}
/**
* MongoDbStore 类处理实际的存储操作。
* 它实现了四个必需方法:read、write、destroy 和 touch。
*/
export class MongoDbStore implements SessionStoreContract {
constructor(protected config: MongoDBConfig) {}
/**
* 读取给定会话 ID 的会话数据。
* 如果会话不存在则返回 null。
*/
async read(sessionId: string): Promise<SessionData | null> {
// 查询你的存储并返回会话数据
}
/**
* 写入给定会话 ID 的会话数据。
*/
async write(
sessionId: string,
data: SessionData,
expiresAt: Date
): Promise<void> {
// 将会话数据保存到你的存储
}
/**
* 删除给定会话 ID 的会话数据。
*/
async destroy(sessionId: string): Promise<void> {
// 从你的存储中移除会话
}
/**
* 在不更改数据的情况下更新会话的过期时间。
*/
async touch(sessionId: string, expiresAt: Date): Promise<void> {
// 更新你的存储中的过期时间戳
}
}
/**
* 工厂函数接受配置并返回一个驱动
* 函数。当会话管理器调用驱动函数时,它会创建
* 存储类的新实例。
*/
export function mongoDbStore(config: MongoDbConfig): SessionStoreFactory {
return (ctx, sessionConfig) => {
return new MongoDBStore(config)
}
}
注册你的自定义存储
使用工厂函数在 config/session.ts 中注册你的自定义存储。
import { defineConfig } from '@adonisjs/session'
import { mongoDbStore } from '#session_stores/mongodb_store'
export default defineConfig({
store: env.get('SESSION_DRIVER'),
stores: {
mongodb: mongoDbStore({
collection: 'sessions',
database: 'myapp'
})
}
})
设置你的环境变量以使用自定义存储。
SESSION_DRIVER=mongodb
完整的实现示例,请参阅 GitHub 上的内置会话存储 。