三、功能
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。