zoukankan      html  css  js  c++  java
  • asp.net core 认证及简单集群

    众所周知,在Asp.net WebAPI中,认证是通过AuthenticationFilter过滤器实现的,我们通常的做法是自定义AuthenticationFilter,实现认证逻辑,认证通过,继续管道处理,认证失败,直接返回认证失败结果,类似如下:

    public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
            {
                var principal = await this.AuthenticateAsync(context.Request);
                if (principal == null)
                {
                    context.Request.Headers.GetCookies().Clear();
                    context.ErrorResult = new AuthenticationFailureResult("未授权请求", context.Request);
                }
                else
                {
                    context.Principal = principal;
                }
            }

    但在.net core中,AuthenticationFilter已经不复存在,取而代之的是认证中间件。至于理由,我想应该是微软觉得Authentication并非业务紧密相关的,放在管道中间件中更合适。那么,话说回来,在.net core中,我们应该怎么实现认证呢?如大家所愿,微软已经为我们提供了认证中间件。这里以CookieAuthenticationMiddleware中间件为例,来介绍认证的实现。

    1、引用Microsoft.AspNetCore.Authentication.Cookies包。项目实践中引用的是"Microsoft.AspNetCore.Authentication.Cookies": "1.1.0"。

    2、Startup中注册及配置认证、授权服务:

    服务注册:

    services.AddMvc(options =>
                {
                    //添加模型绑定过滤器
                    options.Filters.Add(typeof(ModelValidateActionFilter));
    
                    //添加授权过滤器,以便强制执行Authentication跳转及屏蔽逻辑
                    //var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
                    var policy = new AuthorizationPolicyBuilder().AddRequirements(new AuthenticationRequirement()).Build();
                    options.Filters.Add(new AuthorizeFilter(policy));
                });
    
                //services.AddAuthorization(options =>
                //{
                //    options.AddPolicy("RequireAuthentication", policy => policy.AddRequirements(new AuthenticationRequirement()));
                //});

    大家注意,上面代码中有两处注释掉的地方。第一处注释,RequireAuthenticatedUser()是.net core预定义的授权验证,代表通过授权验证的最低要求是提供经过认证的Identity。Demo中,我的要求也是这个,只要是经过基本认证的用户即可,那为什么Demo中没有使用呢?因为这里是个坑!实际实践中,我发现,采用注释中的做法,无论如何,调用总是返回401,迫不得已,download认证及授权源码,发现该处逻辑是这样的:

    var user = context.User;
                var userIsAnonymous =
                    user?.Identity == null ||
                    !user.Identities.Any(i => i.IsAuthenticated);
                if (!userIsAnonymous)
                {
                    context.Succeed(requirement);
                }

    加入断点猛调,发现IsAuthenticated永远是false!!!迫不得已,反编译查看源码,发现ClaimsIdentity的IsAuthenticated属性是这样定义的:

    WTF!!!坑爹么这是!!!.net framework中, 记得 这里的逻辑是,只要Name非空,就返回true,到了.net core中成了这样,你说坑不坑。。。

    那怎么办?总不能放弃吧?我想,大家第一想法应该是继承ClaimsIdentity自定义一个Identity,尤其是看到属性上那个virtual的时候,我也不例外。可继承后, 发现认证框架那儿依然不认,还是一直返回false,可能是我哪里用的不对吧。所以,Startup中第一处注释出现了。最终解决方案是自定义AuthenticationRequirement及处理器,实现要求的验证,如下:

    public class AuthenticationRequirement : AuthorizationHandler<AuthenticationRequirement>, IAuthorizationRequirement
        {
            protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthenticationRequirement requirement)
            {
                var user = context.User;
                var userIsAnonymous =
                    user?.Identity == null
                    || string.IsNullOrWhiteSpace(user.Identity.Name);
                if (!userIsAnonymous)
                {
                    context.Succeed(requirement);
                }
                return TaskCache.CompletedTask;
            }
        }

     上述代码红色的部分便是相对默认实现变化的部分。

    startup中第二部分注释,是注册授权策略的,注册方法也是官网文档中给出的注册方法。那为什么这里又没有采用呢?因为,如果按注释中的方法配置,我需要在每个希望认证的控制器或方法上都用Authorize标记,甚至还需要在特性上配置角色或策略,而这里我的预设是全局认证,所以,直接以全局过滤器的形式添加到了MVC处理管道中。读到这里,细心的读者应该有疑问了,你一个简单的认证,跟授权毛线关系啊,注册授权过滤器作甚!我也觉得没关系啊,这是net core认证的第二个坑,那就是,在.net core或者微软看来,认证仅仅提供Principal的生成、序列化、反序列化及重新生成Principal,它的职责确实也包括了返回401、403等各种认证失败信息,但这部分不会主动触发,必须有处理管道中其他逻辑去触发。我仔细阅读了官网文档,得出的大致结论是,.net core大概认为,认证是个多样化的过程,不光有我们目前看到的或需要的某一种认证,实际需求中很可能会多种认证并存,我们的API也可能会同时允许多种认证方式通过,所以某一种认证失败就直接返回401或403是错误的。这是实践当中第二个坑!那话说回来,添加了授权,就可以触发这个过程,这个是看源码发现的,具体流程就是,如果授权失败,过滤器会返回一个challengeResult,这个Result最终会跑到认证中间件中的对应Challenge方法,在.net core源码中表现如下:

    public async Task ChallengeAsync(ChallengeContext context)
            {
                ChallengeCalled = true;
                var handled = false;
                if (ShouldHandleScheme(context.AuthenticationScheme, Options.AutomaticChallenge))
                {
                    switch (context.Behavior)
                    {
                        case ChallengeBehavior.Automatic:
                            // If there is a principal already, invoke the forbidden code path
                            var result = await HandleAuthenticateOnceSafeAsync();
                            if (result?.Ticket?.Principal != null)
                            {
                                goto case ChallengeBehavior.Forbidden;
                            }
                            goto case ChallengeBehavior.Unauthorized;
                        case ChallengeBehavior.Unauthorized:
                            handled = await HandleUnauthorizedAsync(context);
                            Logger.AuthenticationSchemeChallenged(Options.AuthenticationScheme);
                            break;
                        case ChallengeBehavior.Forbidden:
                            handled = await HandleForbiddenAsync(context);
                            Logger.AuthenticationSchemeForbidden(Options.AuthenticationScheme);
                            break;
                    }
                    context.Accept();
                }
    
                if (!handled && PriorHandler != null)
                {
                    await PriorHandler.ChallengeAsync(context);
                }
            }

    以其中HandleForbiddenAsync为例,具体又如下:

    /// <summary>
            /// Override this method to deal with a challenge that is forbidden.
            /// </summary>
            /// <param name="context"></param>
            protected virtual Task<bool> HandleForbiddenAsync(ChallengeContext context)
            {
                Response.StatusCode = 403;
                return Task.FromResult(true);
            }

    这样,经由授权流程触发Challenge,Challenge返回相应验证结果到API调用方。

    注册完了认证及授权所需相关服务,接下来注册中间件,如下:

    app.UseCookieAuthentication(new CookieAuthenticationOptions
                {
                    AuthenticationScheme = "GuoKun",
                    AutomaticAuthenticate = true,
                    AutomaticChallenge = true,
                    DataProtectionProvider = DataProtectionProvider.Create(new DirectoryInfo(env.ContentRootPath))
                });

    app.UseMvc();

    注意UseCookieAuthentication要放在UseMvc前面。大家注意其中红色部分,这里为什么要自己手动创建DataProtectionProvider呢?因为这里是要做服务集群的,如果单机或单服务实例情况下,采用默认DataProtection机制就可以了。代码中手动指定目录创建,与默认实现的区别就是,默认实现会生成一个与当前机器及应用相关的key进行数据加解密,而手动指定目录创建provider,会在指定的目录下生成一个key的xml文件。这样,服务集群部署时候,加解密key一样,加解密得到的报文也是一致的。别问我怎么知道的,踩过坑,使劲儿调试,外加看官网文档,泪流满面。。。

    3、添加控制器模拟登陆及认证授权

    [Route("api/[controller]")]
        public class AccountController : Controller
        {
            [AllowAnonymous]
            [HttpPost("login")]
            public async Task Login([FromBody]User user)
            {
                IEnumerable<Claim> claims = new List<Claim>()
                    {
                        new Claim(ClaimTypes.Name, user.UID)
                    };
    
                await HttpContext.Authentication.SignInAsync("GuoKun",
                    new ClaimsPrincipal(new ClaimsIdentity(claims)));
            }
    
            [HttpGet("serverresponse")]
            public ContentResult ServerResponse()
            {
                return this.Content($"来自{((Microsoft.AspNetCore.Server.Kestrel.Internal.Http.ConnectionContext)this.HttpContext.Features).LocalEndPoint.ToString()}的响应:{this.User.Identity.Name ?? "匿名"},您好");
            }
        }

    因为授权现在是全局的,所以在登陆方法上用AllowAnonymous标记,跳过认证及授权。

    在ServerResponse方法中,返回当前服务实例绑定的IP及端口号。由于本Demo是采用ANCM寄宿在IIS中的,所以具体服务实例绑定的端口是动态的。

    4、部署。具体在IIS中的部署如下:

    三个站点的端口分别为9001,9002,9003,具体运行时,ANCM会将IIS的请求代理到KestrlServer。

    5、Nginx负载均衡配置:

    upstream guokun    {
            server localhost:9001;
            server localhost:9002;
            server localhost:9003;
        }
    
        server {
            listen       9000;
            server_name  localhost;
    
            #charset koi8-r;
    
            #access_log  logs/host.access.log  main;
    
            location / {
                root   html;
                index  index.html index.htm;
                proxy_pass http://guokun;
            }

    这个比较简单,不废话。

    6、运行效果:

    这里采用Postman模拟请求。当未调用登录API,直接请求api/Account/serverresponse时,如下:

    可以看到,直接401了,而且,响应标头中,有个Location,这个是challenge中默认实现的,告诉我们需要去登录认证,认证完了会跳转到当前请求资源url(在MVC中尤其有用)。

    接下来,登录:

    我们可以看到,登录成功,而且,服务端返回了加密及序列化后的凭证。接下来,我们再请求api/Account/serverresponse:

     

    看到没,请求成功。那么多请求几次,分别得到如下结果:

    可以看见,请求已经被负载到了不同的服务实例。

    有人会问,为什么不部署在多台不同服务器上啊,搞一台机器在那儿模拟。哥没那么多钱整那么多台机器啊,而且,装虚拟机,配置撑不了,望大神勿喷勿吐槽。

    如此,一个简易的基于asp.net core,带认证,具有集群负载的后端,便实现了。

    补充说明

    之前,由于网络原因,ClaimsIdentity部分没有下载源码,而是直接反编译的方式查看,导致得出ClaimsIdentity.IsAuthenticated总是返回false的结论,在此更正,并特别感谢Savorboard大神的特别指正。经过翻阅Github上源码,该属性是这样定义的:

    /// <summary>
            /// Gets a value that indicates if the user has been authenticated.
            /// </summary>
            public virtual bool IsAuthenticated
            {
                get { return !string.IsNullOrEmpty(_authenticationType); }
            }

    之前一直返回false,则是由于登录成功构建ClaimsIdentity时没有指定AuthenticationType。弄清楚了这个,那么对应授权策略的注册,就可以采用如下方式了:

     services.AddMvc(options =>
                {
                    //添加模型绑定过滤器
                    options.Filters.Add(typeof(ModelValidateActionFilter));
    
                    //添加授权过滤器,以便强制执行Authentication跳转及屏蔽逻辑
                    var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
                    //var policy = new AuthorizationPolicyBuilder().AddRequirements(new AuthenticationRequirement()).Build();
                    options.Filters.Add(new AuthorizeFilter(policy));
                });

    相应地,在登录成功后,构建ClaimsIdentity时指定其AuthenticationType:

    await HttpContext.Authentication.SignInAsync("GuoKun",
                    new ClaimsPrincipal(new ClaimsIdentity(claims, "GuoKun")));
  • 相关阅读:
    LeetCode 485. Max Consecutive Ones
    LeetCode 367. Valid Perfect Square
    LeetCode 375. Guess Number Higher or Lower II
    LeetCode 374. Guess Number Higher or Lower
    LeetCode Word Pattern II
    LeetCode Arranging Coins
    LeetCode 422. Valid Word Square
    Session 共享
    java NIO
    非阻塞IO
  • 原文地址:https://www.cnblogs.com/guokun/p/6266558.html
Copyright © 2011-2022 走看看