在现代企业级多租户 B2B SaaS 系统中,身份中心(Identity Subsystem)是整个系统的安全生命线:
- 一人多企业(Multi-Tenancy Mapping):一个自然人用户(如财务专家或集团管理员)可能同时隶属于多个企业租户,在不同租户下拥有完全不同的角色与权限;
- 企业级安全基线:不仅要防范暴力破解撞库、密码弱口令,还要提供开箱即用的 TOTP 双因子认证(2FA)、操作员模拟与企业级 SSO(SAML2 / OIDC)对接能力;
- 高并发与无状态:登录校验毫秒级响应,Token 签发与刷新具备分布式黑名单阻断能力。
BitzOrcas.Modern Identity 模块采用垂直切片与严格的多租户聚合隔离设计,为整个平台提供坚不可摧的身份基石。
Identity 模块核心架构与交互拓扑
第一步:核心领域聚合根 UserAggregate
用户聚合根采用**统一聚合根(Unified Aggregate)**范式,直接映射物理表 SysUser:
using System;using System.Collections.Generic;using System.ComponentModel;using BitzOrcas.Domain.Entities;using BitzOrcas.Domain.Results;using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Identity.Domain;
public static class UserErrors{ public static readonly Error InvalidEmail = Error.Validation("User.InvalidEmail", "邮箱格式不正确。");}
[BitzTable("SysUser", IsTenant = false, IsSoftDelete = true, Description = "平台全局用户主表")][BitzIndex("IX_SysUser_Email", nameof(Email), IsUnique = true)]public sealed class UserAggregate : AggregateRoot<string>{ [BitzColumn(Length = 120, IsRequired = true)] public string Email { get; private set; } = string.Empty;
[BitzColumn(Length = 200, IsRequired = true)] public string PasswordHash { get; private set; } = string.Empty;
[BitzColumn(Length = 50)] public string PhoneNumber { get; private set; } = string.Empty;
// 用户全局状态:Active, Suspended, PendingActivation [BitzColumn(IsRequired = true)] public UserStatus Status { get; private set; } = UserStatus.PendingActivation;
// 是否启用了 TOTP 双因子认证 [BitzColumn(IsRequired = true)] public bool IsTwoFactorEnabled { get; private set; }
// 租户关联子实体列表(记录该用户加入的所有企业租户及当前激活租户) private readonly List<UserTenantMembership> _memberships = []; public IReadOnlyCollection<UserTenantMembership> Memberships => _memberships.AsReadOnly();
[Obsolete("仅供 ORM 持久化物化使用。请使用 Create 工厂方法。", error: true)] [EditorBrowsable(EditorBrowsableState.Never)] public UserAggregate() : base("0") { }
private UserAggregate(string id, string email, string passwordHash) : base(id) { Email = email; PasswordHash = passwordHash; Status = UserStatus.Active; }
// 领域工厂方法:创建新用户 public static Result<UserAggregate> Create(string email, string passwordHash, string initialTenantId) { // 1. 纯内存不变量校验 if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) { return Result<UserAggregate>.Failure(UserErrors.InvalidEmail); }
var user = new UserAggregate(Guid.NewGuid().ToString("N"), email.Trim().ToLowerInvariant(), passwordHash);
// 2. 绑定初始企业租户 user._memberships.Add(new UserTenantMembership(initialTenantId, isDefault: true));
// 3. 产生用户注册领域事件 user.AddDomainEvent(new UserRegisteredDomainEvent(user.Id, user.Email, initialTenantId));
return Result<UserAggregate>.Success(user); }}第二步:垂直切片写操作(LoginCommandHandler)
登录用例完全自包含,在 Handler 中完成凭据校验与令牌签发:
using System.Threading;using System.Threading.Tasks;using BitzOrcas.Application.Abstractions.Security;using BitzOrcas.Domain.Results;using BitzOrcas.Identity.Contracts;
public static class AuthErrors{ public static readonly Error InvalidCredentials = Error.Unauthorized("Auth.InvalidCredentials", "用户名或密码不正确。");
public static readonly Error AccountSuspended = Error.Forbidden("Auth.AccountSuspended", "账户已被禁用或待激活。");}
public sealed class LoginCommandHandler( IUserRepository userRepository, IPasswordHasher passwordHasher, ITokenService tokenService){ public async ValueTask<Result<LoginResponseDto>> Handle(LoginCommand command, CancellationToken ct) { // 1. 根据标准化邮箱检索用户 var user = await userRepository.FindByEmailAsync(command.Email.ToLowerInvariant(), ct); if (user is null) { // 恒定时间响应,防止用户枚举攻击 return Result<LoginResponseDto>.Failure(AuthErrors.InvalidCredentials); }
// 2. 校验密码哈希(Argon2id 算法抗 GPU 破解) var isPasswordValid = passwordHasher.Verify(command.Password, user.PasswordHash); if (!isPasswordValid) { return Result<LoginResponseDto>.Failure(AuthErrors.InvalidCredentials); }
// 3. 检查账户状态 if (user.Status != UserStatus.Active) { return Result<LoginResponseDto>.Failure(AuthErrors.AccountSuspended); }
// 4. 签发多租户 JWT 与 Refresh Token var tokenPair = await tokenService.GenerateTokensAsync(user, command.TenantId, ct);
return Result<LoginResponseDto>.Success(new LoginResponseDto( AccessToken: tokenPair.AccessToken, RefreshToken: tokenPair.RefreshToken, ExpiresInSeconds: tokenPair.ExpiresIn)); }}总结与生产最佳实践
Identity 模块的设计准则:
- 安全第一:密码哈希默认使用 Argon2id,绝不保存明文;
- 多租户自适应:用户可在多个企业租户间无缝切换,无需重复注册多个邮箱;
- 事件驱动解耦:用户注册、密码修改与登录审计通过领域事件异步分发。