NET Core 多身份校驗與策略模式

来源:https://www.cnblogs.com/LaoPaoEr/p/18388246
-Advertisement-
Play Games

背景需求: 系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。 原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書 ...



背景需求:

  系統需要對接到XXX官方的API,但因此官方對接以及管理都十分嚴格。而本人部門的系統中包含諸多子系統,系統間為了穩定,程式間多數固定Token+特殊驗證進行調用,且後期還要提供給其他兄弟部門系統共同調用。

  原則上:每套系統都必須單獨接入到官方,但官方的接入複雜,還要官方指定機構認證的證書等各種條件,此做法成本較大。

so:

  為瞭解決對接的XXX官方API問題,我們搭建了一套中繼系統,顧名思義:就是一套用於請求中轉的中繼系統。在系統搭建的時,Leader提出要做多套鑒權方案,必須做到 動靜結合 身份鑒權。

  動靜結合:就是動態Token 和 靜態固定Token。

    動態Token:用於兄弟部門系統或對外訪問到此中繼系統申請的Token,供後期調用對應API。

    固定Token:用於當前部門中的諸多子系統,提供一個超級Token,此Token長期有效,且不會隨意更換。

入坑:

  因為剛來第一周我就接手了這個項目。項目處於申請賬號階段,即將進入開發。對接的是全英文文檔(申請/對接流程/開發API....),文檔複雜。當時我的感覺:OMG,這不得跑路?整個項目可謂難度之大。然後因為對內部業務也不熟悉,上手就看了微服務等相關係統代碼,註:每套系統之間文檔少的可憐,可以說系統無文檔狀態

  項目移交的時候,Leader之說讓我熟悉並逐漸進入開發,讓我請教同事。好嘛,請教了同事。同事也是接了前任離職的文檔而已,大家都不是很熟悉。於是同事讓我啟新的項目也是直接對接微服務形式開發,一頓操作猛如虎。

  項目開發第二周,已經打出框架模型並對接了部分API。此時,Leader開會問進度,結果來一句:此項目使用獨立API方式運行,部署到Docker,不接入公司的微服務架構。好嘛,幾天功夫白費了,真是取其糟粕去其精華~,恢覆成WebAPI。

技術實現:

  因為之前對身份認證鑒權這一塊沒有做太多的深入瞭解,Leader工期也在屁股追,就一句話:怎麼快怎麼來,先上後迭代。好嘛,為了項目方便,同時為了符合動靜結合的身份認證鑒權 。於是,我用了 JWT+自定義身份認證 實現了需求。

方案一:多身份認證+中間件模式實現

添加服務:Services.AddAuthentication 預設使用JWT

 //多重身份認證
//預設使用JWT,如果Controller使用 AuthenticationSchemes 則採用指定的身份認證
Services.AddAuthentication(options =>
{
    options.AddScheme<CustomAuthenticationHandler>(CustomAuthenticationHandler.AuthenticationSchemeName, CustomAuthenticationHandler.AuthenticationSchemeName);
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;//設置元數據地址或許可權是否需要HTTPs
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
    };
    options.Events = new CustomJwtBearerEvents();
});

自定義身份認證 CustomAuthenticationHandler.cs代碼

    public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public const string AuthenticationSchemeName = "CustomAuthenticationHandler";
        private readonly IConfiguration _configuration;
        public CustomAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IConfiguration configuration)
            : base(options, logger, encoder, clock)
        {
            _configuration = configuration;
        }
        /// <summary>
        /// 固定Token認證
        /// </summary>
        /// <returns></returns>
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            string isAnonymous = Request.Headers["IsAnonymous"].ToString();
            if (!string.IsNullOrEmpty(isAnonymous))
            {
                bool isAuthenticated = Convert.ToBoolean(isAnonymous);
                if (isAuthenticated)
                    return AuthenticateResult.NoResult();
            }

            string authorization = Request.Headers["Authorization"].ToString();
            // "Bearer " --> Bearer後面跟一個空格
            string token = authorization.StartsWith("Bearer ") ? authorization.Remove(0, "Bearer ".Length) : authorization;
            if (string.IsNullOrEmpty(token))
                return AuthenticateResult.Fail("請求頭Authorization不允許為空。");

            //通過密鑰,進行加密、解密對比認證
            if (!VerifyAuthorization(token))
                return AuthenticateResult.Fail("傳入的Authorization身份驗證失敗。");


            return AuthenticateResult.Success(GetTicket());
        }
        private AuthenticationTicket GetTicket()
        {
            // 驗證成功,創建身份驗證票據
            var claims = new[]
            {
                new Claim(ClaimTypes.Role, "Admin"),
                new Claim(ClaimTypes.Role, "Public"),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
            return ticket;
        }
        private bool VerifyAuthorization(string token)
        {
            //token: [0]隨機生成64位字元串,[1]載荷數據,[2]採用Hash對[0]+[1]的簽名
            var tokenArr = token.Split('.');
            if (tokenArr.Length != 3)
            {
                return false;
            }
            try
            {
                //1、先比對簽名串是否一致
                string signature = tokenArr[1].Hmacsha256HashEncrypt().ToLower();
                if (!signature.Equals(tokenArr[2].ToLower()))
                {
                    return false;
                }

                //解密
                var aecStr = tokenArr[1].Base64ToString();
                var clientId = aecStr.DecryptAES();
                //2、再驗證載荷數據的有效性
                var clientList = _configuration.GetSection("FixedClient").Get<List<FixedClientSet>>();
                var clientData = clientList.SingleOrDefault(it => it.ClientID.Equals(clientId));
                if (clientData == null)
                {
                    return false;
                }
            }
            catch (Exception)
            {
                throw;
            }

            return true;
        }
    }

