abp中多种登陆用户的设计

  • Post author:
  • Post category:其他



场景

在《学校管理系统》中,学生、家长、教师、教务都可能登陆,做一些属于他们自己的操作。这些用户需要的属性各不相同,比如学生有学号,而教师没有。

应用程序用户

在编码时,经常需要获取当前登陆用户的信息,这个当前登陆用户就是应用程序用户。asp.net提供了一整套方案来实现应用程序用户,包括身份验证、授权、asp.net identity等

应用程序用户与业务场景中的用户不同,应用程序用户只需要区别是谁,最简单情况只需要知道用户id,它不关心这个用户具体是教师还是学生或其它类型的用户。基于这个用户id还可以实现角色、授权等操作。 浅显点理解应用程序用户主要是识别用户id,方便实现角色和授权

而“学生”、“教师”则是《学校管理系统》这个场景中的具体业务概念。

abp中有个abp zero模块,它已实现应用程序用户管理、登陆、角色、授权等功能。

与abp用户一对一关联

当业务用户需要登陆时,一对一关联到应用程序用户。*

这样系统本身提供的登陆、注销、角色、授权等功能几乎保持不变,按需要可以实现多种业务用户类型。

模块化

我们的系统是按业务分模块开发的,参考:

https://www.bilibili.com/video/BV1b5411L7Hf/


有些业务模块可能存在业务用户,比如【商城模块】中的“顾客”,在设计时需要考虑模块化和可扩展性。

由于是独立模块,所以我们不知道将来模块被什么系统引用,因此也不知道具体的AbpUser类型,因为那是模块使用方自己定义的,因此在开发业务模块时不应该出现具体的AbpUser的类型,需要关联时只能关联Id,必要时可以引用抽象的AbpUser及其管理类。

Core层

按ddd和abp的方式这层需要建立:聚合根、实体、值对象、领域服务、领域事件、仓储接口。

下面以《学校管理系统》场景说明

实体

按ddd方式定义学生实体(它应该是聚合根)。

定义学号、所属学院、所属专业等属性,重点是它有个UserId属性,关联到应用程序用户,注意不要使用导航属性关联到AbpUser,一来ddd建议一个聚合中的实体不要使用导航属性关联到另一个聚合中的实体,二来我们使用的是模块化开发方式,我们在开发自己的模块时并不会知道模块将来被谁使用,因此就不会知道具体的类型,因为在abp中,用户是由开发人员自定义的。

不要考虑模块使用方使用继承来扩展实体,建议使用abp提供的IExtensionObject接口或动态属性系统。

领域实体中可以触发事件,以便模块调用方扩展

领域服务、领域事件、仓储接口。

这个根据需要决定是否定义。

领域服务可以提供虚方法以便模块使用方法提供子类重写,也可以触发相应事件让模块使用方去订阅。

由于将来可能增加更多用户类型,领域服务可以考虑抽象封装在BXJG.Utils(依赖abp的通用功能模块)中

EFCore层

ef映射和种子数据的处理

Application层

新增业务用户时考虑同时建立并关联用户、删除时则一并删除。

可以提供虚方法,以便模块调用方继承并重写

在模块中,没有将新增、删除应用程序用户的逻辑预留给模块使用方,而是在模块内部直接做了,模块调用方可以订阅UserCreating、UserDeleting事件来插入自己的逻辑

由于将来可能增加更多用户类型,应用服务可以考虑抽象封装在BXJG.Utils.Application中

session与登陆

如《学生管理系统》有个学生后台,里面全都是学生可以操作的功能,做这些功能时通常需要获取当前登陆学生的id。abp的seession功能只能获取当前登陆用户id,这是abpUser的id,这个id并不是我们需要的,它与学生实体是一对一关联的。

我们可以通过abp的seesion获取当前abpUser的id,然后去对应用户表查询得到业务用户的id,但这样比较浪费性能。

我们的思路是在用户登陆时将关联的业务场景用户的id存储到claim中,然后提供一个类似abpsession的session来在需要是提供当前业务用户的id

我们可以为每种用户建立登陆页面,在登陆时存储业务用户id到claim中,也可以在统一的登陆页面加各判断,然后获取对应业务用户类型表中的业务用户id

session的设计也可以抽象出来,因为有多种用户类型

如何使用

