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 MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...