使用中間件:UseMiddleware

app.UseAuthentication();
//中間件模式:自定義認證中間件:雙重認證選其一
//如果使用 策略,需要註釋掉 中間件
app.UseMiddleware<FallbackAuthenticationMiddleware>(); //使用中間件實現
app.UseAuthorization();

中間件FallbackAuthenticationMiddleware.cs代碼實現

   public class FallbackAuthenticationMiddleware
  {
      private readonly RequestDelegate _next;
      private readonly IAuthenticationSchemeProvider _schemeProvider;

      public FallbackAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemeProvider)
      {
          _next = next;
          _schemeProvider = schemeProvider;
      }
      /// <summary>
      /// 身份認證方案
      /// 預設JWT。JWT失敗,執行自定義認證
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      public async Task InvokeAsync(HttpContext context)
      {
          var endpoints = context.GetEndpoint();
          if (endpoints == null || !endpoints.Metadata.OfType<IAuthorizeData>().Any() || endpoints.Metadata.OfType<IAllowAnonymous>().Any())
          {
              await _next(context);
              return;
          }

          //預設JWT。JWT失敗,執行自定義認證
          var result = await Authenticate_JwtAsync(context);
          if (!result.Succeeded)
              result = await Authenticate_CustomTokenAsync(context);

          // 設置認證票據到HttpContext中 
          if (result.Succeeded)
              context.User = result.Principal;

          await _next(context);
      }
      /// <summary>
      /// JWT的認證
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      private async Task<dynamic> Authenticate_JwtAsync(HttpContext context)
      {
          var verify = context.User?.Identity?.IsAuthenticated ?? false;
          string authenticationType = context.User.Identity.AuthenticationType;
          if (verify && authenticationType != null)
          {
              return new { Succeeded = verify, Principal = context.User, Message = "" };
          }

          await Task.CompletedTask;

          // 找不到JWT身份驗證方案,或者無法獲取處理程式。
          return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "JWT authentication scheme not found or handler could not be obtained." };
      }

      /// <summary>
      /// 自定義認證
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      private async Task<dynamic> Authenticate_CustomTokenAsync(HttpContext context)
      {
          // 自定義認證方案的名稱
          var customScheme = "CustomAuthenticationHandler";

          var fixedTokenHandler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, customScheme);
          if (fixedTokenHandler != null)
          {
              var Res = await fixedTokenHandler.AuthenticateAsync();
              return new { Res.Succeeded, Res.Principal, Res.Failure?.Message };
          }

          //找不到CustomAuthenticationHandler身份驗證方案,或者無法獲取處理程式。
          return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "CustomAuthenticationHandler authentication scheme not found or handler could not be obtained." };

      }
  }

方案二:通過[Authorize]標簽的AuthenticationSchemes
因為中間件還要多維護一段中間件的代碼,顯得略微複雜,於是通過[Authorize(AuthenticationSchemes = "")]方式。

     //使用特定身份認證    
    //[Authorize(AuthenticationSchemes = CustomAuthenticationHandler.AuthenticationSchemeName)]
    //任一身份認證
    [Authorize(AuthenticationSchemes = $"{CustomAuthenticationHandler.AuthenticationSchemeName},{JwtBearerDefaults.AuthenticationScheme}")]
    public class DataProcessingController : ControllerBase
    {
    }

方案二:通過[Authorize]標簽的policy

  如果還有其他身份認證,那不斷增加AuthenticationSchemes拼接在Controller的頭頂,顯得不太好看,且要是多個Controller使用,也會導致維護麻煩,於是改用策略方式。

  在Program.cs添加服務AddAuthorization。使用策略的好處是增加易維護性。

 //授權策略
