翼度科技»论坛 编程开发 .net 查看内容

NET Core 多身份校验与策略模式

4

主题

4

帖子

12

积分

新手上路

Rank: 1

积分
12
背景需求:
  系统需要对接到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
  1.  //多重身份认证
  2. //默认使用JWT,如果Controller使用 AuthenticationSchemes 则采用指定的身份认证
  3. Services.AddAuthentication(options =>
  4. {
  5.     options.AddScheme<CustomAuthenticationHandler>(CustomAuthenticationHandler.AuthenticationSchemeName, CustomAuthenticationHandler.AuthenticationSchemeName);
  6.     options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  7.     options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  8. })
  9. .AddJwtBearer(options =>
  10. {
  11.     options.RequireHttpsMetadata = false;//设置元数据地址或权限是否需要HTTPs
  12.     options.SaveToken = true;
  13.     options.TokenValidationParameters = new TokenValidationParameters
  14.     {
  15.         ValidateIssuer = true,
  16.         ValidateAudience = true,
  17.         ValidateLifetime = true,
  18.         ValidateIssuerSigningKey = true,
  19.         ValidIssuer = builder.Configuration["Jwt:Issuer"],
  20.         ValidAudience = builder.Configuration["Jwt:Audience"],
  21.         IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
  22.     };
  23.     options.Events = new CustomJwtBearerEvents();
  24. });
复制代码
自定义身份认证 CustomAuthenticationHandler.cs代码
  1.     public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
  2.     {
  3.         public const string AuthenticationSchemeName = "CustomAuthenticationHandler";
  4.         private readonly IConfiguration _configuration;
  5.         public CustomAuthenticationHandler(
  6.             IOptionsMonitor<AuthenticationSchemeOptions> options,
  7.             ILoggerFactory logger,
  8.             UrlEncoder encoder,
  9.             ISystemClock clock,
  10.             IConfiguration configuration)
  11.             : base(options, logger, encoder, clock)
  12.         {
  13.             _configuration = configuration;
  14.         }
  15.         /// <summary>
  16.         /// 固定Token认证
  17.         /// </summary>
  18.         /// <returns></returns>
  19.         protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
  20.         {
  21.             string isAnonymous = Request.Headers["IsAnonymous"].ToString();
  22.             if (!string.IsNullOrEmpty(isAnonymous))
  23.             {
  24.                 bool isAuthenticated = Convert.ToBoolean(isAnonymous);
  25.                 if (isAuthenticated)
  26.                     return AuthenticateResult.NoResult();
  27.             }
  28.             string authorization = Request.Headers["Authorization"].ToString();
  29.             // "Bearer " --> Bearer后面跟一个空格
  30.             string token = authorization.StartsWith("Bearer ") ? authorization.Remove(0, "Bearer ".Length) : authorization;
  31.             if (string.IsNullOrEmpty(token))
  32.                 return AuthenticateResult.Fail("请求头Authorization不允许为空。");
  33.             //通过密钥,进行加密、解密对比认证
  34.             if (!VerifyAuthorization(token))
  35.                 return AuthenticateResult.Fail("传入的Authorization身份验证失败。");
  36.             return AuthenticateResult.Success(GetTicket());
  37.         }
  38.         private AuthenticationTicket GetTicket()
  39.         {
  40.             // 验证成功,创建身份验证票据
  41.             var claims = new[]
  42.             {
  43.                 new Claim(ClaimTypes.Role, "Admin"),
  44.                 new Claim(ClaimTypes.Role, "Public"),
  45.             };
  46.             var identity = new ClaimsIdentity(claims, Scheme.Name);
  47.             var principal = new ClaimsPrincipal(identity);
  48.             var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
  49.             return ticket;
  50.         }
  51.         private bool VerifyAuthorization(string token)
  52.         {
  53.             //token: [0]随机生成64位字符串,[1]载荷数据,[2]采用Hash对[0]+[1]的签名
  54.             var tokenArr = token.Split('.');
  55.             if (tokenArr.Length != 3)
  56.             {
  57.                 return false;
  58.             }
  59.             try
  60.             {
  61.                 //1、先比对签名串是否一致
  62.                 string signature = tokenArr[1].Hmacsha256HashEncrypt().ToLower();
  63.                 if (!signature.Equals(tokenArr[2].ToLower()))
  64.                 {
  65.                     return false;
  66.                 }
  67.                 //解密
  68.                 var aecStr = tokenArr[1].Base64ToString();
  69.                 var clientId = aecStr.DecryptAES();
  70.                 //2、再验证载荷数据的有效性
  71.                 var clientList = _configuration.GetSection("FixedClient").Get<List<FixedClientSet>>();
  72.                 var clientData = clientList.SingleOrDefault(it => it.ClientID.Equals(clientId));
  73.                 if (clientData == null)
  74.                 {
  75.                     return false;
  76.                 }
  77.             }
  78.             catch (Exception)
  79.             {
  80.                 throw;
  81.             }
  82.             return true;
  83.         }
  84.     }
