NET Core 多身份校验与策略模式
背景需求:
系统需要对接到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 时是怎么鉴权的流程:
- 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,以便它们能够协同工作,提供有效的访问控制。
NET Core 多身份校验与策略模式的更多相关文章
- asp.net core 集成JWT(二)token的强制失效,基于策略模式细化api权限
[前言] 上一篇我们介绍了什么是JWT,以及如何在asp.net core api项目中集成JWT权限认证.传送门:https://www.cnblogs.com/7tiny/p/11012035.h ...
- 使用JavaScript策略模式校验表单
表单校验 Web项目中,登录,注册等等功能都需要表单提交,当把用户的数据提交给后台之前,前端一般要做一些力所能及的校验,比如是否填写,填写的长度,密码是否符合规范等等,前端校验可以避免提交不合规范的表 ...
- ASP.NET Core 使用 JWT 自定义角色/策略授权需要实现的接口
目录 ① 存储角色/用户所能访问的 API ② 实现 IAuthorizationRequirement 接口 ③ 实现 TokenValidationParameters ④ 生成 Token ⑤ ...
- ASP.NET Core的身份认证框架IdentityServer4--入门
ASP.NET Core的身份认证框架IdentityServer4--入门 2018年08月11日 10:09:00 qq_42606051 阅读数 4002 https://blog.csdn ...
- Javascript设计模式学习三(策略模式)
定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换.目的:将算法的使用和算法的实现分离开来.比如: if(input == 'A'){ return 1; } if(input == ...
- javascript设计模式实践之策略模式--输入验证
策略模式中的策略就是一种算法或者业务规则,将这些策略作为函数进行封装,并向外提供统一的调用执行. 先定义一个简单的输入表单: <!DOCTYPE html> <html> &l ...
- 大熊君说说JS与设计模式之------策略模式Strategy
一,总体概要 1,笔者浅谈 策略模式,又叫算法簇模式,就是定义了不同的算法,并且之间可以互相替换,此模式让算法的变化独立于使用算法的客户. 策略模式和工厂模式有一定的类似,策略模式相对简单容易理解,并 ...
- javascript设计模式学习之五——策略模式
一.策略模式定义: 定义一些列的算法/规则,将它们封装起来,使得它们可以互相替换/组合使用.其目的在于将算法/规则封装起来,将算法/规则的使用与实现分离出来. 通过策略模式,可以减少算法计算过程中大量 ...
- JS常用的设计模式(9)——策略模式
策略模式的意义是定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换.一个小例子就能让我们一目了然. 回忆下jquery里的animate方法. $( div ).animate( {&quo ...
- 《JavaScript设计模式与开发实践》读书笔记之策略模式
1.策略模式 定义一系列算法,把它们一个个封装起来,并且使它们可以相互替换 1.1 传统实现 根据工资基数和年底绩效来发送年终奖 var calculateBonus= function (perfo ...
随机推荐
- windows server + iis 部署若伊前端vue项目
一.背景说明 工作原因,一直使用若伊前后端分离版框架进行二次开发.客户的服务器多数为windows server系统,少部分为linux系统.过去一直是使用nginx进行前端的部署,nginx的代理功 ...
- CentOS7离线安装devtoolset-9并编译redis6.0.5
首先参照https://www.cnblogs.com/wdw984/p/13330074.html,来进行如何安装Centos和离线下载rpm包. 离线下载jemalloc,上传到CentOS的/d ...
- Mac Mysql初始化密码
初始化密码 step1 苹果->系统偏好设置->最下面一行上点击mysql图标, 在弹出页面中 关闭mysql服务(点击stop mysql server) step2 登录终端:comm ...
- 如何做好一场NPS调研?
我们在工作中经常遇到的一个词,那就是"产品NPS调研".当部分项目缺少专业的用研人员时,设计师.产品经理则经常会接受上级的要求,投身于NPS调研工作. 笔者也曾在2022年的某天突 ...
- oeasy教您玩转vim - 69 - # 折叠folding入门
折叠入门 回忆上次 上次学习了一种新的容器 tabs选项卡 tabs选项卡 包含多个选项卡tab 可以列两个tab 一个编写文件 一个执行指令 互不影响 每个 tab选项卡 还可以对应多个wind ...
- UE5 打包DedicatedServer
UE5开发Dedicate Server直接按教程用Replicated那种蓝图开发即可. 如果打包的话,服务器端需要无界面的运行模式,不同于正常的开发,所以为了打包,这里步骤如下: 1.到githu ...
- 关于异步编程中的bind(this)
异步编程中的.bind(this)方法解决了异步执行后this指针指向全局函数的问题,主要可以通过以下两个场景加以说明:(本文所用例子基于React场景:为简便起见,仅在第一个例子中展示完整HTML代 ...
- java web 开发框架编
学习web 框架上开发需要的是安装 mysql 8.0 idea 2022 git 2.2.23 node 16以上 (新版本不好拉有些库了)jdk 最好是17以上 jdk8也是行的,反正不管 ...
- 为什么我@Value中明明显示了值,他却是null
今天尝试把一些重要东西写入application.yml里,结果在使用的时候发现value取不出来值原因有2个: 1.没有写@compent,没有把这个类交给spring管理 2.在service层n ...
- Python将本地文件上传到服务器
1.首先本地有一个文件"E:\Double\python\dataCheck\html_detail\20221206140345_activeBug.html",我需要上传到服务 ...