.Net Core微服務化ABP之六——處理Authentication


 

上篇中我們已經可以實現sso,並且為各個服務集成sso認證。本篇處理權限系統的角色問題,權限系統分兩層,第一層為整體系統角色權限,區分app用戶、后台用戶、網站用戶的接口權限,第二層為業務系統權限,對業務子系統各個崗位的人划分不同權限。

第一層的角色固化在代碼中,如商家app用戶,師傅app用戶,訂單系統用戶,接單系統用戶等,第二層角色可自定義,和現有系統的角色概念一致。權限系統需要讀取其他各個子系統的權限列表(標記在控制器、action上),並在系統權限中定義第一層權限和第二層權限。然后自定義一個Authorize方法,在里面實現第一層、第二層權限的authentication認證。想要實現權限鑒定,首先需要在用戶Claims中取得除用戶id外必要的用戶信息,所以先實現自定義Claims。

1.自定義Claims
2.MVC項目sso跳轉
3.Authentication驗證

 

自定義Claims

打開Authorize項目的Startup,可以看到AddAbpIdentityServer,查看源碼,我們模仿abp的方式自己實現一個認證。考慮到需要讀取數據庫,所以卸載WebCore項目

先實現AddProtonIdentityServer擴展方法

using System;
using System.IdentityModel.Tokens.Jwt;
using Abp.Authorization.Users;
using Abp.IdentityServer4;
using Abp.Runtime.Security;
using IdentityModel;
using IdentityServer4.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

namespace Authorize.Validation
{
    public static class IdentityServerBuilderExtensions
    {

        /// <summary>
        /// 一個簡單的擴展,用於注冊IdentityServer
        /// </summary>
        /// <typeparam name="TUser"></typeparam>
        /// <param name="builder"></param>
        /// <param name="optionsAction"></param>
        /// <returns></returns>
        public static IIdentityServerBuilder AddProtonIdentityServer<TUser>(this IIdentityServerBuilder builder, Action<AbpIdentityServerOptions> optionsAction = null)
            where TUser : AbpUser<TUser>
        {
            var options = new AbpIdentityServerOptions();
            optionsAction?.Invoke(options);

            builder.AddAspNetIdentity<TUser>();

            builder.AddProfileService<AbpProfileService<TUser>>();
            builder.AddResourceOwnerValidator<ProtonResourceOwnerPasswordValidator<TUser>>();

            builder.Services.Replace(ServiceDescriptor.Transient<IClaimsService, ProtonClaimService>());

            if (options.UpdateAbpClaimTypes)
            {
                AbpClaimTypes.UserId = JwtClaimTypes.Subject;
                AbpClaimTypes.UserName = JwtClaimTypes.Name;
                AbpClaimTypes.Role = JwtClaimTypes.Role;
            }

            if (options.UpdateJwtSecurityTokenHandlerDefaultInboundClaimTypeMap)
            {
                JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.UserId] = AbpClaimTypes.UserId;
                JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.UserName] = AbpClaimTypes.UserName;
                JwtSecurityTokenHandler.DefaultInboundClaimTypeMap[AbpClaimTypes.Role] = AbpClaimTypes.Role;
            }

            return builder;
        }
    }
}

還需要自定義ProtonResourceOwnerPasswordValidator和ProtonClaimService並且替換具體實現,這里的ProtonResourceOwnerPasswordValidator沒有修改abp默認的用戶表。