复制代码
使用中间件:UseMiddleware
  1. app.UseAuthentication();
  2. //中间件模式:自定义认证中间件:双重认证选其一
  3. //如果使用 策略,需要注释掉 中间件
  4. app.UseMiddleware<FallbackAuthenticationMiddleware>(); //使用中间件实现
  5. app.UseAuthorization();
复制代码
中间件FallbackAuthenticationMiddleware.cs代码实现
  1.    public class FallbackAuthenticationMiddleware
  2.   {
  3.       private readonly RequestDelegate _next;
  4.       private readonly IAuthenticationSchemeProvider _schemeProvider;
  5.       public FallbackAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemeProvider)
  6.       {
  7.           _next = next;
  8.           _schemeProvider = schemeProvider;
  9.       }
  10.       /// <summary>
  11.       /// 身份认证方案
  12.       /// 默认JWT。JWT失败,执行自定义认证
  13.       /// </summary>
  14.       /// <param name="context"></param>
  15.       /// <returns></returns>
  16.       public async Task InvokeAsync(HttpContext context)
  17.       {
  18.           var endpoints = context.GetEndpoint();
  19.           if (endpoints == null || !endpoints.Metadata.OfType<IAuthorizeData>().Any() || endpoints.Metadata.OfType<IAllowAnonymous>().Any())
  20.           {
  21.               await _next(context);
  22.               return;
  23.           }
  24.           //默认JWT。JWT失败,执行自定义认证
  25.           var result = await Authenticate_JwtAsync(context);
  26.           if (!result.Succeeded)
  27.               result = await Authenticate_CustomTokenAsync(context);
  28.           // 设置认证票据到HttpContext中
  29.           if (result.Succeeded)
  30.               context.User = result.Principal;
  31.           await _next(context);
  32.       }
  33.       /// <summary>
  34.       /// JWT的认证
  35.       /// </summary>
  36.       /// <param name="context"></param>
  37.       /// <returns></returns>
  38.       private async Task<dynamic> Authenticate_JwtAsync(HttpContext context)
  39.       {
  40.           var verify = context.User?.Identity?.IsAuthenticated ?? false;
  41.           string authenticationType = context.User.Identity.AuthenticationType;
  42.           if (verify && authenticationType != null)
  43.           {
  44.               return new { Succeeded = verify, Principal = context.User, Message = "" };
  45.           }
  46.           await Task.CompletedTask;
  47.           // 找不到JWT身份验证方案,或者无法获取处理程序。
  48.           return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "JWT authentication scheme not found or handler could not be obtained." };
  49.       }
  50.       /// <summary>
  51.       /// 自定义认证
  52.       /// </summary>
  53.       /// <param name="context"></param>
  54.       /// <returns></returns>
  55.       private async Task<dynamic> Authenticate_CustomTokenAsync(HttpContext context)
  56.       {
  57.           // 自定义认证方案的名称
  58.           var customScheme = "CustomAuthenticationHandler";
  59.           var fixedTokenHandler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, customScheme);
  60.           if (fixedTokenHandler != null)
  61.           {
  62.               var Res = await fixedTokenHandler.AuthenticateAsync();
  63.               return new { Res.Succeeded, Res.Principal, Res.Failure?.Message };
  64.           }
  65.           //找不到CustomAuthenticationHandler身份验证方案,或者无法获取处理程序。
  66.           return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "CustomAuthenticationHandler authentication scheme not found or handler could not be obtained." };
  67.       }
  68.   }