//Controller使用 policy 則採用指定的策略配置進行身份認證
builder.Services.AddAuthorization(option =>
{
    option.AddPolicy(CustomPolicy.Policy_A, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName, JwtBearerDefaults.AuthenticationScheme)
            );

    option.AddPolicy(CustomPolicy.Policy_B, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName)
            );

    option.AddPolicy(CustomPolicy.Policy_C, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
            );
});
     //使用特定策略身份認證
    [Authorize(policy:CustomPolicy.Policy_B)]
    public class DataProcessingController : ControllerBase
    {
    }
     /// <summary>
    /// 策略類
    /// </summary>
    public static class CustomPolicy
    {
        public const string Policy_A= "Policy_A";

        public const string Policy_B = "Policy_B";

        public const string Policy_C = "Policy_C";
    }

 最後附上截圖:

添加服務:

使用中間件:

控制器:

這樣,整套中繼系統就能完美的滿足Leader的需求,且達到預期效果。

源碼Demo:https://gitee.com/LaoPaoE/project-demo.git
最後附上:

AuthorizeAttribute 同時使用 Policy 和 AuthenticationSchemes 和 Roles 時是怎麼鑒權的流程:

  1. AuthenticationSchemes鑒權:
    • AuthenticationSchemes 屬性指定了用於驗證用戶身份的認證方案(如Cookies、Bearer Tokens等)。
    • ASP.NET Core會根據這些認證方案對用戶進行身份驗證。如果用戶未通過身份驗證(即未登錄或未提供有效的認證信息),則請求會被拒絕,並可能重定向到登錄頁面。
  2. Roles鑒權(如果指定了Roles):
    • 如果AuthorizeAttribute中還指定了 Roles 屬性,那麼除了通過身份驗證外,用戶還必須屬於這些角色之一。
    • ASP.NET Core會檢查用戶的角色信息,以確定用戶是否屬於 Roles  屬性中指定的一個或多個角色。
  3. Policy鑒權(如果指定了Policy):
    • Policy 屬性指定了一個或多個授權策略,這些策略定義了用戶必須滿足的額外條件才能訪問資源。
    • ASP.NET Core會調用相應的 IAuthorizationHandler 來評估用戶是否滿足該策略中的所有要求。這些要求可以基於角色、聲明(Claims)、資源等定義。
    • 如果用戶不滿足策略中的任何要求,則授權失敗,並返回一個HTTP 403 Forbidden響應。

鑒權順序和組合

  • 通常,AuthenticationSchemes的驗證會首先進行,因為這是訪問任何受保護資源的前提。
  • 如果AuthenticationSchemes驗證通過,接下來會根據是否指定了Roles和Policy來進一步進行鑒權。
  • Roles和Policy的鑒權順序可能因ASP.NET Core的具體版本和配置而異,但一般來說,它們會作為獨立的條件進行評估。
  • 用戶必須同時滿足AuthenticationSchemes、Roles(如果指定)和Policy(如果指定)中的所有條件,才能成功訪問受保護的資源。

