ABP 框架 - 深入理解与使用会话(Session)
在构建现代 Web 应用程序时,管理用户状态和上下文信息是至关重要的。会话(Session)作为一种常见的机制,用于在多个 HTTP 请求之间存储和跟踪用户特定的数据。然而,直接处理底层会话 API 可能会显得繁琐,并且容易产生重复代码。
ABP (ASP.NET Boilerplate) 框架提供了一个强大且抽象的会话管理系统,它将 ASP.NET Core 的会话概念与自己的多租户、授权等体系无缝集成。这套系统通过一个统一的 ISession 接口,让开发者能够以一种类型安全、易于测试和一致的方式访问当前用户和租户的信息。本文将深入探讨 ABP 框架中的会话机制,包括其核心概念、使用方法、常见实践以及最佳实践。
目录#
- ABP 会话系统概述
- 核心接口与类
- 会话数据的来源与存储
- 在应用中使用会话
- 在应用服务中使用
- 在控制器中使用
- 在领域层中使用(谨慎)
- 常见实践与示例
- 最佳实践与陷阱
- 总结
- 参考
ABP 会话系统概述#
ABP 的会话系统建立在 ASP.NET Core 的认证和授权机制之上。它并不替代 ASP.NET Core 的 HttpContext.Session(用于存储任意键值对数据),而是专注于提供对当前已认证用户和当前租户信息的标准化访问。
其主要目标包括:
- 抽象化:提供一致的接口来访问用户和租户 ID,无论底层身份验证机制是 Cookie、JWT Bearer Token 还是其他。
- 多租户支持:与 ABP 的多租户功能深度集成,轻松识别当前请求所在的租户上下文。
- 可测试性:由于依赖接口,在单元测试中可以轻松模拟会话对象。
- 空对象模式:提供空会话实现,优雅处理用户未登录或租户未识别的情况。
核心接口与类#
ISession#
这是 ABP 会话系统的核心接口,你的应用程序代码应该依赖于这个接口,而不是具体的实现。定义在 Volo.Abp 命名空间中。
public interface ISession
{
// 当前用户的ID,如果用户未登录,则返回null。
Guid? UserId { get; }
// 当前租户的ID,如果当前请求未关联任何租户(例如主机用户),则返回null。
Guid? TenantId { get; }
// 当前用户的多租户Side(用于判断用户是租户方还是主机方)。
MultiTenancySides MultiTenancySide { get; }
// 一个简便的属性,用于检查当前用户是否已认证(UserId != null)。
bool UserId.HasValue { get; }
}ClaimsAbpSession#
这是 ISession 的默认实现。它从当前 HttpContext 的 User 对象的 ClaimsPrincipal 中提取信息。
UserId从名为AbpClaimTypes.UserId的声明中获取。TenantId从名为AbpClaimTypes.TenantId的声明中获取。
ABP 框架在身份验证过程中会自动将这些声明添加到用户主体中。
NullAbpSession#
这是 ISession 接口的空对象模式实现。当 ABP 无法解析到有效的 HttpContext(例如,在后台线程或特定的控制台应用程序中)时,会使用此实现。它的所有属性都返回 null 或默认值,这可以防止在非 Web 上下文中出现空引用异常。
会话数据的来源与存储#
ABP 会话数据(UserId, TenantId)的生命周期与单个 HTTP 请求的认证状态绑定。其典型流程如下:
- 用户登录:用户通过
AccountController或类似的身份验证端点登录。ABP 会验证凭证,并创建一个包含UserId和TenantId等声明的身份票据。 - 令牌/Cookie 创建:对于基于浏览器的应用,此票据通常存储在加密的 Cookie 中。对于 API/SPA 应用,则可能是一个 JWT Bearer Token。
- 后续请求:客户端在后续请求中携带此 Cookie 或 Token(通常在 Authorization 头中)。
- 中间件处理:ABP 和 ASP.NET Core 的认证中间件会解析 Cookie/Token,重建
ClaimsPrincipal对象,并将其设置到当前HttpContext.User。 - ISession 读取:在请求处理过程中,当你访问
ISession.UserId或ISession.TenantId时,ClaimsAbpSession实现会从ClaimsPrincipal中读取相应的声明值。
重要:ABP 的 ISession 不用于存储任意数据(如购物车内容)。对于此类需求,你应直接使用 ASP.NET Core 的 HttpContext.Session。
在应用中使用会话#
在应用服务中使用#
在应用服务(Application Services)中,使用会话最常见的方式是依赖注入。ABP 框架会自动将 ISession 注册为依赖项。
using System;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace MyProject.ToDoItems
{
public class ToDoItemAppService : ApplicationService, IToDoItemAppService
{
private readonly IRepository<ToDoItem, Guid> _todoItemRepository;
// 通过构造函数注入 ISession
public ToDoItemAppService(IRepository<ToDoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
public async Task<ListResultDto<ToDoItemDto>> GetMyItemsAsync()
{
// 检查用户是否已登录
if (!CurrentUser.IsAuthenticated) // 另一种检查方式: CurrentUser 是应用服务基类提供的属性
{
return new ListResultDto<ToDoItemDto>();
}
// 获取当前用户的ID
var currentUserId = CurrentUser.Id; // 或者使用 Session.UserId
// var currentUserId = Session.UserId;
// 使用当前用户ID过滤数据
var items = await _todoItemRepository.GetListAsync(item => item.CreatorId == currentUserId);
return ObjectMapper.Map<List<ToDoItem>, ListResultDto<ToDoItemDto>>(items);
}
}
}注意:ApplicationService 基类已经为你注入了 ISession,并暴露为 Session 属性。同时,也提供了更方便的 CurrentUser 属性。
在控制器中使用#
在 MVC 或 Razor Page 控制器中,你同样可以通过依赖注入来使用 ISession。
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
namespace MyProject.Web.Controllers
{
public class HomeController : AbpController // 继承 AbpController
{
private readonly ISession _session;
// 构造函数注入
public HomeController(ISession session)
{
_session = session;
}
public IActionResult Index()
{
// 使用注入的会话
if (_session.UserId.HasValue)
{
ViewData["WelcomeMessage"] = $"Hello, User {_session.UserId}";
}
else
{
ViewData["WelcomeMessage"] = "Hello, Guest";
}
return View();
}
}
}在领域层中使用(谨慎)#
通常,领域层(Domain Layer)应该与表示层和基础设施层的概念(如 HTTP 会话)保持独立,以确保清晰的分层架构和可测试性。
最佳实践是将会话信息(如 UserId)从应用层显式地传递到领域层。
应用服务层:
public async Task CreateToDoItemAsync(CreateToDoItemDto input)
{
// 在应用服务中获取会话信息
var currentUserId = Session.UserId;
// 将UserId传递给领域实体或领域服务
var newItem = new ToDoItem(
GuidGenerator.Create(),
input.Text,
currentUserId // 显式传递
);
await _todoItemRepository.InsertAsync(newItem);
}领域实体:
public class ToDoItem : AuditedAggregateRoot<Guid>
{
public string Text { get; set; }
public Guid? OwnerUserId { get; set; } // 显式存储用户ID
public ToDoItem(Guid id, string text, Guid? ownerUserId)
{
Id = id;
Text = text;
OwnerUserId = ownerUserId; // 通过构造函数设置
}
}这种方式使领域逻辑更加纯净和明确。
常见实践与示例#
示例1:基于当前用户的数据过滤#
这是最常见的用法。ABP 框架的数据过滤器(Data Filter)机制已经内置了 IMustHaveCreator 接口,可以自动基于 ISession.UserId 进行过滤。你也可以手动实现。
// 手动在仓储查询中使用 Session
public async Task<int> GetCurrentUserItemCountAsync()
{
var queryable = await _todoItemRepository.GetQueryableAsync();
var count = await AsyncExecuter.CountAsync(
queryable.Where(item => item.CreatorId == CurrentUser.Id)
);
return count;
}示例2:多租户应用程序中的租户隔离#
在 SaaS 应用中,确保数据在不同租户间隔离是核心需求。
public async Task CreateTenantSpecificDataAsync(CreateDataDto input)
{
// 确保当前请求有租户上下文
if (!Session.TenantId.HasValue)
{
throw new AbpAuthorizationException("This operation requires a tenant context.");
}
var entity = new MyTenantAwareEntity
{
TenantId = Session.TenantId.Value, // 显式设置租户ID
// ... 其他属性
};
await _myRepository.InsertAsync(entity);
}ABP 的 IRepository 默认会自动启用多租户过滤器,因此查询时只会返回当前租户的数据。
示例3:自定义会话属性(扩展)#
虽然 ISession 接口本身是固定的,但你可以通过扩展方法为其添加“虚拟”属性。
public static class SessionExtensions
{
public static async Task<string> GetCurrentUserEmailAsync(this ISession session, IIdentityUserRepository userRepository)
{
if (!session.UserId.HasValue)
{
return null;
}
var user = await userRepository.FindAsync(session.UserId.Value);
return user?.Email;
}
}
// 使用扩展方法
// var userEmail = await _session.GetCurrentUserEmailAsync(_userRepository);注意:这只是一个获取数据的便捷方法,并不会在会话中存储数据。
最佳实践与陷阱#
- 依赖接口,而非实现:始终注入和使用
ISession接口,以保证代码的可测试性。 - 领域层避免直接使用 ISession:如之前所述,通过应用层将用户/租户信息显式传递给领域层。
- 处理空值:始终检查
UserId和TenantId的HasValue属性,因为用户可能未登录,或请求可能没有租户上下文。// 好 if (Session.UserId.HasValue) { ... } // 有风险,如果为null会抛出异常 var userId = Session.UserId.Value; - 理解作用域:
ISession是作用域生命周期(Scoped),即每个请求创建一个实例。不要在单例(Singleton)服务中注入它。 - 不要滥用 ISession 存储业务数据:记住,
ISession用于获取当前上下文,而非存储业务状态。使用缓存(IDistributedCache)或数据库来存储需要持久化的数据。 - 在后台作业/工作单元中使用 NullAbpSession:在后台线程或 Hangfire 作业中,由于没有 HTTP 上下文,
ISession将是NullAbpSession。确保你的代码能正确处理这种情况,或者在启动后台作业时显式传递所需的UserId和TenantId。
总结#
ABP 框架的会话系统是一个设计精良的抽象层,它极大地简化了在 ASP.NET Core 应用程序中处理当前用户和租户上下文的工作。通过遵循依赖注入原则和本文概述的最佳实践,你可以构建出结构清晰、易于测试且具备强大多租户能力的应用程序。
核心要点是:使用 ISession 接口获取上下文信息,在应用层进行决策,并将必要的信息显式地传递给下层,而不是让下层隐式地依赖于会话。