复制代码
方案二:通过[Authorize]标签的AuthenticationSchemes
因为中间件还要多维护一段中间件的代码,显得略微复杂,于是通过[Authorize(AuthenticationSchemes = "")]方式。
  1.      //使用特定身份认证   
  2.     //[Authorize(AuthenticationSchemes = CustomAuthenticationHandler.AuthenticationSchemeName)]
  3.     //任一身份认证
  4.     [Authorize(AuthenticationSchemes = $"{CustomAuthenticationHandler.AuthenticationSchemeName},{JwtBearerDefaults.AuthenticationScheme}")]
  5.     public class DataProcessingController : ControllerBase
  6.     {
  7.     }
复制代码
方案二:通过[Authorize]标签的policy
  如果还有其他身份认证,那不断增加AuthenticationSchemes拼接在Controller的头顶,显得不太好看,且要是多个Controller使用,也会导致维护麻烦,于是改用策略方式。
  在Program.cs添加服务AddAuthorization。使用策略的好处是增加易维护性。
  1.  //授权策略
  2. //Controller使用 policy 则采用指定的策略配置进行身份认证
  3. builder.Services.AddAuthorization(option =>
  4. {
  5.     option.AddPolicy(CustomPolicy.Policy_A, policy => policy
  6.             .RequireAuthenticatedUser()
  7.             .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName, JwtBearerDefaults.AuthenticationScheme)
  8.             );
  9.     option.AddPolicy(CustomPolicy.Policy_B, policy => policy
  10.             .RequireAuthenticatedUser()
  11.             .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName)
  12.             );
  13.     option.AddPolicy(CustomPolicy.Policy_C, policy => policy
  14.             .RequireAuthenticatedUser()
  15.             .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
  16.             );
  17. });
复制代码
  1.      //使用特定策略身份认证
  2.     [Authorize(policy:CustomPolicy.Policy_B)]
  3.     public class DataProcessingController : ControllerBase
  4.     {
  5.     }
复制代码
  1.      /// <summary>
  2.     /// 策略类
  3.     /// </summary>
  4.     public static class CustomPolicy
  5.     {
  6.         public const string Policy_A= "Policy_A";
  7.         public const string Policy_B = "Policy_B";
  8.         public const string Policy_C = "Policy_C";
  9.     }
复制代码
 最后附上截图:
添加服务:

使用中间件:

控制器:

这样,整套中继系统就能完美的满足Leader的需求,且达到预期效果。
源码Demo:https://gitee.com/LaoPaoE/project-demo.git
最后附上:
AuthorizeAttribute 同时使用 Policy 和 AuthenticationSchemes 和 Roles 时是怎么鉴权的流程:

  • AuthenticationSchemes鉴权:

    • AuthenticationSchemes 属性指定了用于验证用户身份的认证方案(如Cookies、Bearer Tokens等)。
    • ASP.NET Core会根据这些认证方案对用户进行身份验证。如果用户未通过身份验证(即未登录或未提供有效的认证信息),则请求会被拒绝,并可能重定向到登录页面。

  • Roles鉴权(如果指定了Roles):

    • 如果AuthorizeAttribute中还指定了 Roles 属性,那么除了通过身份验证外,用户还必须属于这些角色之一。
    • ASP.NET Core会检查用户的角色信息,以确定用户是否属于 Roles  属性中指定的一个或多个角色。

  • 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,以便它们能够协同工作,提供有效的访问控制。

来源:https://www.cnblogs.com/LaoPaoEr/p/18388246
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具