註意事項

  • 在某些情況下,即使AuthenticationSchemes和Roles驗證都通過,但如果Policy中的要求未得到滿足,用戶仍然無法訪問資源。
  • 可以通過自定義 IAuthorizationRequirement 和 IAuthorizationHandler 來實現複雜的授權邏輯,以滿足特定的業務需求。
  • 確保在應用程式的身份驗證和授權配置中正確設置了AuthenticationSchemes、Roles和Policy,以便它們能夠協同工作,提供有效的訪問控制。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 帶箭頭的直線就是有方向的直線,既可以用來表示矢量,也可以用來標記某個關鍵位置。manim中提供了4種常用的帶箭頭的直線模塊: Arrow:單箭頭的直線 DoubleArrow:雙箭頭的直線 LabeledArrow:帶標簽的直線 Vector:向量 其中,DoubleArrow,LabeledArr ...
  • 六,Spring Boot 容器中 Lombok 插件的詳細使用,簡化配置,提高開發效率 @目錄六,Spring Boot 容器中 Lombok 插件的詳細使用,簡化配置,提高開發效率1. Lombok 介紹2. Lombok 常用註解2.1 @ToString2.2 @Setter2.3 @Dat ...
  • Springboot黑馬點評(3)——優惠券秒殺 【還剩Redisson的最後兩節沒測試 後續補上】 另外,後期單獨整理一份關於分散式鎖筆記 1 優惠券秒殺實現 1.1 用戶-優惠券訂單設計 1.1.1 全局ID生成器 使用資料庫自增ID作為訂單ID存在問題 1.1.2 考慮全局唯一ID生成邏輯 時 ...
  • 原文地址https://blog.fanscore.cn/a/61/ 1. wssh 1.1 開發背景 公司內部的發佈系統提供一個連接到k8s pod的web終端,可以在網頁中連接到k8s pod內。實現原理大概為通過websocket協議代理了k8s pod ssh,然後在前端通過xterm.js ...
  • 我們在某寶或某多多上搶購商品時,如果只是下了訂單但沒有進行實際的支付,那在訂單頁面會有一個支付倒計時,要是過了這個時間點那麼訂單便會自動取消。在這樣的業務場景中,一般情況下就會使用到延時隊列。 ...
  • 國內文章 【音視頻通話】使用asp.net core 8+vue3 實現高效音視頻通話 https://www.cnblogs.com/1996-Chinese-Chen/p/18384394 該文章描述了使用SRS實現音視頻通話和共用桌面的經驗。從最初使用nginx的RTMP到研究SRS和ZLMe ...
  • 前言 大家好,推薦一個.NET 8.0 為核心,結合前端 Vue 框架,實現了前後端完全分離的設計理念。它不僅提供了強大的基礎功能支持,如許可權管理、代碼生成器等,還通過採用主流技術和最佳實踐,顯著降低了開發難度,加快了項目交付速度。 如果你需要一個高效的開發解決方案,本框架能幫助大家輕鬆應對挑戰,實 ...
  • 本文介紹下電腦設備關機的情況下如何通過網路喚醒設備,之前電源S狀態 電腦Power電源狀態- 唐宋元明清2188 - 博客園 (cnblogs.com) 有介紹過遠程喚醒設備,後面這倆天瞭解多了點所以單獨加個隨筆 設備關機的情況下,使用網路喚醒的前提條件: 1. 被喚醒設備需要支持這WakeOnL ...
一周排行
    -Advertisement-
    Play Games
  • 前言 推薦一款基於.NET 8、WPF、Prism.DryIoc、MVVM設計模式、Blazor以及MySQL資料庫構建的企業級工作流系統的WPF客戶端框架-AIStudio.Wpf.AClient 6.0。 項目介紹 框架採用了 Prism 框架來實現 MVVM 模式,不僅簡化了 MVVM 的典型 ...
  • 先看一下效果吧: 我們直接通過改造一下原版的TreeView來實現上面這個效果 我們先創建一個普通的TreeView 代碼很簡單: <TreeView> <TreeViewItem Header="人事部"/> <TreeViewItem Header="技術部"> <TreeViewItem He ...
  • 1. 生成式 AI 簡介 https://imp.i384100.net/LXYmq3 2. Python 語言 https://imp.i384100.net/5gmXXo 3. 統計和 R https://youtu.be/ANMuuq502rE?si=hw9GT6JVzMhRvBbF 4. 數 ...
  • 本文為大家介紹下.NET解壓/壓縮zip文件。雖然解壓縮不是啥核心技術,但壓縮性能以及進度處理還是需要關註下,針對使用較多的zip開源組件驗證,給大家提供個技術選型參考 之前在《.NET WebSocket高併發通信阻塞問題 - 唐宋元明清2188 - 博客園 (cnblogs.com)》講過,團隊 ...
  • 之前寫過兩篇關於Roslyn源生成器生成源代碼的用例,今天使用Roslyn的代碼修複器CodeFixProvider實現一個cs文件頭部註釋的功能, 代碼修複器會同時涉及到CodeFixProvider和DiagnosticAnalyzer, 實現FileHeaderAnalyzer 首先我們知道修 ...
  • 在軟體行業,經常會聽到一句話“文不如表,表不如圖”說明瞭圖形在軟體應用中的重要性。同樣在WPF開發中,為了程式美觀或者業務需要,經常會用到各種個樣的圖形。今天以一些簡單的小例子,簡述WPF開發中幾何圖形(Geometry)相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 在 C# 中使用 RabbitMQ 通過簡訊發送重置後的密碼到用戶的手機號上,你可以按照以下步驟進行 1.安裝 RabbitMQ 客戶端庫 首先,確保你已經安裝了 RabbitMQ 客戶端庫。你可以通過 NuGet 包管理器來安裝: dotnet add package RabbitMQ.Clien ...
  • 1.下載 Protocol Buffers 編譯器(protoc) 前往 Protocol Buffers GitHub Releases 頁面。在 "Assets" 下找到適合您系統的壓縮文件,通常為 protoc-{version}-win32.zip 或 protoc-{version}-wi ...
  • 簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...