abp官方文档教程中有[扩展session](https://aspnetboilerplate.com/Pages/Documents/Articles%5CHow-To%5Cadd-custom-session-field-aspnet-core)的说明,我们也是按这个思路做的。由于系统可能存在多种业务用户,因此需要简单封装下,下面看看商城模块中的顾客是如何实现session和登陆的

实体实现IBusinessUserEntity

1 public class CustomerEntity : FullAuditedAggregateRoot<long>, IBusinessUserEntity , IMustHaveTenant, IExtendableObject
2 //略....

定义session接口和实现

 1 public interface ICustomerSession : IBusinessUserSession<long>{}
 2 
 3 public class CustomerClaimSession : BusinessUserClaimSession<long>, ICustomerSession
 4 {
 5     public CustomerClaimSession(IPrincipalAccessor principalAccessor,
 6     IMultiTenancyConfig multiTenancy,
 7     ITenantResolver tenantResolver,
 8     IAmbientScopeProvider<SessionOverride> sessionOverrideScopeProvider) :         base(principalAccessor, multiTenancy, tenantResolver,     sessionOverrideScopeProvider, CoreConsts.CustomerIdClaim)
 9     {
10     }
11 }    

在模块中实现ioc注入

1 public override void Initialize()
2 {
3      IocManager.IocContainer.Register(Component.For<ICustomerSession>()
4      .ImplementedBy<CustomerClaimSession>()
5      .LifestyleCustom<MsScopedLifestyleManager>()
6      .Named("sdf234sdf"));//CustomerClaimSession的父类已单例注册了,需要重命名下
7 
8 }

定义登陆器接口和实现

public interface ICustomerLoginManager<TUser> : IBusinessUserLoginManager<TUser> { }

/// <summary>
/// 提供与顾客登陆相关功能
/// </summary>
public class CustomerLoginManager<TTenant,
    TRole,
    TUser,
    TUserManager> : BusinessUserLoginManager<CustomerEntity,
                long,
                TTenant,
                TRole,
                TUser,
                TUserManager>, ICustomerLoginManager<TUser>
where TTenant : AbpTenant<TUser>
where TRole : AbpRole<TUser>, new()
where TUser : AbpUser<TUser>
where TUserManager : AbpUserManager<TRole, TUser>
{
        public CustomerLoginManager(IRepository<CustomerEntity, long> repository,
TUserManager userManager) : base(repository,
userManager,
CoreConsts.CustomerRoleName,
CoreConsts.CustomerIdClaim)
{ }
}                        

主程序的XXX.Core的Module类中添加依赖注入

1 IocManager.Register<ICustomerLoginManager<User>, CustomerLoginManager<Tenant, Role, User, UserManager>>(Abp.Dependency.DependencyLifeStyle.Transient);

使用登陆器

最后在主程序的UserClaimsPrincipalFactory中

 1  public class UserClaimsPrincipalFactory : AbpUserClaimsPrincipalFactory<User, Role>
 2   {
 3     private readonly ICustomerLoginManager<User> customerLoginManager;
 4     public UserClaimsPrincipalFactory(
 5     UserManager userManager,
 6     RoleManager roleManager,
 7     IOptions<IdentityOptions> optionsAccessor, ICustomerLoginManager<User>         customerLoginManager)
 8     : base(
 9     userManager,
10     roleManager,
11     optionsAccessor)
12     {
13     this.customerLoginManager = customerLoginManager;
14     }
15     public override async Task<ClaimsPrincipal> CreateAsync(User user)
16     {
17     var claim = await base.CreateAsync(user);
18     var c = await customerLoginManager.GetBusinessUserClaim(user);
19     if(c!=null)
20 claim.Identities.First().AddClaim(c);
21         return claim;
22     }
23 }

流程说明

登陆时AbpLoginManager会调用UserClaimsPrincipalFactory来向当前登陆用户的Claims中插入Claim

UserClaimsPrincipalFactory会调用ICustomerLoginManager,先判断用户是否是顾客的角色,若是则根据用户Id找到顾客Id,然后将顾客Id存储到Claims中

后续我们在需要获取当前登陆的顾客的id时在我们的服务中依赖注入ICustomerSession就可以了,它会从当前登陆用户的Claims中去找到顾客id

源码

完整源码参考:

商城模块中顾客的实现