【.NET Core项目实战-统一认证平台】开篇及目录索引
上篇文章我们介绍了网关使用Redis进行缓存,并介绍了如何进行缓存实现,缓存信息清理接口的使用。本篇我们将介绍如何实现网关自定义客户端授权,实现可以为不同的接入客户端设置不同的访问权限。
.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、功能描述
网关重点功能之一鉴权,需要实现对不同的客户端进行授权访问,禁止访问未经授权的路由地址,且需要对无权访问的请求,返回通用的格式。
比如网关有1-10个可用路由,客户端A只能访问1-5,客户端B只能访问6-10,这时我们就无法通过Ocelot配置授权来进行自定义认证,这块就需要我们增加自定义的认证管道来实现功能,尽量不影响网关已有的功能。
下面我们就该功能如何实现展开讲解,希望大家先理解下功能需求,然后在延伸到具体实现。
二、数据库设计
我在第三章 网关篇-数据库存储配置(1)中讲解了我们网关配置信息设计,本篇将在那个基础上增加客户端认证需要用到的表的相关设计,设计客户端授权结构如下。其中客户端使用的IdentityServer4客户端表结构。
设计好概念模型后,我们生成物理模型,然后生成数据库脚本。
设计思想为可以添加自定义的授权组,为每一个授权分配能够访问的路由,然后为网关授权的客户端分配一个或多个授权组,每次客户端请求时,如果路由设置了授权访问,就校验客户端是否存在路由访问权限,如果无访问权限,直接返回401未授权提醒。
感觉是不是很简单呢?有了这个自定义的客户端认证,那么我们后端服务可以专注于自己的业务逻辑而无需再过多了进行权限处理了。
三、功能实现
1、功能开启配置
网关应该支持自定义客户端授权中间件是否启用,因为一些小型项目是不需要对每个客户端进行单独授权的,中型和大型项目才有可能遇到自定义配置情况,所以我们需要在配置文件增加配置选项。在AhphOcelotConfiguration.cs配置类中增加属性,默认不开启,而且需要知道客户端标识名称。
/// 
/// 金焰的世界
/// 2018-11-15
/// 是否启用客户端授权,默认不开启
/// 
public bool ClientAuthorization { get; set; } = false;
/// 
/// 金焰的世界
/// 2018-11-15
/// 客户端授权缓存时间,默认30分钟
/// 
public int ClientAuthorizationCacheTime { get; set; } = 1800;
/// 
/// 金焰的世界
/// 2018-11-15
/// 客户端标识,默认 client_id
/// 
public string ClientKey { get; set; } = "client_id";
那我们如何把自定义的授权增加到网关流程里呢?这块我们就需要订制自己的授权中间件。
2、实现客户端授权中间件
首先我们定义一个自定义授权中间件AhphAuthenticationMiddleware,需要继承OcelotMiddleware,然后我们要实现Invoke方法,详细代码如下。
using Ctr.AhphOcelot.Configuration;
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration;
using Ocelot.Logging;
using Ocelot.Middleware;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace Ctr.AhphOcelot.Authentication.Middleware
{
    /// 
    /// 金焰的世界
    /// 2018-11-15
    /// 自定义授权中间件
    /// 
    public class AhphAuthenticationMiddleware : OcelotMiddleware
    {
        private readonly OcelotRequestDelegate _next;
        private readonly AhphOcelotConfiguration _options;
        private readonly IAhphAuthenticationProcessor _ahphAuthenticationProcessor;
        public AhphAuthenticationMiddleware(OcelotRequestDelegate next,
            IOcelotLoggerFactory loggerFactory,
            IAhphAuthenticationProcessor ahphAuthenticationProcessor,
            AhphOcelotConfiguration options)
            : base(loggerFactory.CreateLogger
())
        {
            _next = next;
            _ahphAuthenticationProcessor = ahphAuthenticationProcessor;
            _options = options;
        }
        public async Task Invoke(DownstreamContext context)
        {
            if (!context.IsError && context.HttpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(context.DownstreamReRoute))
            {
                if (!_options.ClientAuthorization)
                {
                    Logger.LogInformation($"未启用客户端授权管道");
                    await _next.Invoke(context);
                }
                else
                {
                    Logger.LogInformation($"{context.HttpContext.Request.Path} 是认证路由. {MiddlewareName} 开始校验授权信息");
                    #region 提取客户端ID
                    var clientId = "client_cjy";
                    var path = context.DownstreamReRoute.UpstreamPathTemplate.OriginalValue; //路由地址
                    var clientClaim = context.HttpContext.User.Claims.FirstOrDefault(p => p.Type == _options.ClientKey);
                    if (!string.IsNullOrEmpty(clientClaim?.Value))
                    {//从Claims中提取客户端id
                        clientId = clientClaim?.Value;
                    }
                    #endregion
                    if (await _ahphAuthenticationProcessor.CheckClientAuthenticationAsync(clientId, path))
                    {
                        await _next.Invoke(context);
                    }
                    else
                    {//未授权直接返回错误
                        var errResult = new ErrorResult() { errcode=401, errmsg= "请求地址未授权" };
                        var message = errResult.ToJson();
                        context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
                        await context.HttpContext.Response.WriteAsync(message);
                        return;
                    }
                }
            }
            else
            {
                await _next.Invoke(context);
            }
        }
        private static bool IsAuthenticatedRoute(DownstreamReRoute reRoute)
        {
            return reRoute.IsAuthenticated;
        }
    }
}
有了这个中间件,那么如何添加到Ocelot的管道里呢?这里就需要查看Ocelot源代码了,看是如何实现管道调用的,OcelotMiddlewareExtensions实现管道部分如下,BuildOcelotPipeline里具体的流程。其实我在之前的Ocelot源码解读里也讲解过原理了,奈斯,既然找到了,那么我们就加入我们自定义的授权中间件即可。
public static async Task UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    var configuration = await CreateConfiguration(builder);
    ConfigureDiagnosticListener(builder);
    return CreateOcelotPipeline(builder, pipelineConfiguration);
}
private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices);
    pipelineBuilder.BuildOcelotPipeline(pipelineConfiguration);
    var firstDelegate = pipelineBuilder.Build();
    /*
            inject first delegate into first piece of asp.net middleware..maybe not like this
            then because we are updating the http context in ocelot it comes out correct for
            rest of asp.net..
            */
    builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware";
    builder.Use(async (context, task) =>
                {
                    var downstreamContext = new DownstreamContext(context);
                    await firstDelegate.Invoke(downstreamContext);
                });
    return builder;
}
添加使用自定义授权中间件扩展AhphAuthenticationMiddlewareExtensions,代码如下。
using Ocelot.Middleware.Pipeline;
using System;
using System.Collections.Generic;
using System.Text;
namespace Ctr.AhphOcelot.Authentication.Middleware
{
    /// 
    /// 金焰的世界
    /// 2018-11-15
    /// 使用自定义授权中间件
    /// 
    public static class AhphAuthenticationMiddlewareExtensions
    {
        public static IOcelotPipelineBuilder UseAhphAuthenticationMiddleware(this IOcelotPipelineBuilder builder)
        {
            return builder.UseMiddleware();
        }
    }
}
有了这个中间件扩展后,我们就在管道的合适地方加入我们自定义的中间件。我们添加我们自定义的管道扩展OcelotPipelineExtensions,然后把自定义授权中间件加入到认证之后。
using System;
using System.Threading.Tasks;
using Ctr.AhphOcelot.Authentication.Middleware;
using Ocelot.Authentication.Middleware;
using Ocelot.Authorisation.Middleware;
using Ocelot.Cache.Middleware;
using Ocelot.Claims.Middleware;
using Ocelot.DownstreamRouteFinder.Middleware;
using Ocelot.DownstreamUrlCreator.Middleware;
using Ocelot.Errors.Middleware;
using Ocelot.Headers.Middleware;
using Ocelot.LoadBalancer.Middleware;
using Ocelot.Middleware;
using Ocelot.Middleware.Pipeline;
using Ocelot.QueryStrings.Middleware;
using Ocelot.RateLimit.Middleware;
using Ocelot.Request.Middleware;
using Ocelot.Requester.Middleware;
using Ocelot.RequestId.Middleware;
using Ocelot.Responder.Middleware;
using Ocelot.WebSockets.Middleware;
namespace Ctr.AhphOcelot.Middleware
{
    /// 
    /// 金焰的世界
    /// 2018-11-15
    /// 网关管道扩展
    /// 
    public static class OcelotPipelineExtensions
    {
        public static OcelotRequestDelegate BuildAhphOcelotPipeline(this IOcelotPipelineBuilder builder,
            OcelotPipelineConfiguration pipelineConfiguration)
        {
            // This is registered to catch any global exceptions that are not handled
            // It also sets the Request Id if anything is set globally
            builder.UseExceptionHandlerMiddleware();
            // If the request is for websockets upgrade we fork into a different pipeline
            builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest,
                app =>
                {
                    app.UseDownstreamRouteFinderMiddleware();
                    app.UseDownstreamRequestInitialiser();
                    app.UseLoadBalancingMiddleware();
                    app.UseDownstreamUrlCreatorMiddleware();
                    app.UseWebSocketsProxyMiddleware();
                });
            // Allow the user to respond with absolutely anything they want.
            builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware);
            // This is registered first so it can catch any errors and issue an appropriate response
            builder.UseResponderMiddleware();
            // Then we get the downstream route information
            builder.UseDownstreamRouteFinderMiddleware();
            //Expand other branch pipes
            if (pipelineConfiguration.MapWhenOcelotPipeline != null)
            {
                foreach (var pipeline in pipelineConfiguration.MapWhenOcelotPipeline)
                {
                    builder.MapWhen(pipeline);
                }
            }
            // Now we have the ds route we can transform headers and stuff?
            builder.UseHttpHeadersTransformationMiddleware();
            // Initialises downstream request
            builder.UseDownstreamRequestInitialiser();
            // We check whether the request is ratelimit, and if there is no continue processing
            builder.UseRateLimiting();
            // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware)
            // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten
            // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware.
            builder.UseRequestIdMiddleware();
            // Allow pre authentication logic. The idea being people might want to run something custom before what is built in.
            builder.UseIfNotNull(pipelineConfiguration.PreAuthenticationMiddleware);
            // Now we know where the client is going to go we can authenticate them.
            // We allow the ocelot middleware to be overriden by whatever the
            // user wants
            if (pipelineConfiguration.AuthenticationMiddleware == null)
            {
                builder.UseAuthenticationMiddleware();
            }
            else
            {
                builder.Use(pipelineConfiguration.AuthenticationMiddleware);
            }
            //添加自定义授权中间 2018-11-15 金焰的世界
            builder.UseAhphAuthenticationMiddleware();
            // Allow pre authorisation logic. The idea being people might want to run something custom before what is built in.
            builder.UseIfNotNull(pipelineConfiguration.PreAuthorisationMiddleware);
            // Now we have authenticated and done any claims transformation we 
            // can authorise the request
            // We allow the ocelot middleware to be overriden by whatever the
            // user wants
            if (pipelineConfiguration.AuthorisationMiddleware == null)
            {
                builder.UseAuthorisationMiddleware();
            }
            else
            {
                builder.Use(pipelineConfiguration.AuthorisationMiddleware);
            }
            // Allow the user to implement their own query string manipulation logic
            builder.UseIfNotNull(pipelineConfiguration.PreQueryStringBuilderMiddleware);
            // Get the load balancer for this request
            builder.UseLoadBalancingMiddleware();
            // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used
            builder.UseDownstreamUrlCreatorMiddleware();
            // Not sure if this is the best place for this but we use the downstream url 
            // as the basis for our cache key.
            builder.UseOutputCacheMiddleware();
            //We fire off the request and set the response on the scoped data repo
            builder.UseHttpRequesterMiddleware();
            return builder.Build();
        }
        private static void UseIfNotNull(this IOcelotPipelineBuilder builder,
            Func, Task> middleware)
        {
            if (middleware != null)
            {
                builder.Use(middleware);
            }
        }
    }
}
有了这个自定义的管道扩展后,我们需要应用到网关启动里,修改我们创建管道的方法如下。
private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices);
    //pipelineBuilder.BuildOcelotPipeline(pipelineConfiguration);
    //使用自定义管道扩展 2018-11-15 金焰的世界
    pipelineBuilder.BuildAhphOcelotPipeline(pipelineConfiguration);
    var firstDelegate = pipelineBuilder.Build();
    /*
            inject first delegate into first piece of asp.net middleware..maybe not like this
            then because we are updating the http context in ocelot it comes out correct for
            rest of asp.net..
            */
    builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware";
    builder.Use(async