• ABP-Zero模块


    一、介绍

    二、启动模版

    三、功能

      1,租户管理

      2,版本管理

      3,用户管理

      4,角色管理

      5,组织单位管理

      6,权限管理

      7,语言管理

      8,Identity Server集成

    一、介绍

    1,Zero模块实现ASP.NET Boilerplate框架的所有基本概念。如:

    租户管理(多租户)、角色管理、用户管理、session、授权(权限管理)、设置管理、语言管理、审计管理

    2,Microsoft ASP.NET Identity模块有2个版本:

    • Abp.Zero.* 软件包基于Microsoft ASP.NET身份和EF6.x.
    • Abp.ZeroCore.* 软件包基于Microsoft ASP.NET Core身份和Entity Framework Core。 这些软件包也支持.net内核。

    二、启动模版

    注意:确保您已经为您的Visual Studio安装了Typescript 2.0+,因为Abp.Web.Resources nuget包附带d.ts,它需要Typescript 2.0+。

    1,基于令牌的认证

    启动模板使用基于cookie的浏览器身份验证。 但是,如果要从移动应用程序中使用Web API或应用程序服务(通过动态Web api公开),则可能需要基于令牌的身份验证机制。 启动模板包括承载令牌认证基础设施。 .WebApi项目中的AccountController包含用于获取令牌的Authenticate操作。 然后我们可以使用令牌进行下一个请求。

    ①认证

    只需发送POST请求到http://localhost:6334/api/Account/Authenticate和 Context-Type="application/json"头,如下所示:

    我们发送了一个JSON请求体,其中包含userNameOrEmailAddress和密码。 另外,应该为租户用户发送TenancyName。 如上所述,返回JSON的result属性包含令牌。 我们可以保存并用于下一个请求。

    ②使用API

    在认证和获取令牌之后,我们可以使用它来调用任何授权的操作。 所有应用程序服务都可以远程使用。 例如,我们可以使用租户服务来获取租户列表: 

     

    只需向http://localhost:6334/api/services/app/tenant/GetTenants发送POST请求到 Content-Type="application/json"Authorization="Bearer your-auth-token "。 请求正文只是空的{}。 当然,请求和响应机构对于不同的API将是不同的。

    UI上几乎所有可用的操作也可用作Web API(由于UI使用相同的Web API),并且可以轻松地使用。

    2,Migrator 控制台应用程序

    启动模板包含一个工具Migrator.exe,可轻松迁移数据库。 您可以运行此应用程序来创建/迁移主机和租户数据库。

    此应用程序从它自己的.config文件获取主机连接字符串。 在开头的web.config中将是一样的。 确保配置文件中的连接字符串是您想要的数据库。 获取主机连接后,首先创建主机数据库或应用迁移(如果它已经存在)。 然后,它获取租户数据库的连接字符串,并为这些数据库运行迁移。 如果没有一个专用数据库,或者它的数据库已经迁移到另一个租户(用于多个租户之间的共享数据库),它会跳过租户。

    您可以在开发或产品环境中使用此工具来迁移部署时的数据库,而不是EntityFramework自己的Migrate.exe(这需要一些配置,并且可以在一次运行中为单个数据库工作)。

    3,单元测试
    启动模板包括测试基础架构设置和.Test项目下的一些测试。 您可以检查它们并轻松编写类似的测试。 实际上,它们是集成测试而不是单元测试,因为它们使用所有ASP.NET Boilerplate基础架构(包括验证,授权,工作单元...)测试代码。

    三、租户管理

    1,启用多租户
    ASP.NET Boilerplate和Zero模块可以运行多租户或单租户模式。 默认情况下禁用多租户。 我们可以在我们的模块的PreInitialize方法中启用它,如下所示:

    [DependsOn(typeof(AbpZeroCoreModule))]
    public class MyCoreModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.MultiTenancy.IsEnabled = true;    
        }
    
        ...
    }

    当我们创建一个基于ASP.NET Boilerplate和Zero模块的项目模板时,我们有一个Tenant实体和TenantManager领域服务。

    2,租户实体

    租户实体代表申请的租户。

    public class Tenant : AbpTenant<Tenant, User>
    {
    
    }

    它源于通用的AbpTenant类。 租户实体存储在数据库中的AbpTenants表中。 您可以将自定义属性添加到Tenant类。

    AbpTenant类定义了一些基本属性,大多数重要的是:

    • TenancyName:租户名称,唯一。不能正常改变 它可以用来为“mytenant.mydomain.com”这样的租户分配子域名。 Tenant.TenancyNameRegex常量定义命名规则。
    • Name: 任意可读的名称
    • IsActive:true:这个租户可以使用该应用程序;false:该租户的用户不能登录到系统

    AbpTenant类继承自FullAuditedEntity。 这意味着它具有创建,修改和删除审计属性。 它也是软删除。 所以,当我们删除租户时,它不会从数据库中删除,只是被标记为已删除。

    最后,AbpTenant的Id被定义为int。

    3,租户管理

    租户管理是为租户执行领域逻辑的服务

    public class TenantManager : AbpTenantManager<Tenant, Role, User>
    {
        public TenantManager(IRepository<Tenant> tenantRepository)
            : base(tenantRepository)
        {
    
        }
    }

    TenantManager也用于管理租户功能。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpTenantManager基类的方法。

    4,默认租户
    ASP.NET Boilerplate和Zero模块假设有一个预定义的租户,TenancyName为“Default”,Id为1.在单租户应用程序中,与租户一样使用。 在多租户应用程序中,您可以将其删除或将其设为被动。

    四、版本管理

    大多数SaaS(多租户)应用程序都有具有不同功能的版本(包)。 因此,他们可以向租户(客户)提供不同的价格和功能选项。

    1,版本实体(Edition Entity)

    版本是一个简单的实体代表应用程序的一个版本(或包)。 它只有Name和DisplayName属性。

    2,版本管理

    public class EditionManager : AbpEditionManager
    {
    }

    它来自AbpEditionManager类。 您可以注入并使用EditionManager来创建,删除和更新版本。 此外,EditionManager用于管理版本的功能。 它内部缓存版本功能以获得更好的性能。

    五、用户管理

    1,用户实体

    用户实体表示应用程序的用户。 它应该从AbpUser类派生,如下所示:

    public class User : AbpUser<Tenant, User>
    {
        //在这里添加您自己的用户属性
    }

    此类在您安装模块零时创建。 用户存储在数据库中的AbpUsers表中。 您可以将自定义属性添加到User类(并为更改创建数据库迁移)。

    AbpUser类定义了一些基本属性。 一些属性是:

    • UserName: 用户的登录名对于租户应该是唯一的。
    • EmailAddress: 用户的电子邮件地址。 对于租户来说应该是独一无二的。
    • Password: 用户的密码。
    • IsActive:true:用户可以登录到系统
    • Name(用户姓名) 和 Surname(姓氏)

    还有一些属性,如角色(Roles)、权限(Permissions)、租户(Tenant)、设置(Settings)、IsEmailConfirmed(邮箱是否确认)、

    AbpUser类继承自FullAuditedEntity。 这意味着它具有创建,修改和删除审计属性。 它也是软删除。 所以,当我们删除一个用户时,它不会从数据库中删除,只是被标记为已删除。

    AbpUser类实现了IMayHaveTenant过滤器,以便在多租户应用程序中正常工作。

    最后,用户的ID被定义为long。

    2,用户管理

    UserManager是为用户执行领域逻辑的服务:

    public class UserManager : AbpUserManager<Tenant, Role, User>
    {
        //...
    }

    您可以注入并使用UserManager来创建,删除,更新用户,授予权限,为用户更改角色等等。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpUserManager基类的方法。

    ①多租户

    UserManager旨在一次为单个租户工作。 它适用于当前租户的默认值。 让我们看看UserManager的一些用法:

    public class MyTestAppService : ApplicationService
    {
        private readonly UserManager _userManager;
    
        public MyTestAppService(UserManager userManager)
        {
            _userManager = userManager;
        }
    
        public void TestMethod_1()
        {
            //通过电子邮件寻找当前租户的用户
            var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com");
        }
    
        public void TestMethod_2()
        {
            //切换到租户42
            CurrentUnitOfWork.SetFilterParameter(AbpDataFilters.MayHaveTenant, AbpDataFilters.Parameters.TenantId, 42);
    
            //通过电子邮件找到租户42的一个用户
            var user = _userManager.FindByEmail("sampleuser@aspnetboilerplate.com");
        }
    
        public void TestMethod_3()
        {
            //禁用MayHaveTenant过滤器,所以我们可以覆盖所有用户
            using (CurrentUnitOfWork.DisableFilter(AbpDataFilters.MayHaveTenant))
            {
                //现在,我们可以在所有租户中搜索用户名
                var users = _userManager.Users.Where(u => u.UserName == "sampleuser").ToList();
    
                //或者我们可以添加TenantId过滤器,如果我们要搜索一个特定的租户
                var user = _userManager.Users.FirstOrDefault(u => u.TenantId == 42 && u.UserName == "sampleuser");
            }
        }
    }

    ②用户登录

    Zero模块定义了LoginManager,它具有用于登录应用程序的LoginAsync方法。 它检查所有用于登录的逻辑,并返回登录结果。 LoginAsync方法还会自动将所有登录尝试保存到数据库(即使是尝试失败)。 您可以使用UserLoginAttempt实体进行查询。

    ③关于IdentityResults

    UserManager的一些方法返回IdentityResult,而不是在某些情况下抛出异常。 这是ASP.NET Identity Framework的本质。 Zero模块也随之而来。 所以,我们应该检查这个返回的结果对象来知道操作是否成功。

    Zero模块定义了CheckErrors扩展方法,如果需要,可自动检查错误并抛出异常(本地化的UserFriendlyException)。 使用示例

    (await UserManager.CreateAsync(user)).CheckErrors();

    要获得本地化的例外,我们应该提供一个ILocalizationManager实例:

    (await UserManager.CreateAsync(user)).CheckErrors(LocalizationManager);

    3,外部认证

    Zero模块登录方式从数据库中的AbpUsers表中认证用户。 一些应用程序可能需要从一些外部来源(如活动目录,从另一个数据库的表甚至远程服务)来验证用户。

    对于这种情况,UserManager定义了一个名为“外部认证来源”的扩展点。 我们可以创建一个派生自IExternalAuthenticationSource的类并注册到配置。 有一个DefaultExternalAuthenticationSource类来简化IExternalAuthenticationSource的实现。 我们来看一个例子:

    public class MyExternalAuthSource : DefaultExternalAuthenticationSource<Tenant, User>
    {
        public override string Name
        {
            get { return "MyCustomSource"; }
        }
    
        public override Task<bool> TryAuthenticateAsync(string userNameOrEmailAddress, string plainPassword, Tenant tenant)
        {
            //TODO:验证用户并返回true或false
        }
    }

    在TryAuthenticateAsync方法中,我们可以从某些来源检查用户名和密码,如果给定用户由此源进行身份验证,则返回true。 此外,我们可以覆盖CreateUser和UpdateUser方法来控制用户创建和更新此源。

    当用户通过外部源进行身份验证时,Zero模块检查该用户是否存在于数据库(AbpUsers表)中。 如果没有,它调用CreateUser来创建用户,否则调用UpdateUser来允许身份验证源来更新现有的用户信息。

    我们可以在应用程序中定义多个外部认证源。 AbpUser实体具有AuthenticationSource属性,该属性显示哪个源验证了该用户。

    要注册我们的认证来源,我们可以在我们的模块的PreInitialize中使用这样的代码:

    Configuration.Modules.Zero().UserManagement.ExternalAuthenticationSources.Add<MyExternalAuthSource>();

    ①LDAP/Active Directory

    LdapAuthenticationSource是外部身份验证的一种实现,使用户可以使用其LDAP(活动目录)用户名和密码登录。

    如果我们要使用LDAP认证,我们首先将Abp.Zero.Ldap nuget包添加到我们的项目(通常为Core(domain)项目)。 那么我们应该为我们的应用程序扩展LdapAuthenticationSource,如下所示:

    public class MyLdapAuthenticationSource : LdapAuthenticationSource<Tenant, User>
    {
        public MyLdapAuthenticationSource(ILdapSettings settings, IAbpZeroLdapModuleConfig ldapModuleConfig)
            : base(settings, ldapModuleConfig)
        {
        }
    }

    最后,我们应该将模块依赖关系设置为AbpZeroLdapModule,并使用上面创建的auth源启用LDAP:

    [DependsOn(typeof(AbpZeroLdapModule))]
    public class MyApplicationCoreModule : AbpModule
    {
        public override void PreInitialize()
        {
            Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource));    
        }
    
        ...
    }

    在这些步骤之后,将为您的应用程序启用LDAP模块。 但默认情况下,LDAP认证未启用。 我们可以使用设置启用它。

    1)设置

    LdapSettingNames类定义了设置名称的常量。 您可以在更改设置(或获取设置)时使用这些常量名称。 LDAP设置是每个租户(对于多租户应用程序)。 因此,不同的租户具有不同的设置(请参阅在github上设置定义)。

    您可以在MyLdapAuthenticationSource构造函数中看到,LdapAuthenticationSource期望ILdapSettings作为构造函数参数。 此界面用于获取LDAP设置,如域,用户名和密码以连接到Active Directory。 默认实现(LdapSettings类)从设置管理器获取这些设置。

    如果您使用设置管理器,那么没有问题。 您可以使用设置管理器API更改LDAP设置。 如果需要,您可以将初始/种子数据添加到数据库,默认情况下启用LDAP验证。

    注意:如果您不定义域,用户名和密码,LDAP认证适用于当前域,如果应用程序在具有适当权限的域中运行。

    2)自定义设置

    如果要定义另一个设置源,可以实现自定义ILdapSettings类,如下所示:

    public class MyLdapSettings : ILdapSettings
    {
        public async Task<bool> GetIsEnabled(int? tenantId)
        {
            return true;
        }
    
        public async Task<ContextType> GetContextType(int? tenantId)
        {
            return ContextType.Domain;
        }
    
        public async Task<string> GetContainer(int? tenantId)
        {
            return null;
        }
    
        public async Task<string> GetDomain(int? tenantId)
        {
            return null;
        }
    
        public async Task<string> GetUserName(int? tenantId)
        {
            return null;
        }
    
        public async Task<string> GetPassword(int? tenantId)
        {
            return null;
        }
    }

    并在您的模块的PreInitialize中注册到IOC:

    [DependsOn(typeof(AbpZeroLdapModule))]
    public class MyApplicationCoreModule : AbpModule
    {
        public override void PreInitialize()
        {
            IocManager.Register<ILdapSettings, MyLdapSettings>(); //更改默认设置源
            Configuration.Modules.ZeroLdap().Enable(typeof (MyLdapAuthenticationSource));
        }
    
        ...
    }

    然后,您可以从任何其他来源获取LDAP设置。

    六、角色管理

    1,角色实体(Role Entity)

    角色实体代表应用程序的角色。 它应该从AbpRole类派生,如下所示:

    public class Role : AbpRole<Tenant, User>
    {
        //在这里添加你自己的角色属性
    }

    此类在您安装Zero模块时创建。 角色存储在数据库中的AbpRoles表中。 您可以将自定义属性添加到Role类(并为更改创建数据库迁移)。

    AbpRole定义了一些属性:

    • Name: 租户角色的独特名称。
    • DisplayName: 显示名称的角色。
    • IsDefault:这个角色是否默认分配给新用户?
    • IsStatic: 这个角色是静态的(预构建,不能被删除)。

    角色用于分组权限。 当用户有角色时,他/她将具有该角色的所有权限。 用户可以有多个角色。 该用户的权限将是所有分配角色的所有权限的合并。

    2,动态vs静态角色

    在模块零中,角色可以是动态的或静态的:

    • Static role: 静态角色有一个已知的名称(如“admin”),不能更改此名称(我们可以更改显示名称)。 它存在于系统启动,无法删除。 因此,我们可以根据静态角色名称编写代码。
    • Dynamic (non static) role: 我们可以在部署后创建动态角色。 然后我们可以授予该角色的权限,我们可以将角色分配给某些用户,我们可以将其删除。 在开发时期,我们无法知道动态角色的名称。

    使用IsStatic属性为角色设置它。 此外,我们应该在我们的模块的PreInitialize上注册静态角色。 假设我们对租户有一个“Admin”静态角色:

    Configuration.Modules.Zero().RoleManagement.StaticRoles.Add(new StaticRoleDefinition("Admin", MultiTenancySides.Tenant));

    因此,Zero模块将意识到静态角色。

    3,默认角色

    一个或多个角色可以设置为默认。 默认角色默认分配给新添加/注册的用户。 这不是开发时间属性,可以在部署后设置或更改。 使用IsDefault属性进行设置。

    4,角色管理

    public class RoleManager : AbpRoleManager<Tenant, Role, User>
    {
        //...
    }

    您可以注入并使用RoleManager来创建,删除,更新角色,授予角色权限等等。 你可以在这里添加你自己的方法。 此外,您可以根据自己的需要覆盖任何AbpRoleManager基类的方法。

    像UserManager一样,RoleManager的一些方法也返回IdentityResult,而不是在某些情况下抛出异常。 有关详细信息,请参阅用户管理文档

    七、组织单位管理

    组织单位(OU)可用于对用户和实体进行分层分组。

    1,OrganizationUnit实体

    OU由OrganizationUnit实体表示。 这个实体的基本属性是:

    • TenantId: 租户的这个OU的ID。 主机OU可以为空.
    • ParentId: 父OU的ID。 如果这是根OU,则可以为null.
    • Code:租户独有的层次化字符串代码.
    • DisplayName: 显示OU的名称.

    OrganizationUnit entitiy的主键(id)为long类型,它源自FullAuditedEntity,它提供审计信息并实现ISoftDelete界面(因此,OU不会从数据库中删除,它们只被标记为已删除)

    2,组织树
    由于OU可以拥有父级,所以租户的所有OU都是树形结构。 这棵树有一些规则:

    • 可以有多个根(它们具有null ParentId)。
    • 最大深度树被定义为一个常量作为OrganizationUnit.MaxDepth,它是16.
    • 存在用于第一级子计数的OU的(因为固定OU代码单位长度在下面解释)的限制.

    2,OU Code

    OU代码由OrganizationUnit Manager自动生成和维护。 这是一个字符串,如:

    "00001.00042.00005"

    该代码可用于轻松查询OU的所有子项(递归)的数据库。 这段代码有一些规则:

    • 这对租户来说是独一无二的.
    • 同一个OU的所有子代都有父OU的代码开头的代码.
    • 它是基于树中OU的级别的固定长度,如示例所示.
    • 当OU代码是唯一的,如果您移动OU,它可以是可更改的。 因此,我们应该通过Id引用OU,而不是Code.

    3,OrganizationUnit 管理

    可以注入OrganizationUnitManager类并用于管理OU。 常见用例有:

    • 创建,更新或删除OU
    • 在OU树中移动OU.
    • 获取关于OU树和项目的信息.

    4,多租户

    OrganizationUnitManager旨在一次为单个租户工作。 它适用于当前租户的默认值。

    5,常用案例

    在这里,我们将看到OU的常见用例。 您可以在这里找到样品的源代码。

    6,为组织单位创建实体,
    OU的最明显的使用是将实体分配给OU。 我们来看一个示例实体:

    public class Product : Entity, IMustHaveTenant, IMustHaveOrganizationUnit
    {
        public virtual int TenantId { get; set; }
    
        public virtual long OrganizationUnitId { get; set; }
        
        public virtual string Name { get; set; }
    
        public virtual float Price { get; set; }
    }

    我们简单地创建了OrganizationUnitId属性来将此实体分配给OU。 IMustHaveOrganizationUnit定义了OrganizationUnitId属性。 我们不必执行它,但建议提供标准化。 还有一个具有可空的OrganizationUnitId属性的IMayHaveOrganizationId。

    现在,我们可以将产品与OU相关联并查询特定OU的产品。

    注意; 产品实体有一个TenantId(它是IMustHaveTenant的属性),用于区分多租户应用中不同租户的产品(见多租户文档)。 如果您的应用程序不是多租户,则不需要此接口和属性。

    7,获取组织单位中的实体

    获取OU的产品很简单。 我们来看看这个示例域服务:

    public class ProductManager : IDomainService
    {
        private readonly IRepository<Product> _productRepository;
    
        public ProductManager(IRepository<Product> productRepository)
        {
            _productRepository = productRepository;
        }
    
        public List<Product> GetProductsInOu(long organizationUnitId)
        {
            return _productRepository.GetAllList(p => p.OrganizationUnitId == organizationUnitId);
        }
                    
    }

    我们可以简单地写一个反对Product.OrganizationUnitId的谓词,如上所示。

    8,在组织单位中获得实体,包括其子单位单位
    我们可能想要获得包含子组织单位的组织单位的产品。 在这种情况下,OU代码可以帮助我们:

    public class ProductManager : IDomainService
    {
        private readonly IRepository<Product> _productRepository;
        private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository;
    
        public ProductManager(
            IRepository<Product> productRepository, 
            IRepository<OrganizationUnit, long> organizationUnitRepository)
        {
            _productRepository = productRepository;
            _organizationUnitRepository = organizationUnitRepository;
        }
    
        [UnitOfWork]
        public virtual List<Product> GetProductsInOuIncludingChildren(long organizationUnitId)
        {
            var code = _organizationUnitRepository.Get(organizationUnitId).Code;
    
            var query =
                from product in _productRepository.GetAll()
                join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id
                where organizationUnit.Code.StartsWith(code)
                select product;
    
            return query.ToList();
        }
    }

    首先,我们得到了给定OU的代码。 然后我们创建了一个带有连接和StartsWith(代码)条件的LINQ(StartsWith在SQL中创建一个LIKE查询)。 因此,我们可以分级地获得OU的产品。

    9,过滤用户的实体
    我们可能希望获得特定用户的OU中的所有产品。 示例代码:

    public class ProductManager : IDomainService
    {
        private readonly IRepository<Product> _productRepository;
        private readonly UserManager _userManager;
    
        public ProductManager(
            IRepository<Product> productRepository, 
            UserManager userManager)
        {
            _productRepository = productRepository;
            _organizationUnitRepository = organizationUnitRepository;
            _userManager = userManager;
        }
    
        public async Task<List<Product>> GetProductsForUserAsync(long userId)
        {
            var user = await _userManager.GetUserByIdAsync(userId);
            var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user);
            var organizationUnitIds = organizationUnits.Select(ou => ou.Id);
    
            return await _productRepository.GetAllListAsync(p => organizationUnitIds.Contains(p.OrganizationUnitId));
        }
    }

    我们简单地发现了用户的OU的Ids。 然后在获得产品时使用包含条件。 当然,我们可以使用join创建一个LINQ查询来获取相同的列表。

    我们可能想要在用户的OU中获得产品,包括其子OU:

    public class ProductManager : IDomainService
    {
        private readonly IRepository<Product> _productRepository;
        private readonly IRepository<OrganizationUnit, long> _organizationUnitRepository;
        private readonly UserManager _userManager;
    
        public ProductManager(
            IRepository<Product> productRepository, 
            IRepository<OrganizationUnit, long> organizationUnitRepository, 
            UserManager userManager)
        {
            _productRepository = productRepository;
            _organizationUnitRepository = organizationUnitRepository;
            _userManager = userManager;
        }
    
        [UnitOfWork]
        public virtual async Task<List<Product>> GetProductsForUserIncludingChildOusAsync(long userId)
        {
            var user = await _userManager.GetUserByIdAsync(userId);
            var organizationUnits = await _userManager.GetOrganizationUnitsAsync(user);
            var organizationUnitCodes = organizationUnits.Select(ou => ou.Code);
    
            var query =
                from product in _productRepository.GetAll()
                join organizationUnit in _organizationUnitRepository.GetAll() on product.OrganizationUnitId equals organizationUnit.Id
                where organizationUnitCodes.Any(code => organizationUnit.Code.StartsWith(code))
                select product;
    
            return query.ToList();
        }
    }

    我们将Any与StartsWith条件组合在LINQ连接语句中。

    当然可能需要更复杂的要求,但是所有这些都可以用LINQ或SQL来完成。

    10,设置

    您可以注入并使用IOrganizationUnitSettings接口来获取组织单位设置值。 目前,只有一个可以根据您的应用需求进行更改的设置:

    MaxUserMembershipCount:用户最大允许的会员数。
    默认值为int.MaxValue,允许用户在同一时间成为无限制OU的成员。
    设置名称是在AbpZeroSettingNames.OrganizationUnits.MaxUserMembershipCount中定义的常量。

    八、权限管理

    1,角色权限

    如果我们授予权限角色,则所有用户都有权限授权(除非明确禁止特定用户使用)。

    我们使用RoleManager更改角色的权限。 例如,SetGrantedPermissionsAsync可用于在一个方法调用中更改角色的所有权限:

    public class RoleAppService : IRoleAppService
    {
        private readonly RoleManager _roleManager;
        private readonly IPermissionManager _permissionManager;
    
        public RoleAppService(RoleManager roleManager, IPermissionManager permissionManager)
        {
            _roleManager = roleManager;
            _permissionManager = permissionManager;
        }
    
        public async Task UpdateRolePermissions(UpdateRolePermissionsInput input)
        {
            var role = await _roleManager.GetRoleByIdAsync(input.RoleId);
            var grantedPermissions = _permissionManager
                .GetAllPermissions()
                .Where(p => input.GrantedPermissionNames.Contains(p.Name))
                .ToList();
    
            await _roleManager.SetGrantedPermissionsAsync(role, grantedPermissions);
        }
    }

    在这个例子中,我们得到一个RoleId和已授予的权限名称列表(input.GrantedPermissionNames是List <string>)作为输入。 我们使用IPermissionManager按名称查找所有权限对象。 然后我们调用SetGrantedPermissionsAsync方法来更新角色的权限。

    还有其他方法,如GrantPermissionAsync和ProhibitPermissionAsync一个一个地控制权限。

    2,用户权限

    虽然基于角色的权限管理对于大多数应用程序来说足够,但我们可能需要控制每个用户的权限。 当我们为用户定义权限设置时,它覆盖权限设置来自用户的角色。

    举个例子; 假设我们有一个应用程序服务来禁止用户的权限:

    public class UserAppService : IUserAppService
    {
        private readonly UserManager _userManager;
        private readonly IPermissionManager _permissionManager;
    
        public UserAppService(UserManager userManager, IPermissionManager permissionManager)
        {
            _userManager = userManager;
            _permissionManager = permissionManager;
        }
    
        public async Task ProhibitPermission(ProhibitPermissionInput input)
        {
            var user = await _userManager.GetUserByIdAsync(input.UserId);
            var permission = _permissionManager.GetPermission(input.PermissionName);
    
            await _userManager.ProhibitPermissionAsync(user, permission);
        }
    }

    UserManager有许多方法来控制用户的权限。 在这个例子中,我们得到一个UserId和PermissionName,并使用UserManager的ProhibitPermissionAsync方法来禁止用户的权限。

    当我们禁止用户的许可时,即使他/她的角色被授予许可,他/她也不能获得此许可。 我们可以说同样的原则给予。 当我们授予专门为用户授予的权限时,该用户被授予权限,即使用户的角色也不被授予权限。 我们可以为用户使用ResetAllPermissionsAsync来删除用户的所有用户特定权限设置。

    九、语言管理

    虽然在大多数情况下都有好处,但我们可能希望在数据库上动态定义语言和文本。 Zero模块允许我们动态管理每个租户的应用程序语言和文本。

    1,介绍

    ①EnableDbLocalization

    启用

    Configuration.Modules.Zero().LanguageManagement.EnableDbLocalization();

    这应该在顶级模块的PreInitialize方法(它是Web应用程序的Web模块)中导入Abp.Zero.Configuration命名空间(使用Abp.Zero.Configuration)来查看Zero()扩展方法)。

    ②种子数据库语言

    由于ABP将从数据库中获得语言列表,所以我们应该将默认语言插入数据库。 如果您使用EntityFramework,您可以像下面那样使用种子代码

    using System.Collections.Generic;
    using System.Linq;
    using Abp.Localization;
    using AbpCompanyName.AbpProjectName.EntityFramework;
    
    namespace AbpCompanyName.AbpProjectName.Migrations.SeedData
    {
        public class DefaultLanguagesCreator
        {
            public static List<ApplicationLanguage> InitialLanguages { get; private set; }
    
            private readonly AbpProjectNameDbContext _context;
    
            static DefaultLanguagesCreator()
            {
                InitialLanguages = new List<ApplicationLanguage>
                {
                    new ApplicationLanguage(null, "en", "English", "famfamfam-flag-gb"),
                    new ApplicationLanguage(null, "tr", "Türkçe", "famfamfam-flag-tr"),
                    new ApplicationLanguage(null, "zh-CN", "简体中文", "famfamfam-flag-cn"),
                    new ApplicationLanguage(null, "pt-BR", "Português-BR", "famfamfam-flag-br"),
                    new ApplicationLanguage(null, "es", "Español", "famfamfam-flag-es"),
                    new ApplicationLanguage(null, "fr", "Français", "famfamfam-flag-fr"),
                    new ApplicationLanguage(null, "it", "Italiano", "famfamfam-flag-it"),
                    new ApplicationLanguage(null, "ja", "日本語", "famfamfam-flag-jp"),
                    new ApplicationLanguage(null, "nl-NL", "Nederlands", "famfamfam-flag-nl"),
                    new ApplicationLanguage(null, "lt", "Lietuvos", "famfamfam-flag-lt")
                };
            }
    
            public DefaultLanguagesCreator(AbpProjectNameDbContext context)
            {
                _context = context;
            }
    
            public void Create()
            {
                CreateLanguages();
            }
    
            private void CreateLanguages()
            {
                foreach (var language in InitialLanguages)
                {
                    AddLanguageIfNotExists(language);
                }
            }
    
            private void AddLanguageIfNotExists(ApplicationLanguage language)
            {
                if (_context.Languages.Any(l => l.TenantId == language.TenantId && l.Name == language.Name))
                {
                    return;
                }
    
                _context.Languages.Add(language);
    
                _context.SaveChanges();
            }
        }
    }

    ③删除静态语言配置

    如果您具有如下所示的静态语言配置,您可以从配置代码中删除这些行,因为它将从数据库中获取语言。

    Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true));

    ④注意现有的XML本地化源

    不要删除您的XML本地化文件和源配置代码。 因为这些文件被用作回退源,并且所有的本地化密钥都是从这个源获得的。

    因此,当您需要一个新的本地化文本时,按照正常情况将其定义为XML文件。 您应至少在默认语言的XML文件中定义它。 因此,您不需要将本地化文本的默认值添加到数据库迁移代码。

    2,管理语言

    IApplicationLanguageManager接口被注入并用于管理语言。 它具有GetLanguagesAsync,AddAsync,RemoveAsync,UpdateAsync等方法来管理主机和租户的语言。

    ①语言列表逻辑

    语言列表按租户和主机存储,计算方法如下:

    • 有一个为主机定义的语言列表。 该列表被视为所有租户的默认列表.
    • 每个租户有一个单独的语言列表。 此列表继承主机列表添加特定于特定语言的语言。 租户不能删除或更新主机定义(默认)语言(但可以覆盖本地化文本,我们将在后面看到).

    ②ApplicationLanguage实体
    ApplicationLanguage实体表示租户或主机的语言。

    [Serializable]
    [Table("AbpLanguages")]
    public class ApplicationLanguage : FullAuditedEntity, IMayHaveTenant
    {
        //...
    }

    它的基本属性是:

    • TenantId (可空): 包含有关租户的Id,如果这种语言是特定于租户的。 如果这是主机语言,则为null。
    • Name: 语言名称 这必须是列表中的文化代码。
    • DisplayName: 显示语言的名称。 这可以是任意名称,一般是CultureInfo.DisplayName.
    • Icon: .语言的任意图标/标志。 这可以用于在UI上显示语言的标志

    此外,ApplicationLanguage继承了您所看到的FullAuditedEntity。 这意味着它是一个软删除实体,并自动审核(有关更多信息,请参阅实体文档)。

    ApplicationLanguage实体存储在数据库中的AbpLanguages表中。

    3,管理本地化文本

    IApplicationLanguageTextManager接口被注入并用于管理本地化文本。 它需要获取/设置租户或主机的本地化文本的方法。

    ①本地化文本
    让我们看看当你想本地化一个文本时会发生什么?

    • 尝试获得当前文化(获得使用CurrentThread.CurrentUICulture).
      • 它检查给定文本是否定义(覆盖)当前租户(获取使用IAbpSession.TenantId)在数据库中的当前文化。 如果定义,则返回值.
      • 然后,它检查数据库中当前文化中的主机是否定义(覆盖)给定文本。 如果定义,则返回值.
      • 然后,它检查在当前文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
    • 尝试寻找回归文化。 这样计算:如果现在的文化是“en-GB”,那么后备文化就是“en”.
      • 它检查数据库中的后备文化中当前租户是否定义(覆盖)给定文本。 如果定义,则返回值.
      • 然后,它检查在数据库中的后备文化中主机是否定义(覆盖)给定文本。 如果定义,则返回值.
      • 然后,它会检查在后备文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
    • 尝试找到默认文化.
      • 它检查数据库中默认文化中当前租户是否定义(覆盖)给定文本。 如果定义,则返回值.
      • 然后它检查在数据库中默认文化中主机是否定义(覆盖)给定文本。 如果定义,则返回值.
      • 然后,它会检查在默认文化中的底层XML文件中是否定义了给定的文本。 如果定义,则返回值.
    • 获取相同的文本或抛出异常
      • 如果根本没有找到给定的文本(键),ABP会抛出异常或通过用[和]包装返回相同的文本(键).

    因此,获取本地化的文本有点复杂。 但它起作用很快,因为它使用缓存。

    ②ApplicationLanguageText实体
    ApplicationLanguageText用于存储数据库中的本地化值。

    [Serializable]
    [Table("AbpLanguageTexts")]
    public class ApplicationLanguageText : AuditedEntity<long>, IMayHaveTenant
    {
        //...
    }

    它的基本属性是:

    • TenantId (可空):如果本地化文本是特定于租户的,则包含相关租户的身份证号码。 如果这是主机本地化的文本,它为null .
    • LanguageName: 语言名称 这必须是列表中的文化代码。 这与ApplicationLanguage.Name匹配,但不强制外键使其独立于语言条目。 IApplicationLanguageTextManager正确处理它.
    • Source: 本地化源名称.
    • Key: 本地化文本的键/名称.
    • Value: 本地化值.

    ApplicationLanguageText实体存储在数据库的AbpLanguageTexts表中。

    十、Identity Server集成

    Identity Server是一个开源OpenID Connect和OAuth 2.0框架。 它可以用于使您的应用程序在服务器上进行身份验证/单一登录。 它还可以为第三方客户端发出访问令牌。 本文档介绍如何将IdentityServer集成到项目中。

    1,安装

    由于EF核心包已经取决于第一个,您只能将Abp.ZeroCore.IdentityServer4.EntityFrameworkCore包安装到您的项目中。 安装到项目中包含您的DbContext(.EntityFrameworkCore项目为默认模板):

    Install-Package Abp.ZeroCore.IdentityServer4.EntityFrameworkCore

    然后你可以添加依赖关系到你的模块(一般来说,你的EntityFrameworkCore项目):

    [DependsOn(typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))]
    public class MyModule : AbpModule
    {
        //...
    }

    2,配置

    使用Abp.ZeroCore配置和使用IdentityServer4类似于独立使用IdentityServer4。 您应该阅读它自己的文档来了解和使用它。 在本文档中,我们仅显示了与Abp.ZeroCore集成所需的其他配置。

    ①Startup Class

    在ASP.NET Core Startup类中,我们应该将IdentityServer添加到服务集合和ASP.NET Core中间件管道中。 突出了与标准IdentityServer4使用的差异:

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //...
            
            services.AddAbpIdentity<Tenant, User, Role>()
                ...
                .AddAbpIdentityServer();
            
            //...
            
            services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources())
                .AddInMemoryApiResources(IdentityServerConfig.GetApiResources())
                .AddInMemoryClients(IdentityServerConfig.GetClients())
                .AddAbpPersistedGrants<YourDbContext>()
                .AddAbpIdentityServer<User>();
    
            //...
        }
    
        public void Configure(IApplicationBuilder app)
        {
            //...
            app.UseIdentityServer();
            //...
        }
    }

    1)在AddAbpIdentity ... chain之后添加了.AddAbpIdentityServer()。 在ABP中,启动模板AddAbpIdentity位于IdentityRegistrar.Register(services)方法中。 所以,你可以像IdentityRegistrar.Register(services).AddAbpIdentityServer()链接。

    2)在启动项目中,在IdentityRegistrar.Register(services)之后添加了services.AddIdentityServer()并添加了app.UseIdentityServer()只是app.UseAuthentication()。

    3,IdentityServerConfig类
    我们使用IdentityServerConfig类来获取身份资源,api资源和客户端。 您可以在自己的文档中找到有关此类的更多信息。 对于最简单的情况,它可以是一个静态类,如下所示

    public static class IdentityServerConfig
    {
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("default-api", "Default (all) API")
            };
        }
    
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResources.Phone()
            };
        }
    
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "client",
                    AllowedGrantTypes = GrantTypes.ClientCredentials.Union(GrantTypes.ResourceOwnerPassword),
                    AllowedScopes = {"default-api"},
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
            };
        }
    }

    4,DbContext Changes

     AddAbpPersistentGrants()方法用于保存持久数据存储的同意响应。 为了使用它,YourDbContext必须实现IAbpPersistedGrantDbContext接口,如下所示:

    public class YourDbContext : AbpZeroDbContext<Tenant, Role, User, YourDbContext>, IAbpPersistedGrantDbContext
    {
        public DbSet<PersistedGrantEntity> PersistedGrants { get; set; }
    
        public YourDbContext(DbContextOptions<YourDbContext> options)
            : base(options)
        {
        }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
    
            modelBuilder.ConfigurePersistedGrantEntity();
        }
    }

    IAbpPersistedGrantDbContext定义了PersistedGrants DbSet。 我们还应该调用上面显示的modelBuilder.ConfigurePersistedGrantEntity()扩展方法,以便为PersistedGrantEntity配置EntityFramework。

    请注意,YourDbContext中的此更改会导致新的数据库迁移。 因此,请记住使用“添加迁移”和“更新数据库”命令来更新数据库。

    即使您不调用AddAbpPersistedGrants <YourDbContext>()扩展方法,IdentityServer4仍将继续工作,但在这种情况下,用户同意响应将被存储在内存数据存储中(重新启动应用程序时会被清除)。

    5,JWT认证中间件
    如果我们要针对相同的应用程序授权客户端,我们可以使用IdentityServer身份验证中间件。

    首先,将IdentityServer4.AccessTokenValidation包从nuget安装到您的项目中:

    Install-Package IdentityServer4.AccessTokenValidation

    然后我们可以将中间件添加到Startup类中,如下所示

    app.UseIdentityServerAuthentication(
        new IdentityServerAuthenticationOptions
        {
            Authority = "http://localhost:62114/",
            RequireHttpsMetadata = false,
            AutomaticAuthenticate = true,
            AutomaticChallenge = true
        });

    我刚刚在启动项目中的app.UseIdentityServer()之后添加了这个。

    6,测试

    现在,我们的身份服务器已准备好从客户端获取请求。 我们可以创建一个控制台应用程序来发出请求并获得响应。

    • 在解决方案中创建一个新的控制台应用.
    • 将IdentityModel nuget软件包添加到控制台应用程序。 此包用于为OAuth端点创建客户端.

    虽然IdentityModel nuget软件包足以创建客户端并使用您的API,但我想以更安全的方式显示使用API:我们将将传入的数据转换为应用程序服务返回的DTO。

    • 从控制台应用程序添加对应用程序层的引用。 这将允许我们使用客户端应用层返回的相同的DTO类.
    • 添加Abp.Web.Common nuget包。 这将允许我们使用ASP.NET Boilerplate类中定义的AjaxResponse类。 否则,我们将处理原始的JSON字符串来处理服务器响应.

    那么我们可以改变Program.cs,如下所示:

    using System;
    using System.Net;
    using System.Net.Http;
    using System.Threading.Tasks;
    using Abp.Application.Services.Dto;
    using Abp.Json;
    using IdentityModel.Client;
    using Abp.MultiTenancy;
    using Abp.Web.Models;
    using IdentityServerIntegrationDemo.Users.Dto;
    using Newtonsoft.Json;
    
    namespace IdentityServerIntegrationDemo.ConsoleApiClient
    {
        class Program
        {
            static void Main(string[] args)
            {
                RunDemoAsync().Wait();
                Console.ReadLine();
            }
    
            public static async Task RunDemoAsync()
            {
                var accessToken = await GetAccessTokenViaOwnerPasswordAsync();
                await GetUsersListAsync(accessToken);
            }
    
            private static async Task<string> GetAccessTokenViaOwnerPasswordAsync()
            {
                var disco = await DiscoveryClient.GetAsync("http://localhost:62114");
    
                var httpHandler = new HttpClientHandler();
                httpHandler.CookieContainer.Add(new Uri("http://localhost:62114/"), new Cookie(MultiTenancyConsts.TenantIdResolveKey, "1")); //Set TenantId
                var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler);
                var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "123qwe");
    
                if (tokenResponse.IsError)
                {
                    Console.WriteLine("Error: ");
                    Console.WriteLine(tokenResponse.Error);
                }
    
                Console.WriteLine(tokenResponse.Json);
    
                return tokenResponse.AccessToken;
            }
    
            private static async Task GetUsersListAsync(string accessToken)
            {
                var client = new HttpClient();
                client.SetBearerToken(accessToken);
    
                var response = await client.GetAsync("http://localhost:62114/api/services/app/user/getUsers");
                if (!response.IsSuccessStatusCode)
                {
                    Console.WriteLine(response.StatusCode);
                    return;
                }
    
                var content = await response.Content.ReadAsStringAsync();
                var ajaxResponse = JsonConvert.DeserializeObject<AjaxResponse<PagedResultDto<UserListDto>>>(content);
                if (!ajaxResponse.Success)
                {
                    throw new Exception(ajaxResponse.Error?.Message ?? "Remote service throws exception!");
                }
    
                Console.WriteLine();
                Console.WriteLine("Total user count: " + ajaxResponse.Result.TotalCount);
                Console.WriteLine();
                foreach (var user in ajaxResponse.Result.Items)
                {
                    Console.WriteLine($"### UserId: {user.Id}, UserName: {user.UserName}");
                    Console.WriteLine(user.ToJsonString(indented: true));
                }
            }
        }
    }

    运行此应用程序之前,请确保您的Web项目已启动并运行,因为此控制台应用程序将向Web应用程序发出请求。 另外,请确保请求端口(62114)与您的Web应用程序相同。

    您可以在此处看到本教程的源代码:https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/IdentityServerDemo。 

  • 相关阅读:
    AOP从静态代理到动态代理 Emit实现
    云计算仿真工具CloudSim介绍和使用
    SSH框架中配置log4j的方法
    SSH常见面试题
    第一章
    shell 生成目录的树状视图、生成文件及子目录的汇总信息
    shell拼写检查,利用Linux字典
    SHELL:多文件的重命名和移动
    sort
    tr1
  • 原文地址:https://www.cnblogs.com/zd1994/p/7711942.html
Copyright © 2020-2023  润新知