.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

 

posted @ 2019-07-16 16:37  rockcode777  阅读(3528)  评论(1编辑  收藏  举报