using Abp.Authorization.Users;
using Abp.Domain.Uow;
using Abp.Json;
using Abp.Runtime.Security;
using IdentityModel;
using IdentityServer4.AspNetIdentity;
using IdentityServer4.Models;
using IdentityServer4.Services;
using IdentityServer4.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Authorize.Validation
{
    /// <summary>
    /// 自定義 Resource owner password 驗證器
    /// </summary>
    /// <typeparam name="TUser"></typeparam>
    public class ProtonResourceOwnerPasswordValidator<TUser> : ResourceOwnerPasswordValidator<TUser> where TUser : AbpUser<TUser>
    {
        /// <summary>
        /// 使用真實數據庫驗證用戶
        /// </summary>
        protected UserManager<TUser> UserManager { get; }

        protected SignInManager<TUser> SignInManager { get; }

        protected ILogger<ResourceOwnerPasswordValidator<TUser>> Logger { get; }

        public ProtonResourceOwnerPasswordValidator(
            UserManager<TUser> userManager,
            SignInManager<TUser> signInManager,
            IEventService eventService,
            ILogger<ResourceOwnerPasswordValidator<TUser>> logger)
            : base(
                  userManager,
                  signInManager,
                  eventService,
                  logger)
        {
            UserManager = userManager;
            SignInManager = signInManager;
            Logger = logger;
        }

        [UnitOfWork]
        public override async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            var user = await UserManager.FindByNameAsync(context.UserName);
            if (user != null)
            {
                var result = await SignInManager.CheckPasswordSignInAsync(user, context.Password, true);
                if (result.Succeeded)
                {
                    Logger.LogInformation("Credentials validated for username: {username}", context.UserName);

                    //驗證通過返回結果 
                    //subjectId 為用戶唯一標識 一般為用戶id
                    //authenticationMethod 描述自定義授權類型的認證方法 
                    //authTime 授權時間
                    //claims 需要返回的用戶身份信息單元 此處應該根據我們從數據庫讀取到的用戶信息 添加Claims 如果是從數據庫中讀取角色信息,那么我們應該在此處添加 此處只返回必要的Claim
                    var sub = await UserManager.GetUserIdAsync(user);
                    var claims = GetAdditionalClaimsOrNull(user);
                    context.Result = new GrantValidationResult(
                        sub,
                        OidcConstants.AuthenticationMethods.Password,
                        claims);

                    //Logger.LogInformation($"claims:{claims.Select(d => d.Type + ":" + d.Value).ToJsonString()}");
                    return;
                }
                else if (result.IsLockedOut)
                {
                    Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", context.UserName);
                }
                else if (result.IsNotAllowed)
                {
                    Logger.LogInformation("Authentication failed for username: {username}, reason: not allowed", context.UserName);
                }
                else
                {
                    Logger.LogInformation("Authentication failed for username: {username}, reason: invalid credentials", context.UserName);
                }
            }
            else
            {
                Logger.LogInformation("No user found matching username: {username}", context.UserName);
            }

            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant);
        }

        protected virtual IEnumerable<Claim> GetAdditionalClaimsOrNull(TUser user)
        {
            var additionalClaims = new List<Claim>();
            if (user.TenantId.HasValue)
            {
                additionalClaims.Add(new Claim(AbpClaimTypes.TenantId, user.TenantId?.ToString()));
            }

            /*
             * 系統角色
             * 如超級管理員,xx子系統管理員,xx子系統普通用戶,xxApp普通用戶等,
             * 角色列表被固化在代碼中,sso系統中每個賬戶可擁有一個或多個系統角色,每個接口上有標記Authorize[Role="abc"],不屬於角色列表的用戶無法調用接口。
             * 注意和業務角色的區分,兩者是完全不同的概念。業務角色是指以往所說的同類權限的用戶組集合。
             */
            additionalClaims.Add(new Claim(ProtonClaimTypes.SystemRole, "systemrole")); //從用戶表取

            /*
             * 直營商id
             * 類似於tenantid,但全部放在業務系統處理相關邏輯。可能會換成tenantid
             */
            additionalClaims.Add(new Claim(ProtonClaimTypes.PartnerId, "0"));

            return additionalClaims;
        }
    }

    /// <summary>
    /// Proton自定義ClaimTypes
    /// </summary>
    public static class ProtonClaimTypes
    {
        public const string PartnerId = "partner_id";

        public const string SystemRole = "system_role";
    }
}

注意在GetAdditionalClaimsOrNull方法添加需要在api服務獲取的Claim屬性。ProtonClaimService代碼如下:

using Abp.Json;
using Abp.Runtime.Security;
using IdentityModel;
using IdentityServer4.Services;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace Authorize.Validation
{
    public class ProtonClaimService : DefaultClaimsService
    {
        public ProtonClaimService(IProfileService profile, ILogger<DefaultClaimsService> logger)
            : base(profile, logger)
        {
        }

        protected override IEnumerable<Claim> GetOptionalClaims(ClaimsPrincipal subject)
        {
            var claims = base.GetOptionalClaims(subject);

            var tenantClaim = subject.FindFirst(AbpClaimTypes.TenantId);
            if (tenantClaim != null)
            {
                claims = claims.Union(new[] { tenantClaim });
            }

            var sysRoleClaim = subject.FindFirst(ProtonClaimTypes.SystemRole);
            if (sysRoleClaim != null)
            {
                claims = claims.Union(new[] { sysRoleClaim });
            }
            var partnerIdClaim = subject.FindFirst(ProtonClaimTypes.PartnerId);
            if (partnerIdClaim != null)
            {
                claims = claims.Union(new[] { partnerIdClaim });
            }

            Logger.LogInformation(claims.Select(d => d.Type + ":" + d.Value).ToJsonString());
            return claims;
        }
    }
}

同樣需要把必要的Claims都加上,否則api服務取不到。如果是自帶的claims,想在api服務中取得,那么修改IdentityServerConfig.GetApiResources方法

 /// <summary>
        /// 允許使用認證服務的api列表
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("serviceorder", "Default (all) API",new List<string>(){JwtClaimTypes.Role}),
                new ApiResource("servicepartner", "Default (all) API1"),
            };
        }

修改一下訂單服務的Default控制器以適應測試數據

 // GET: api/Default
        [HttpGet]
        [Authorize(Roles = "Admin")]
        public String Get()
        {
            return $"claims:{HttpContext.User.Claims.Select(d => d.Type+":"+d.Value).ToJsonString()}";
            //return new string[] {  $"{serviceName}: {DateTime.Now.ToString()} {Environment.MachineName} " +
            //                       $"OS: {Environment.OSVersion.VersionString}" }; 
        }

        // GET: api/Default/5
        [Authorize(Roles = "SuperAdmin")]
        [HttpGet("{id}", Name = "Get")]
        public string Get(int id)
        {
            return $"claims:{HttpContext.User.Claims.Select(d => d.Type + ":" + d.Value).ToJsonString()}";
            return serviceName + ".value." + id;
        }

設想的結果是,Admin角色可以訪問/api/default接口,並且返回Claims數據集合,但無法訪問/api/default/1接口,我們測試一下

從authorize服務取得token,然后用這個token請求/api/default

正常返回claims信息,繼續請求/api/default/1

 

提示403 forbidden,測試通過

 

MVC項目sso跳轉

authorize服務本身的api接口,未授權401時,也會跳轉到Account/Login,就用這個來做測試。sso登錄需要統一的登錄頁,

IdentityServer4官方提供了一個QuickstartUI組件,包含了登錄、授權、查看權限等基本功能,可以基於此建立第一個版本

https://github.com/IdentityServer/IdentityServer4.Quickstart.UI

下載來代碼后,我們需要以下3個文件夾,復制到Authorize服務Host項目下

修改AuthConfigurer

services.AddAuthentication().AddIdentityServerAuthentication(configuration["Authentication:JwtBearer:DefaultScheme"], options =>
                {
                    options.Authority = $"http://{configuration["Service:IP"]}:{configuration["Service:Port"]}/";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "serviceauthorize";
                });

啟用CORS,支持跨域,這里直接用abp的代碼,未做改變

   // 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()
                )
            );
 app.UseCors(_defaultCorsPolicyName); // Enable CORS!

修改appsettings.json,增加以下節點

  "App": {
    "CorsOrigins": "http://localhost:21021,http://localhost:8080,http://localhost:8081,http://localhost:3000"
  },

