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

无昵称老炮儿發表於2024-08-30


背景需求:

  系統需要對接到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";
    }

最後附上截圖:

新增服務:

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

使用中介軟體:

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

控制器:

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

這樣,整套中繼系統就能完美的滿足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,以便它們能夠協同工作,提供有效的訪問控制。

相關文章