本人開發環境
- Visual Studio 2019
- SQL Server 2012
- .Net Core 3.1
創建應用程序
我們使用ABP模板來創建應用程序,訪問http://www.aspnetboilerplate.com/Templates,你將會看到如下頁面
- 參照上圖所示的選項選擇,務必選擇include login...,會包含部分必須的IdentityServer4依賴包
- 輸入項目名稱,我這裏是"myAbpMulti"
- 輸入驗證碼
點擊“創建項目”, 接着我們就會從ABP模板網站上獲得一個項目源碼的壓縮包myAbpMulti.zip. 解壓縮myAbpMulti.zip就會獲得初始項目的源代碼。
運行應用程序
- 進入解壓目錄,點擊aspnet-core/myAbpMulti.sln,打開解決方案
- 在本地Sql Server數據庫實例中創建數據庫myAbpMulti
-
找到myAbpMulti.Web.Host/appsettings.json, 根據自己本地環境修改數據庫連接(ConnectionStrings)
"ConnectionStrings": { "Default": "Server=localhost; Database=myAbpMultiDb; Trusted_Connection=True;" }
- 使用數據庫遷移腳本創建初始數據庫
- 在Visual Studio中選擇工具-> Nuget包管理器-> 包管理器控制檯
- 設置myAbpMulti.Web.Host爲啓動項目
- 在包管理器控制檯中設置myAbpMulti.EntityFrameworkCore爲默認項目
- 包管理器控制檯中執行命令 update-database -verbos, 等待命令成功執行完成,就完成了數據庫的同步
-
運行myAbpMulti.Web.Host, 將會出現swagger的api頁面,程序運行成功
運行應用程序 Mvc項目
- 運行項目可能會出現確實libs靜態資源未引入情況導致沒有樣式,可在Mvc項目下找到libman.json文件,右擊,點擊還原客戶端庫即可,由於網絡原因可能出現還原失敗,可再點擊重試。
- 默認賬號[email protected],密碼123qwe登錄即可。
考慮到Mvc項目帶有太多不相關的視圖、靜態資源,選擇用Host項目,給Web.Core項目安裝Abp.ZeroCore.IdentityServer4。因爲EntityFrameworkCore項目依賴於Core項目,所以在Core項目安裝Abp.ZeroCore.EntityFrameworkCore和Abp.ZeroCore.IdentityServer4.EntityFrameworkCore(僅用於演示功能實現,實際項目可能會有變化)
修改Core項目的myAbpMultiCoreModule
[DependsOn(typeof(AbpZeroCoreModule), typeof(AbpZeroCoreIdentityServerEntityFrameworkCoreModule))] public class myAbpMultiCoreModule : AbpModule { //... }
修改Starup.cs
using Abp.AspNetCore; using Abp.Castle.Logging.Log4Net; using Abp.Extensions; using Abp.IdentityServer4; using Castle.Facilities.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Cors.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using myAbpMulti.Authentication.JwtBearer; using myAbpMulti.Authorization.Users; using myAbpMulti.Configuration; using myAbpMulti.Identity; using Swashbuckle.AspNetCore.Swagger; using System; using System.Linq; using System.Reflection; namespace myAbpMulti.Web.Host.Startup { public class Startup { private const string _defaultCorsPolicyName = "localhost"; private readonly IConfigurationRoot _appConfiguration; public Startup(IHostingEnvironment env) { _appConfiguration = env.GetAppConfiguration(); } public IServiceProvider ConfigureServices(IServiceCollection services) { // MVC services.AddMvc( options => options.Filters.Add(new CorsAuthorizationFilterFactory(_defaultCorsPolicyName)) ); IdentityRegistrar.Register(services); services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(IdentityServerConfig.GetIdentityResources()) .AddInMemoryApiResources(IdentityServerConfig.GetApiResources()) //.AddTestUsers(IdentityServerConfig.GetUsers().ToList()) .AddInMemoryClients(IdentityServerConfig.GetClients()) .AddAbpPersistedGrants<IAbpPersistedGrantDbContext>() .AddAbpIdentityServer<User>(); AuthConfigurer.Configure(services, _appConfiguration); //services.AddSignalR(); // Configure CORS for angular2 UI services.AddCors( options => options.AddPolicy( _defaultCorsPolicyName, builder => builder .WithOrigins( // App:CorsOrigins in appsettings.json can contain more than one address separated by comma. _appConfiguration["App:CorsOrigins"] .Split(",", StringSplitOptions.RemoveEmptyEntries) .Select(o => o.RemovePostFix("/")) .ToArray() ) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials() ) ); // Swagger - Enable this line and the related lines in Configure method to enable swagger UI services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new Info { Title = "myAbpMulti API", Version = "v1" }); options.DocInclusionPredicate((docName, description) => true); // Define the BearerAuth scheme that's in use options.AddSecurityDefinition("bearerAuth", new ApiKeyScheme() { Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Name = "Authorization", In = "header", Type = "apiKey" }); }); // Configure Abp and Dependency Injection return services.AddAbp<myAbpMultiWebHostModule>( // Configure Log4Net logging options => options.IocManager.IocContainer.AddFacility<LoggingFacility>( f => f.UseAbpLog4Net().WithConfig("log4net.config") ) ); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseAbp(options => { options.UseAbpRequestLocalization = false; }); // Initializes ABP framework. app.UseCors(_defaultCorsPolicyName); // Enable CORS! app.UseStaticFiles(); app.UseAuthentication(); app.UseJwtTokenMiddleware("IdentityBearer"); app.UseIdentityServer(); app.UseAbpRequestLocalization(); //app.UseSignalR(routes => //{ // routes.MapHub<AbpCommonHub>("/signalr"); //}); app.UseMvc(routes => { routes.MapRoute( name: "defaultWithArea", template: "{area}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); // Enable middleware to serve generated Swagger as a JSON endpoint app.UseSwagger(); // Enable middleware to serve swagger-ui assets (HTML, JS, CSS etc.) app.UseSwaggerUI(options => { options.SwaggerEndpoint(_appConfiguration["App:ServerRootAddress"].EnsureEndsWith('/') + "swagger/v1/swagger.json", "myAbpMulti API V1"); options.IndexStream = () => Assembly.GetExecutingAssembly() .GetManifestResourceStream("myAbpMulti.Web.Host.wwwroot.swagger.ui.index.html"); }); // URL: /swagger } } }
重點在於 IdentityRegistrar.Register(services) 之後的 services.AddIdentityServer(),以及app.UseAuthentication() 之後的兩行。其中的IdentityServerConfig和IdentityRegistrar類放在了同一個文件,如下:
using System.Collections.Generic; using System.Linq; using Abp.IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Test; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using myAbpMulti.Authorization; using myAbpMulti.Authorization.Roles; using myAbpMulti.Authorization.Users; using myAbpMulti.Editions; using myAbpMulti.MultiTenancy; namespace myAbpMulti.Identity { public static class IdentityRegistrar { public static IdentityBuilder Register(IServiceCollection services) { services.AddLogging(); return services.AddAbpIdentity<Tenant, User, Role>() .AddAbpTenantManager<TenantManager>() .AddAbpUserManager<UserManager>() .AddAbpRoleManager<RoleManager>() .AddAbpEditionManager<EditionManager>() .AddAbpUserStore<UserStore>() .AddAbpRoleStore<RoleStore>() .AddAbpLogInManager<LogInManager>() .AddAbpSignInManager<SignInManager>() .AddAbpSecurityStampValidator<SecurityStampValidator>() .AddAbpUserClaimsPrincipalFactory<UserClaimsPrincipalFactory>() .AddPermissionChecker<PermissionChecker>() .AddDefaultTokenProviders(); } } 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).ToList(), AllowedScopes = {"default-api"}, ClientSecrets = { new Secret("secret".Sha256()) } } }; } } }
因爲我們將用戶信息放在數據庫中,所以還要修改DbContext
修改爲:
using Abp.IdentityServer4; using Microsoft.EntityFrameworkCore; using Abp.Zero.EntityFrameworkCore; using myAbpMulti.Authorization.Roles; using myAbpMulti.Authorization.Users; using myAbpMulti.MultiTenancy; namespace myAbpMulti.EntityFrameworkCore { public class myAbpMultiDbContext : AbpZeroDbContext<Tenant, Role, User, myAbpMultiDbContext>, IAbpPersistedGrantDbContext { /* Define a DbSet for each entity of the application */ public DbSet<PersistedGrantEntity> PersistedGrants { get; set; } public myAbpMultiDbContext(DbContextOptions<myAbpMultiDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ConfigurePersistedGrantEntity(); } } }
修改完成後需要更新數據庫,執行Add-Migration "xxx" 和 Update-Database
至此IdentityServer的服務端配置完畢
根據IdentityServerConfig中填寫的client_id,client_secret,grant_type 填寫postman參數,username和password爲數據表abpusers中存儲的user信息
如果請求成功,可以在右側獲取到返回的token信息
向WebApi項目整合權限
在權限服務體系中,IdentityServer可以視作服務端,那其他所有需要權限驗證的api服務,就全部屬於客戶端。
回到myAbpBasic.Web項目,在Core項目安裝IdentityServer4.AccessTokenValidation
修改Startup.cs
public IServiceProvider ConfigureServices(IServiceCollection services) { //Configure DbContext // Mvc // identity server services.AddMvcCore().AddAuthorization().AddJsonFormatters(); services.AddAuthentication("IdentityBearer").AddIdentityServerAuthentication("IdentityBearer", options => { options.Authority = "http://localhost:21021/"; options.RequireHttpsMetadata = false; options.ApiName = "default-api"; // match with configuration in IdentityServer }); // Swagger }
AddAuthentication後的參數,必須和權限服務中UseJwtTokenMiddleware名稱一致。Authority自行改爲權限服務地址。
在app.UseMvc之前調用app.UseAuthentication()
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // authentication app.UseAuthentication(); app.UseMvc(); // swagger ....... }
爲了方便演示,給Default控制器Get方法增加Authorize特性,直接訪問該api,會提示401
修改postman配置
成功取得對應結果。
向MVC項目整合權限
暫緩,理論上mvc訪問受權限保護的頁面,會自動跳轉到權限服務進行驗證,成功後自動跳回原本頁面,頁面呈現後,左右的接口數據都需要直接從微服務獲取了。不再經過mvc控制器。那麼客戶端需要拿到這個token纔行。
權限角色區分
權限分爲 系統角色權限和業務角色權限
系統角色是指整體平臺的用戶角色,大體上將app用戶、網站用戶、後臺用戶分開,分別擁有不同的接口權限。系統角色需要在每個api服務的每個接口進行標記。規劃將系統角色分爲以下幾種
app_installer_user 師傅app
app_customer_user 商家app
web_customer_user 商家網站
web_admin_user 後臺最高權限
web_manager_user 後臺運維管理系統
web_business_user 後臺業務配置系統
web_order_user 後臺訂單系統
業務角色是指業務子系統內部的權限劃分。比如訂單處理系統中就有訂單查詢、修改、派單、改派等權限,這個權限列表是固化在代碼中的,業務系統中可以配置角色,以及每個角色擁有哪些權限。這部分權限不會出現在api服務中,而應該只在mvc客戶端或api網關中出現,不涉及應用服務。mvc客戶端可以輕鬆控制視圖,但提交數據的接口應該是通過服務中心直接調用下游接口的,這部分就變得不可控。
本文涉及的是前者系統角色權限。用戶登錄驗證後頒發token,同時該token也有對應的系統角色,一般而言,一個用戶賬號只對應一種客戶端角色。基本上不存在一個賬號既是商家,又是師傅的情況。
再次思考,identityserver中帶的Claims-role控制不適合當前系統角色場景,因爲它是用戶-角色對應關係的,更像是業務角色。我們需要的是客戶端-token-角色,也就是一類客戶端同屬一個角色,比如所有的商家版app客戶端,都屬於app_customer_user,那麼顯然可以簡單地使用identityServer4自帶的apiresource,clientid來控制。同時還允許一個賬號(客戶端)可能有多重權限的問題,admin_user有運維、業務、訂單的所有權限。比如這樣配置:
/// <summary> /// 允許使用認證服務的api列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("default-api", "Default (all) API"), new ApiResource("app_installer_user", "Default (all) API1"), new ApiResource("app_customer_user", "Default (all) API2"), new ApiResource("web_customer_user", "Default (all) API3"), new ApiResource("web_admin_user ", "Default (all) API4"), new ApiResource("web_manager_user ", "Default (all) API5"), new ApiResource("web_business_user ", "Default (all) API6"), new ApiResource("web_order_user ", "Default (all) API7") }; } /// <summary> /// 允許使用認證服務的應用客戶端 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = {"default-api"}, ClientSecrets = new[]{ new Secret("secret".Sha256())} }, new Client { ClientId = "app_installer_client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = {"app_installer_user"}, ClientSecrets = new[]{ new Secret("secret".Sha256())} }, new Client { ClientId = "web_admin_client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = { "web_admin_user", "web_manager_user", "web_business_user", "web_order_user", }, ClientSecrets = new[]{ new Secret("secret".Sha256())} } }; }
2019-4-11 10:15:57 之前的理解完全錯誤,ApiResource指的是提供api服務,比如serviceorder,servicepartner,對我們來說client只需要設置一個default即可,因爲暫時沒有需要限制某種類型客戶端只能訪問幾個服務。
public static class IdentityServerConfig { /// <summary> /// 允許使用認證服務的api列表 /// </summary> /// <returns></returns> public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("serviceorder", "Default (all) API"), new ApiResource("servicepartner", "Default (all) API1"), }; } /// <summary> /// /// </summary> /// <returns></returns> public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResources.Phone() }; } /// <summary> /// 允許使用認證服務的應用客戶端 /// </summary> /// <returns></returns> public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "default_client", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, AllowedScopes = { "serviceorder", "servicepartner", }, ClientSecrets = new[]{ new Secret("secret".Sha256())} } }; } }
繼續說這個權限體系,系統角色的實現好像有點困難。其實本質上是要限制api網關可以訪問的api範圍,可能用swagger的分組,爲每個api網關單獨配置swagger.json可以解決。每個網關只能看到自己有權限的api文檔(不知道能不能限制權限),或者是本身網關程序上寫的api接口。這樣完美解決系統角色的問題。
繼續看業務角色問題,業務角色權限爲了和api服務解耦,個人覺得應該放在上層api網關處理。目前打算是將各個子系統獨立開來,這樣每個子系統的MVC程序(對應mpa應用)(or api網關程序,對應spa應用)就需要數據庫來存儲這些權限規則,考慮到每個應用都需要這個數據庫,而且數據庫結構是類似的,所以建立一個後臺權限服務,考慮和基礎服務合併。這個數據庫包含後臺用戶表,後臺角色表,角色用戶表,角色權限表,用戶的註冊添加是在這個服務進行的,只不過註冊成功的同時需要向sso(identityserver)註冊新用戶。子系統mvc程序讀取所有權限列表,賦權給對應用戶,並將角色-權限對應關係(增加字段,api網關名稱)保存在數據庫中,這步再具體開發應用時再實現,現在只需要控制apigateway項目的權限即可。
參考文章:
https://aspnetboilerplate.com/Pages/Documents/Zero/Identity-Server
https://www.cnblogs.com/edisonchou/p/identityserver4_foundation_and_quickstart_01.html