去掉項目本身自帶的HomeController(和demo的homecontroller沖突了),然后修改demo帶來的每個控制器基類為AuthorizeControllerBase,比如:

 /// <summary>
    /// This sample controller allows a user to revoke grants given to clients
    /// </summary>
    [SecurityHeaders]
    [Authorize]
    public class GrantsController : AuthorizeControllerBase
    {
        private readonly IIdentityServerInteractionService _interaction;
        private readonly IClientStore _clients;
        private readonly IResourceStore _resources;
        private readonly IEventService _events;

        public GrantsController(IIdentityServerInteractionService interaction,
            IClientStore clients,
            IResourceStore resources,
            IEventService events)
        {
            _interaction = interaction;
            _clients = clients;
            _resources = resources;
            _events = events;
        }
        
        //......
    }

最后,添加一個defaultcontroller用於測試,給Get方法添加Authorize標記

    [DontWrapResult]
    [Route("api/[controller]")]
    public class DefaultController : AuthorizeControllerBase
    {

        private string serviceName = string.Empty;
        public IConfiguration Configuration { get; }


        public DefaultController(IConfiguration configuration)
        {
            Configuration = configuration;
            serviceName = Configuration["Service:Name"];
        }



        // GET: api/Default
        [HttpGet]
        [Authorize]
        public IEnumerable<string> Get()
        {
            return new string[] {  $"{serviceName}: {DateTime.Now.ToString()} {Environment.MachineName} " +
                                   $"OS: {Environment.OSVersion.VersionString}" };
        }

         // other methods
    }

運行項目,可以順利打開http://localhost:21021/Account/Login

訪問/api/default,會跳轉到login,輸入用戶名密碼登錄后,可以訪問到api/default這個受權限保護的接口,在實際mvc應用中,這個接口就是視圖頁

接下來處理新建一個mvc應用來實現上述功能。建立mvc應用是因為現有后端開發可以先行開發mpa應用,如果想開發spa,則需要單獨招聘前端人員。mvc應用只提供視圖,不提供任何其他接口,應用前端通過api網關直接向service請求數據,這是為了以后mpa轉變為spa更方便。

權限問題,mtn系統可以管理整個系統的用戶,用戶的系統角色屬性可能是商家app用戶,師傅app用戶,mtn管理員,mtn普通用戶,order管理員,order普通用戶等。一個用戶可以有多個系統角色。mtn系統對每個系統角色都可以自定義權限范圍,權限全集是所有頁面權限(來自mpa應用或spa自定義的規則)和接口權限。mtn管理員在進入mtn系統后,可以添加mtn系統的業務角色,指定每個業務角色的權限(在全集內),還可以管理mtn系統的用戶(從authorize服務取所有系統角色屬於mtn管理員和mtn普通用戶的所有用戶),並為這些用戶指定角色(可多個)。這部分功能放在基礎服務實現。

從abp下載一個帶login的模板,並用rename.ps1來重命名,注意CompanyName不要使用“Service”!,會導致rename時替換不應該替換的字符。去掉不相關的項目,只保留mvc

繼承過程中遇到這個問題

 IdentityServer4.Hosting.IdentityServerMiddleware[0]
      Unhandled exception: 系統找不到指定的文件。
Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: 系統找不到指定的文件。
   at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider, CngKeyOpenOptions openOptions)
   at System.Security.Cryptography.CngKey.Open(String keyName, CngProvider provider)
   at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
   at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()
   at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()
   at Microsoft.IdentityModel.Tokens.X509SecurityKey.get_PrivateKey()
   at Microsoft.IdentityModel.Tokens.X509SecurityKey.get_PrivateKeyStatus()

解決方案:應用程序池,高級設置,修改“加載用戶配置文件”為“True”即可!

https://blog.csdn.net/mushui0633/article/details/78596615

mvc應用訪問authorize限制的頁面,自動跳轉到權限中心登錄頁,但輸入賬號密碼登錄成功后,不跳轉,有以下幾種錯誤信息:

Showing login: User is not active

Client requested access token - but client is not configured to receive access tokens via browser(服務端clients屬性沒開)

Navigation property 'Claims' on entity of type 'User' cannot be loaded because the entity is not being tracked

Unable to obtain configuration from: '[PII is hidden]'

Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.

Executing ChallengeResult with authentication schemes ().

