ABP框架(3)基於IdentityService4建立權限服務

本人開發環境

  • 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文件,右擊,點擊還原客戶端庫即可,由於網絡原因可能出現還原失敗,可再點擊重試。

 

 

考慮到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

https://aspnetboilerplate.com/Pages/Documents/Authorization

https://www.cnblogs.com/rockcode777/p/10684129.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章