【.NET Core項目實戰-統一認證平臺】第六章 網關篇-自定義客戶端受權

【.NET Core項目實戰-統一認證平臺】開篇及目錄索引

上篇文章咱們介紹了網關使用Redis進行緩存,並介紹瞭如何進行緩存實現,緩存信息清理接口的使用。本篇咱們將介紹如何實現網關自定義客戶端受權,實現能夠爲不一樣的接入客戶端設置不一樣的訪問權限。html

.netcore項目實戰交流羣(637326624),有興趣的朋友能夠在羣裏交流討論。mysql

1、功能描述

網關重點功能之一鑑權,須要實現對不一樣的客戶端進行受權訪問,禁止訪問未經受權的路由地址,且須要對無權訪問的請求,返回通用的格式。
好比網關有1-10個可用路由,客戶端A只能訪問1-5,客戶端B只能訪問6-10,這時咱們就沒法經過Ocelot配置受權來進行自定義認證,這塊就須要咱們增長自定義的認證管道來實現功能,儘可能不影響網關已有的功能。web

下面咱們就該功能如何實現展開講解,但願你們先理解下功能需求,而後在延伸到具體實現。sql

2、數據庫設計

我在第三章 網關篇-數據庫存儲配置(1)中講解了咱們網關配置信息設計,本篇將在那個基礎上增長客戶端認證須要用到的表的相關設計,設計客戶端受權結構以下。其中客戶端使用的IdentityServer4客戶端表結構。
數據庫

設計好概念模型後,咱們生成物理模型,而後生成數據庫腳本。c#

設計思想爲能夠添加自定義的受權組,爲每個受權分配可以訪問的路由,而後爲網關受權的客戶端分配一個或多個受權組,每次客戶端請求時,若是路由設置了受權訪問,就校驗客戶端是否存在路由訪問權限,若是無訪問權限,直接返回401未受權提醒。後端

感受是否是很簡單呢?有了這個自定義的客戶端認證,那麼咱們後端服務能夠專一於本身的業務邏輯而無需再過多了進行權限處理了。緩存

3、功能實現

一、功能開啓配置websocket

網關應該支持自定義客戶端受權中間件是否啓用,由於一些小型項目是不須要對每一個客戶端進行單獨受權的,中型和大型項目纔有可能遇到自定義配置狀況,因此咱們須要在配置文件增長配置選項。在AhphOcelotConfiguration.cs配置類中增長屬性,默認不開啓,並且須要知道客戶端標識名稱。app

/// <summary>
/// 金焰的世界
/// 2018-11-15
/// 是否啓用客戶端受權,默認不開啓
/// </summary>
public bool ClientAuthorization { get; set; } = false;

/// <summary>
/// 金焰的世界
/// 2018-11-15
/// 客戶端受權緩存時間,默認30分鐘
/// </summary>
public int ClientAuthorizationCacheTime { get; set; } = 1800;
/// <summary>
/// 金焰的世界
/// 2018-11-15
/// 客戶端標識,默認 client_id
/// </summary>
public string ClientKey { get; set; } = "client_id";

那咱們如何把自定義的受權增長到網關流程裏呢?這塊咱們就須要訂製本身的受權中間件。

二、實現客戶端受權中間件

首先咱們定義一個自定義受權中間件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
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-15
    /// 自定義受權中間件
    /// </summary>
    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<AhphAuthenticationMiddleware>())
        {
            _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<IApplicationBuilder> 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
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-15
    /// 使用自定義受權中間件
    /// </summary>
    public static class AhphAuthenticationMiddlewareExtensions
    {
        public static IOcelotPipelineBuilder UseAhphAuthenticationMiddleware(this IOcelotPipelineBuilder builder)
        {
            return builder.UseMiddleware<AhphAuthenticationMiddleware>();
        }
    }
}

有了這個中間件擴展後,咱們就在管道的合適地方加入咱們自定義的中間件。咱們添加咱們自定義的管道擴展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
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-15
    /// 網關管道擴展
    /// </summary>
    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<DownstreamContext, Func<Task>, 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 (context, task) =>
                {
                    var downstreamContext = new DownstreamContext(context);
                    await firstDelegate.Invoke(downstreamContext);
                });

    return builder;
}

如今咱們完成了網關的擴展和應用,可是是否注意到了,咱們的網關接口還未實現呢?什麼接口呢?

IAhphAuthenticationProcessor這個接口雖然定義了,可是一直未實現,如今開始咱們要實現下這個接口,咱們回看下咱們使用這個接口的什麼方法,就是檢查客戶端是否有訪問路由的權限。