最后發現權限服務AddProtonIdentityServer方法中的AbpProfileService方法有問題,但是去掉后又導致role這個claim丟失,所以先寫固定值,后面自己重寫一套usermanager

增加一個ProtonProfileService

using Abp.Authorization.Users;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Authorize.Validation
{
    public class ProtonProfileService<TUser> : IProfileService where TUser : AbpUser<TUser>
    {
        protected UserManager<TUser> UserManager { get; }
        protected ILogger<ProtonProfileService<TUser>> Logger { get; }
        protected readonly IUserClaimsPrincipalFactory<TUser> ClaimsFactory;

        public ProtonProfileService(
            UserManager<TUser> userManager,
            ILogger<ProtonProfileService<TUser>> logger,
            IUserClaimsPrincipalFactory<TUser> claimsFactory)
        {
            UserManager = userManager;
            Logger = logger;
            ClaimsFactory = claimsFactory;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {

            //var sub = context.Subject?.GetSubjectId();
            //if (sub == null) throw new Exception("No sub claim present");

            //var user = await UserManager.FindByIdAsync(sub);
            //if (user == null)
            //{
            //    Logger?.LogWarning("No user found matching subject Id: {0}", sub);
            //}
            //else
            //{
            //    var principal = await ClaimsFactory.CreateAsync(user);
            //    if (principal == null) throw new Exception("ClaimsFactory failed to create a principal");

            //    context.AddRequestedClaims(principal.Claims);
            //}
            var claims = context.Subject.Claims.ToList();
          
            context.IssuedClaims = claims.ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
        }
    }
}

修改IdentityRegistrar中的Client

  new Client
                {
                    ClientId = "default_mvc_client",
                    ClientName="default_name1114324324",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    RedirectUris = { $"{Configuration["Clients:MvcClient:RedirectUri"]}signin-oidc" },
                    PostLogoutRedirectUris = { $"{Configuration["Clients:MvcClient:RedirectUri"]}signout-callback-oidc" },
                    AllowedScopes =new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "serviceorder",
                    },
                    AllowAccessTokensViaBrowser = true
                }

將IdentityServerBuilderExtensions中的AbpProfileService替換為ProtonProfileService

修改ProtonResourceOwnerPasswordValidator中的GetAdditionalClaimsOrNull,增加一個role,后期重寫時會替換為真實的用戶角色

 protected virtual IEnumerable<Claim> GetAdditionalClaimsOrNull(TUser user)
        {
            var additionalClaims = new List<Claim>();
            if (user.TenantId.HasValue)
            {
                additionalClaims.Add(new Claim(AbpClaimTypes.TenantId, user.TenantId?.ToString()));
            }

            /*
             * 系統角色
             * 如超級管理員,xx子系統管理員,xx子系統普通用戶,xxApp普通用戶等,
             * 角色列表被固化在代碼中,sso系統中每個賬戶可擁有一個或多個系統角色,每個接口上有標記Authorize[Role="abc"],不屬於角色列表的用戶無法調用接口。
             * 注意和業務角色的區分,兩者是完全不同的概念。業務角色是指以往所說的同類權限的用戶組集合。
             */
            additionalClaims.Add(new Claim(ProtonClaimTypes.SystemRole, "systemrole")); //從用戶表取


            /*
             * 業務角色
             * 業務子系統的角色
             */
            additionalClaims.Add(new Claim(JwtClaimTypes.Role, "Admin"));


            /*
             * 直營商id
             * 類似於tenantid,但全部放在業務系統處理相關邏輯。可能會換成tenantid
             */
            additionalClaims.Add(new Claim(ProtonClaimTypes.PartnerId, "0"));

            return additionalClaims;
        }

修改appsettings.json

"Clients": {
    "MvcClient": {
      "RedirectUri": "http://192.168.8.157:5200/",
      "LogoutRedirectUri": "http://192.168.8.157:5200/"
    }
  }

修改新增的mpa.maintenance項目的HomeController,給About這個action加上Authorize

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Mpa.Maintenance.Web.Controllers
{
    public class HomeController : MaintenanceControllerBase
    {
        public ActionResult Index()
        {
            return View();
        }

        [Authorize]
        public ActionResult About()
        {
            return View();
        }
    }
}

