三、功能
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是一个开源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。