三、結合數據庫實現校驗及緩存

每次請求都須要校驗客戶端是否受權,若是不緩存此熱點數據,那麼對網關開銷很大,因此咱們須要增長緩存。

新建AhphAuthenticationProcessor類來實現認證接口,代碼以下。

using Ctr.AhphOcelot.Configuration;
using Ocelot.Cache;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace Ctr.AhphOcelot.Authentication
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-15
    /// 實現自定義受權處理器邏輯
    /// </summary>
    public class AhphAuthenticationProcessor : IAhphAuthenticationProcessor
    {
        private readonly IClientAuthenticationRepository _clientAuthenticationRepository;
        private readonly AhphOcelotConfiguration _options;
        private readonly IOcelotCache<ClientRoleModel> _ocelotCache;
        public AhphAuthenticationProcessor(IClientAuthenticationRepository clientAuthenticationRepository, AhphOcelotConfiguration options, IOcelotCache<ClientRoleModel> ocelotCache)
        {
            _clientAuthenticationRepository = clientAuthenticationRepository;
            _options = options;
            _ocelotCache = ocelotCache;
        }
        /// <summary>
        /// 校驗當前的請求地址客戶端是否有權限訪問
        /// </summary>
        /// <param name="clientid">客戶端ID</param>
        /// <param name="path">請求地址</param>
        /// <returns></returns>
        public async Task<bool> CheckClientAuthenticationAsync(string clientid, string path)
        {
            var enablePrefix = _options.RedisKeyPrefix + "ClientAuthentication";
            var key = AhphOcelotHelper.ComputeCounterKey(enablePrefix, clientid, "", path);
            var cacheResult = _ocelotCache.Get(key, enablePrefix);
            if (cacheResult!=null)
            {//提取緩存數據
                return cacheResult.Role;
            }
            else
            {//從新獲取認證信息
                var result = await _clientAuthenticationRepository.ClientAuthenticationAsync(clientid, path);
                  //添加到緩存裏
                  _ocelotCache.Add(key, new ClientRoleModel() { CacheTime = DateTime.Now,Role=result }, TimeSpan.FromMinutes(_options.ClientAuthorizationCacheTime), enablePrefix);
                return result;
            }
        }
    }
}

代碼很簡單,就是從緩存中查找看是否有數據,若是存在直接返回,若是不存在,就從倉儲中提取訪問權限,而後寫入緩存,寫入緩存的時間可由配置文件寫入,默認爲30分鐘,可自行根據業務須要修改。

如今咱們還須要解決2個問題,這個中間件才能正常運行,第一IClientAuthenticationRepository接口未實現和注入;第二IOcelotCache<ClientRoleModel>未注入,那咱們接下來實現這兩塊,而後就能夠測試咱們第一個中間件啦。

新建SqlServerClientAuthenticationRepository類,來實現IClientAuthenticationRepository接口,實現代碼以下。

using Ctr.AhphOcelot.Authentication;
using Ctr.AhphOcelot.Configuration;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Text;
using System.Threading.Tasks;
using Dapper;
namespace Ctr.AhphOcelot.DataBase.SqlServer
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-16
    /// 使用sqlserver實現客戶端受權倉儲
    /// </summary>
    public class SqlServerClientAuthenticationRepository : IClientAuthenticationRepository
    {
        private readonly AhphOcelotConfiguration _option;
        public SqlServerClientAuthenticationRepository(AhphOcelotConfiguration option)
        {
            _option = option;
        }
        /// <summary>
        /// 校驗獲取客戶端是否有訪問權限
        /// </summary>
        /// <param name="clientid">客戶端ID</param>
        /// <param name="path">請求路由</param>
        /// <returns></returns>
        public async Task<bool> ClientAuthenticationAsync(string clientid, string path)
        {
            using (var connection = new SqlConnection(_option.DbConnectionStrings))
            {
                string sql = @"SELECT COUNT(1) FROM  AhphClients T1 INNER JOIN AhphClientGroup T2 ON T1.Id=T2.Id INNER JOIN AhphAuthGroup T3 ON T2.GroupId = T3.GroupId INNER JOIN AhphReRouteGroupAuth T4 ON T3.GroupId = T4.GroupId INNER JOIN AhphReRoute T5 ON T4.ReRouteId = T5.ReRouteId WHERE Enabled = 1 AND ClientId = @ClientId AND T5.InfoStatus = 1 AND UpstreamPathTemplate = @Path";
                var result= await connection.QueryFirstOrDefaultAsync<int>(sql, new { ClientId = clientid, Path = path });
                return result > 0;
            }
        }
    }
}

如今須要注入下實現,這塊應該都知道在哪裏加入了吧?沒錯ServiceCollectionExtensions擴展又用到啦,如今梳理下流程感受是否是很清晰呢?

builder.Services.AddSingleton<IClientAuthenticationRepository, SqlServerClientAuthenticationRepository>();

builder.Services.AddSingleton<IAhphAuthenticationProcessor, AhphAuthenticationProcessor>();

再添加緩存的注入實現,到此咱們的第一個中間件所有添加完畢了,如今能夠開始測試咱們的中間件啦。

builder.Services.AddSingleton<IOcelotCache<ClientRoleModel>, InRedisCache<ClientRoleModel>>();

四、測試受權中間件

咱們先在數據庫插入客戶端受權腳本,腳本以下。

--插入測試客戶端
INSERT INTO AhphClients(ClientId,ClientName) VALUES('client1','測試客戶端1')
INSERT INTO AhphClients(ClientId,ClientName) VALUES('client2','測試客戶端2')
--插入測試受權組
INSERT INTO AhphAuthGroup VALUES('受權組1','只能訪問/cjy/values路由',1);
INSERT INTO AhphAuthGroup VALUES('受權組2','能訪問全部路由',1);

--插入測試組權限
INSERT INTO AhphReRouteGroupAuth VALUES(1,1);

INSERT INTO AhphReRouteGroupAuth VALUES(2,1);
INSERT INTO AhphReRouteGroupAuth VALUES(2,2);

--插入客戶端受權
INSERT INTO AhphClientGroup VALUES(1,1);
INSERT INTO AhphClientGroup VALUES(2,2);

--設置測試路由只有受權才能訪問
UPDATE AhphReRoute SET AuthenticationOptions='{"AuthenticationProviderKey": "TestKey"}' WHERE ReRouteId IN(1,2);

這塊設置了客戶端2能夠訪問路由/cjy/values,客戶端1能夠訪問路由/cjy/values 和 /ctr/values/{id},開始使用PostMan來測試這個中間件看是否跟我設置的一毛同樣,各類dotnet run啓動吧。啓動前別忘了在咱們網關配置文件裏,設置啓動客戶端受權 option.ClientAuthorization = true;,是否是很簡單呢?

爲了測試受權效果,咱們須要把網關項目增長認證,詳細看代碼,裏面就是定義了受權認證,啓動咱們默認的認證地址。

var authenticationProviderKey = "TestKey";
Action<IdentityServerAuthenticationOptions> gatewayoptions = o =>
{
o.Authority = "http://localhost:6611";
o.ApiName = "gateway";
o.RequireHttpsMetadata = false;
};

services.AddAuthentication()
.AddIdentityServerAuthentication(authenticationProviderKey, gatewayoptions);

測試結果以下,達到咱們預期目的。



終於完成了咱們的自定義客戶端受權啦,此處應該掌聲不斷。


五、增長mysql支持

看過我前面的文章應該知道,支持mysql太簡單啦,直接重寫IClientAuthenticationRepository實現,而後注入到UseMySql裏,問題就解決啦。感受是否是難以想象,這就是.netcore的魅力,簡單到我感受到我再貼代碼就是侮辱智商同樣。

六、重構認證失敗輸出,保持與Ocelot一致風格

前面咱們定義了未受權使用自定義的ClientRoleModel輸出,最後發現這樣太不優雅啦,咱們須要簡單重構下,來保持與Ocelot默認管道一致風格,修改代碼以下。

//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;
var error = new UnauthenticatedError($"請求認證路由 {context.HttpContext.Request.Path}客戶端未受權");
Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 自定義認證管道校驗失敗. {error}");
SetPipelineError(context, error);

再測試下未受權,返回狀態爲401,強迫症患者表示舒服多了。

4、總結及預告

本篇咱們講解的是網關如何實現自定義客戶端受權功能,從設計到實現一步一步詳細講解,雖然只用一篇就寫完了,可是涉及的知識點仍是很是多的,但願你們認真理解實現的思想,看我是如何從規劃到實現的,爲了更好的幫助你們理解,從本篇開始,個人源代碼都是一個星期之後再開源,你們能夠根據博客內容本身手動實現下,有利於消化,若是在操做中遇到什麼問題,能夠加.NET Core項目實戰交流羣(QQ羣號:637326624)諮詢做者。

下一篇開始講解自定義客戶端限流,在學習下篇前能夠本身先了解下限流相關內容,而後本身試着實現看看,帶着問題學習可能事半功倍哦。

相關文章
相關標籤/搜索