修改其Startup.cs,其中Authority地址必須和認證服務地址一致,ClientId和權限服務配置的client信息一致。

using System;
using System.IdentityModel.Tokens.Jwt;
using Abp.AspNetCore;
using Abp.Castle.Logging.Log4Net;
//using Abp.EntityFrameworkCore;
//using Mpa.Maintenance.EntityFrameworkCore;
using Castle.Facilities.Logging;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Mpa.Maintenance.Web.Startup
{
    public class Startup
    {
        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            //Configure DbContext
            //services.AddAbpDbContext<MaintenanceDbContext>(options =>
            //{
            //    DbContextOptionsConfigurer.Configure(options.DbContextOptions, options.ConnectionString);
            //});

            services.AddMvc(options =>
            {
                options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
            });

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = "Cookies";
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie("Cookies")
                .AddOpenIdConnect("oidc", options =>
                {
                    options.SignInScheme = "Cookies";

                    options.Authority = "http://192.168.8.157:5100";
                    options.RequireHttpsMetadata = false;

                    options.ClientId = "default_mvc_client";
                    options.ResponseType = "id_token token"; // allow to return access token
                    options.SaveTokens = true;
                });

            //Configure Abp and Dependency Injection
            return services.AddAbp<MaintenanceWebModule>(options =>
            {
                //Configure Log4Net logging
                options.IocManager.IocContainer.AddFacility<LoggingFacility>(
                    f => f.UseAbpLog4Net().WithConfig("log4net.config")
                );
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            app.UseAbp(); //Initializes ABP framework.

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                //app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }
            app.UseAuthentication();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

再次測試,把之前的兩個測試都重復一遍,驗證api客戶端和mvc客戶端都可以正常使用

Authentication驗證

 有了基礎的權限以后,我們需要考慮一下權限系統如何實現,然后再處理如何在controller,api,mpa_controller中獲取session信息。authorize特性可以基於用戶角色來驗證,但這個角色必須固化在action上面,只能處理固化的系統角色,並不符合當前業務需求。當前業務需求是基於系統角色、業務角色來限制權限,需要靈活可配置每個角色的權限。所以需要自定義authorizeattribute,並設置一些屬性來標記權限值(字符串),同時在控制器上也需要自定義attribute,這樣就獲得一個action的權限值,如“controllerName.actionName”,可以經由api接口或其他途徑提供給authorize服務,在數據庫中建立與角色的對應關系。最后再自定義一個filter用於驗證權限即可。session的實現,可以經由Httpcontext.user.claims取得並賦值。

首先實現session功能,參考https://www.jianshu.com/p/930c10287e2a方式二。

第一步是確保所需的claims已經正常添加到context,然后我們基於abpSession進行擴展。新建一個IProtonSesson,繼承IAbpSession,並添加我們需要的屬性.

using Abp.Runtime.Session;

namespace Proton.Web.Session
{
    public interface IProtonSession : IAbpSession
    {
        string PartnerId { get; }

        //int UserId { get; }

        string SystemPermissions { get; }
    }
}

新建ProtonSession類,繼承ClaimsAbpSession,IProtonSession,注意其中override重寫了UserId字段

using Abp.Configuration.Startup;
using Abp.Json;
using Abp.MultiTenancy;
using Abp.Runtime;
using Abp.Runtime.Session;
using Castle.Core.Logging;
using IdentityModel;
using Microsoft.AspNetCore.Http;
using Proton.IdentityModel;
using System;
using System.Linq;

namespace Proton.Web.Session
{
    public class ProtonSession : ClaimsAbpSession, IProtonSession
    {

        private IHttpContextAccessor _accessor;
        public ILogger Logger { get; set; }

        public ProtonSession(
            IPrincipalAccessor principalAccessor,
            IMultiTenancyConfig multiTenancy,
            ITenantResolver tenantResolver,
            IAmbientScopeProvider<SessionOverride> sessionOverrideScopeProvider,
            IHttpContextAccessor accessor)
            : base(
                principalAccessor,
                multiTenancy,
                tenantResolver,
                sessionOverrideScopeProvider)
        {
            _accessor = accessor;
            Logger = NullLogger.Instance;
        }

        public string PartnerId => GetClaimValue(ProtonClaimTypes.PartnerId);

        public override long? UserId => Int32.Parse(GetClaimValue(JwtClaimTypes.Subject));

        public string SystemPermissions => GetClaimValue(ProtonClaimTypes.SystemPermissions);

        private dynamic GetClaimValue(string claimType)
        {
            //var claimsPrincipal = PrincipalAccessor.Principal;

            //var claim = claimsPrincipal?.Claims.FirstOrDefault(c => c.Type == claimType);
            var claim = _accessor.HttpContext.User.Claims.FirstOrDefault(c => c.Type == claimType);
            Logger.InfoFormat(_accessor.HttpContext.User.Claims.Select(d => d.Type + ":" + d.Value).ToJsonString());
            if (string.IsNullOrEmpty(claim?.Value))
            {
                return null;
            }

            return claim.Value;
        }
    }
}

這里用到IHttpContextAccessor,參考https://www.cnblogs.com/liuxiaoji/p/6860122.html,需要在service提前注入實現。

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<IProtonSession, ProtonSession>();

替換掉注入的AbpSession,在ControllerBase中處理

        public new IProtonSession AbpSession { get; set; }

修改defaultcontroller來測試一下

   public string Get()
        {
            var session = AbpSession;
            var userId = AbpSession.UserId;
            var partnerid = AbpSession.PartnerId;


            var r = $"claims:{HttpContext.User.Claims.Select(d => d.Type + ":" + d.Value).ToJsonString()}";
            return $"session:{session.ToJsonString()};.\r\n"
                   + r + "\r\n"
                   + $"userId:{userId},partnerid:{partnerid}";
        }

 

可以看到成功取得userid,partnerid,並可以看到claims中所有的值。

下面處理權限驗證。首先定義ProtonAuthorizeAttribute

using Abp.Extensions;
using Microsoft.AspNetCore.Authorization;
using System;

namespace Proton.Web.Authorize
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
    public class ProtonAuthorizeAttribute : AuthorizeAttribute
    {
        /// <summary>
        /// Action權限值名稱,英文,同一控制器下唯一
        /// </summary>
        public string AuthName { get; set; }

        /// <summary>
        /// 權限中文名,用於權限列表展示
        /// </summary>
        public string AuthNameDisplay { get; set; }

        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="authName">權限值</param>
        /// <param name="displayName">權限值中文名稱。</param>
        public ProtonAuthorizeAttribute(string authName, string displayName = "")
        {
            AuthName = authName;
            AuthNameDisplay = displayName.IsNullOrEmpty() ? authName : displayName; ;
        }
    }
}

定義ProtonControllerAttribute

using System;

namespace Proton.Web.Authorize
{
    public class ProtonControllerAttribute : Attribute
    {
        /// <summary>
        /// ControllerID,Guid,唯一值,用於和數據庫對比記錄
        /// </summary>
        public string Id { get; set; }

        /// <summary>
        /// Controller權限值名稱
        /// </summary>
        public string ControllerName { get; set; }

        /// <summary>
        /// Controller中文名稱,用於權限列表顯示
        /// </summary>
        public string ControllerNameDisplay { get; set; }
        public ProtonControllerAttribute(string id, string controllerName, string controllerNameDisplay = "")
        {
            Id = id;
            ControllerName = controllerName;
            ControllerNameDisplay =
                String.IsNullOrEmpty(controllerNameDisplay) ? controllerName : controllerNameDisplay;
        }
    }
}

其中預置的ProtonAuthNames如下:

using System.ComponentModel;

namespace Proton.Web.Authorize
{
    /// <summary>
    /// 頁面元素預置權限類型
    /// 可以自定義,只允許添加通用類型,特殊類型直接用字符串
    /// </summary>
    public static class ProtonAuthNames
    {
        public const string List = "list";
        public const string Detail = "look";
        public const string Create = "create";
        public const string Edit = "edit";
        public const string Delete = "delete";
        public const string Import = "import";
        public const string Export = "export";
        public const string Accept = "accept";
        public const string Reject = "reject";
        public const string CustomeOne = "custom1";


    }
}

 

新建authorizefilter,定義AuthorizeFilter

using Abp.Collections.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Proton.IdentityModel;
using System.Collections.Generic;
using System.Linq;

namespace Proton.Web.Authorize
{
    /// <summary>
    /// 權限驗證攔截器
    /// 當前只用於第一級系統角色驗證,還需適應第二級業務角色驗證
    /// </summary>
    public class ProtonAuthorizationFilter : RequireHttpsAttribute
    {
        public override void OnAuthorization(AuthorizationFilterContext context)
        {
            if (context.Filters.Any(item => item is IAllowAnonymousFilter))
            {
                return;
            }

            if (!(context.ActionDescriptor is ControllerActionDescriptor))
            {
                return;
            }

            //不存在claims信息,需要驗證登錄
            if (!context.HttpContext.User.Claims.Any())
            {
                return;
            }
            
            var attributeList = new List<object>();
            attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.GetCustomAttributes(true));
            attributeList.AddRange((context.ActionDescriptor as ControllerActionDescriptor).MethodInfo.DeclaringType.GetCustomAttributes(true));


            //獲取當前服務id,控制器id,控制器名稱,action名稱
            var actionAttributes = attributeList.OfType<ProtonAuthorizeAttribute>().ToList();
            string authName = actionAttributes.FirstOrDefault()?.AuthName;

            var controllerAttributes = attributeList.OfType<ProtonControllerAttribute>().ToList();
            string controllerName = controllerAttributes.FirstOrDefault()?.ControllerName;
            var permissionStr = $"{controllerName}.{authName}";

            //獲取用戶權限並判斷
            var claims = context.HttpContext.User.Claims;
            var permissions = claims.FirstOrDefault(c => c.Type == ProtonClaimTypes.SystemPermissions)?.Value.Split(',');
            permissions = new[] { "default_one.list" };
            if (!permissions.IsNullOrEmpty() && permissions.Any(s => s.Equals(permissionStr)))
            {
                //授權通過
                return;
            }

            //未授權返回403
            context.Result = new StatusCodeResult(403);
        }
    }
}

准備測試數據,修改DefaultController

 [HttpGet]
        [ProtonAuthorize(ProtonAuthNames.List, "列表")]
        public string Get()
        {
            var session = AbpSession;
            var userId = AbpSession.UserId;
            var partnerid = AbpSession.PartnerId;


            var r = $"claims:{HttpContext.User.Claims.Select(d => d.Type + ":" + d.Value).ToJsonString()}";
            return $"session:{session.ToJsonString()};.\r\n"
                   + r + "\r\n"
                   + $"userId:{userId},partnerid:{partnerid}";
        }

        // GET: api/Default/5
        [HttpGet("{id}", Name = "Get")]
        [ProtonAuthorize(ProtonAuthNames.Accept, "提交")]
        public string Get(int id)
        {
            return serviceName + ".value." + id;
        }

其中控制器也要加上Attribute

    [ProtonController("72701F1B-4DAF-4E4C-AB87-9DA36CEC9D02", "default_one", "默認頁面")]
    public class DefaultController : AuthorizeControllerBase

filter中定義了固化的permission="default_one.list"(claims沒傳回來,需要修改),所以理論上是/api/default和/api/default/1都需要登錄,但前者有權限,可以讀取內容,后者無權限,返回403.

 測試結果表明,api訪問可以正常取得所有claim,mpa應用和authorize服務不能,只能取到以下信息,這個問題后面在解決

 

參考文章:

https://www.cnblogs.com/stulzq/p/8726002.html

https://www.cnblogs.com/edisonchou/p/integration_authentication-authorization_service_foundation.html

https://www.cnblogs.com/edisonchou/p/identityserver4_foundation_and_quickstart_01.html

https://www.jianshu.com/p/930c10287e2a

https://www.cnblogs.com/jaycewu/p/7791102.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM