校园兼职网站开发用例图,网站设计 开发人员,主流网站开发技术框架,网站的流量检测怎么做ASP.NET Core的认证与授权已经不是什么新鲜事了#xff0c;微软官方的文档对于如何在ASP.NET Core中实现认证与授权有着非常详细深入的介绍。但有时候在开发过程中#xff0c;我们也往往会感觉无从下手#xff0c;或者由于一开始没有进行认证授权机制的设计与规划#xff0… ASP.NET Core的认证与授权已经不是什么新鲜事了微软官方的文档对于如何在ASP.NET Core中实现认证与授权有着非常详细深入的介绍。但有时候在开发过程中我们也往往会感觉无从下手或者由于一开始没有进行认证授权机制的设计与规划使得后期出现一些混乱的情况。这里我就尝试结合一个实际的例子从0到1来介绍ASP.NET Core中如何实现自己的认证与授权机制。当我们使用Visual Studio自带的ASP.NET Core Web API项目模板新建一个项目的时候Visual Studio会问我们是否需要启用认证机制如果你选择了启用那么Visual Studio会在项目创建的时候加入一些辅助依赖和一些辅助类比如加入对Entity Framework以及ASP.NET Identity的依赖以帮助你实现基于Entity Framework和ASP.NET Identity的身份认证。如果你还没有了解过ASP.NET Core的认证与授权的一些基础内容那么当你打开这个由Visual Studio自动创建的项目的时候肯定会一头雾水不知从何开始你甚至会怀疑自动创建的项目中真的是所有的类或者方法都是必须的吗所以为了让本文更加简单易懂我们还是选择不启用身份认证直接创建一个最简单的ASP.NET Core Web API应用程序以便后续的介绍。新建一个ASP.NET Core Web API应用程序这里我是在Linux下使用JetBrains Rider新建的项目也可以使用标准的Visual Studio或者VSCode来创建项目。创建完成后运行程序然后使用浏览器访问/WeatherForecast端点就可以获得一组随机生成的天气及温度数据的数组。你也可以使用下面的curl命令来访问这个API1curl -X GET http://localhost:5000/WeatherForecast -H accept: text/plain现在让我们在WeatherForecastController的Get方法上设置一个断点重新启动程序仍然发送上述请求以命中断点此时我们比较关心User对象的状态打开监视器查看User对象的属性发现它的IsAuthenticated属性为false在很多情况下我们可能并不需要在Controller的方法中获取认证用户的信息因此也从来不会关注User对象是否真的处于已被认证的状态。但是当API需要根据用户的某些信息来执行一些特殊逻辑时我们就需要在这里让User的认证信息处于一种合理的状态它是已被认证的并且包含API所需的信息。这就是本文所要讨论的ASP.NET Core的认证与授权。认证应用程序对于使用者的身份认定包含两部分认证和授权。认证是指当前用户是否是系统的合法用户而授权则是指定合法用户对于哪些系统资源具有怎样的访问权限。我们先来看如何实现认证。在此我们单说由ASP.NET Core应用程序本身实现的认证不讨论具有统一Identity Provider完成身份认证的情况比如单点登录这样的话就能够更加清晰地了解ASP.NET Core本身的认证机制。接下来我们尝试在ASP.NET Core应用程序上实现Basic认证。Basic认证需要将用户的认证信息附属在HTTP请求的Authorization的头Header上认证信息是一串由用户名和密码通过BASE64编码后所产生的字符串例如当你采用Basic认证并使用daxnet和password作为访问WeatherForecast API的用户名和密码时你可能需要使用下面的命令行来调用WeatherForecast1curl -X GET http://localhost:5000/WeatherForecast -H accept: text/plain -H Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk在ASP.NET Core Web API中当应用程序接收到上述请求后就会从Request的Header里读取Authorization的信息然后BASE64解码得到用户名和密码然后访问数据库来确认所提供的用户名和密码是否合法以判断认证是否成功。这部分工作通常可以采用ASP.NET Core Identity框架来实现不过在这里为了能够更加清晰地了解认证的整个过程我们选择自己动手来实现。首先我们定义一个User对象并且预先设计好几个用户以便模拟存储用户信息的数据库这个User对象的代码如下12345678910111213141516public class User{ public string UserName { get; set; } public string Password { get; set; } public IEnumerablestring Roles { get; set; } public int Age { get; set; } public override string ToString() UserName; public static readonly User[] AllUsers { new User { UserName daxnet, Password password, Age 16, Roles new[] { admin, super_admin } }, new User { UserName admin, Password admin, Age 29, Roles new[] { admin } } };}该User对象包括用户名、密码以及它的角色名称不过暂时我们不需要关心角色信息。User对象还包含一个静态字段我们将它作为用户信息数据库来使用。接下来在应用程序中添加一个AuthenticationHandler用来获取Request Header中的用户信息并对用户信息进行验证代码如下123456789101112131415161718192021222324252627282930313233343536public class BasicAuthenticationHandler : AuthenticationHandlerBasicAuthenticationSchemeOptions{ public BasicAuthenticationHandler( IOptionsMonitorBasicAuthenticationSchemeOptions options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override TaskAuthenticateResult HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey(Authorization)) { return Task.FromResult(AuthenticateResult.Fail(Authorization header is not specified.)); } var authHeader Request.Headers[Authorization].ToString(); if (!authHeader.StartsWith(Basic )) { return Task.FromResult( AuthenticateResult.Fail(Authorization header value is not in a correct format)); } var base64EncodedValue authHeader[Basic .Length..]; var userNamePassword Encoding.UTF8.GetString(Convert.FromBase64String(base64EncodedValue)); var userName userNamePassword.Split(:)[0]; var password userNamePassword.Split(:)[1]; var user User.AllUsers.FirstOrDefault(u u.UserName userName u.Password password); if (user null) { return Task.FromResult(AuthenticateResult.Fail(Invalid username or password.)); } var claims new[] { new Claim(ClaimTypes.NameIdentifier, user.UserName), new Claim(ClaimTypes.Role, string.Join(,, user.Roles)), new Claim(ClaimTypes.UserData, user.Age.ToString()) }; var claimsPrincipal new ClaimsPrincipal(new ClaimsIdentity( claims, Basic, ClaimTypes.NameIdentifier, ClaimTypes.Role)); var ticket new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties { IsPersistent false }, Basic); return Task.FromResult(AuthenticateResult.Success(ticket)); }}在上面的HandleAuthenticateAsync代码中首先对Request Header进行合法性校验比如是否包含Authorization的Header以及Authorization Header的值是否合法然后将Authorization Header的值解析出来通过Base64解码后得到用户名和密码与用户信息数据库里的记录进行匹配找到匹配的用户。接下来基于找到的用户对象创建ClaimsPrincipal并基于ClaimsPrincipal创建AuthenticationTicket然后返回。这段代码中有几点值得关注BasicAuthenticationSchemeOptions本身只是一个继承于AuthenticationSchemeOptions的POCO类。AuthenticationSchemeOptions类通常是为了向AuthenticationHandler提供一些输入参数。比如在某个自定义的用户认证逻辑中可能需要通过环境变量读入字符串解密的密钥信息此时就可以在这个自定义的AuthenticationSchemeOptions中增加一个Passphrase的属性然后在Startup.cs中通过service.AddScheme调用将从环境变量中读取的Passphrase的值传入除了将用户名作为Identity Claim加入到ClaimsPrincipal中之外我们还将用户的角色Role用逗号串联起来作为Role Claim添加到ClaimsPrincipal中目前我们暂时不需要涉及角色相关的内容但是先将这部分代码放在这里以备后用。另外我们将用户的年龄Age放在UserData claim中在实际中应该是在用户对象上有该用户的出生日期这样比较合理然后这个出生日期应该放在DateOfBirth claim中这里为了简单起见就先放在UserData中了ClaimsPrincipal的构造函数中可以指定哪个Claim类型可被用作用户名称而哪个Claim类型又可被用作用户的角色。例如上面代码中我们选择NameIdentifier类型作为用户名而Role类型作为用户角色于是在接下来的Controller代码中由NameIdentifier这种Claim所指向的字符串值就会被看成用户名而被绑定到Identity.Name属性上回过头来看看BasicAuthenticationSchemeOptions类它的实现非常简单1234public class BasicAuthenticationSchemeOptions : AuthenticationSchemeOptions{}接下来在Startup.cs文件里修改ConfigureServices和Configure方法加入Authentication的支持1234567891011121314151617181920public void ConfigureServices(IServiceCollection services){ services.AddControllers(); services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title WebAPIAuthSample, Version v1 }); }); services.AddAuthentication(Basic) .AddSchemeBasicAuthenticationSchemeOptions, BasicAuthenticationHandler( Basic, options { });}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c c.SwaggerEndpoint(/swagger/v1/swagger.json, WebAPIAuthSample v1)); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); app.UseEndpoints(endpoints { endpoints.MapControllers(); });}现在运行应用程序在WeatherForecastController的Get方法上设置断点然后执行上面的curl命令当断点被命中时观察this.User对象可以发现IsAuthenticated属性变为了trueName属性也被设置为用户名大多数身份认证框架会提供一些辅助方法来帮助开发人员将AuthenticationHandler注册到应用程序中例如基于JWT持有者身份认证的框架会提供一个AddJwtBearer的方法将JWT身份认证机制加入到应用程序中它本质上也是调用AddScheme方法来完成AuthenticationHandler的注册。在这里我们也可以自定义一个AddBasicAuthentication的扩展方法1234567public static class Extensions{ public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder) builder.AddSchemeBasicAuthenticationSchemeOptions, BasicAuthenticationHandler( Basic, options { });}然后修改Starup.cs文件将ConfigureServices方法改为下面这个样子123456789public void ConfigureServices(IServiceCollection services){ services.AddControllers(); services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title WebAPIAuthSample, Version v1 }); }); services.AddAuthentication(Basic).AddBasicAuthentication();}这样做的好处是你可以为开发人员提供更多比较有针对性的配置认证机制的编程接口这对于一个认证模块/框架的开发是一个很好的设计。在curl命令中如果我们没有指定Authorization Header或者Authorization Header的值不正确那么WeatherForecast API仍然可以被调用只不过IsAuthenticated属性为false也无法从this.User对象得到用户信息。其实阻止未认证用户访问API并不是认证的事情API被未认证或者说未登录用户访问也是合理的事情因此要实现对于未认证用户的访问限制就需要进一步实现ASP.NET Core Web API的另一个安全控制组件授权。授权与认证相比授权的逻辑会比较复杂认证更多是技术层面的事情而授权则更多地与业务相关。市面上常见的认证机制顶多也就是那么几种或者十几种而授权的方式则是多样化的因为不同app不同业务对于app资源访问的授权需求是不同的。最为常见的一种授权方式就是RBACRole Based Access Control基于角色的访问控制它定义了什么样的角色对于什么资源具有怎样的访问权限。在RBAC中不同的用户都被赋予了不同的角色而为了管理方便又为具有相同资源访问权限的用户设计了用户组而将访问控制设置在用户组上更进一步组和组之间还可以有父子关系。请注意上面的黑体字每一个黑体标注的词语都是授权相关的概念在ASP.NET Core中每一个授权需求Authorization Requirement对应一个实现IAuthorizationRequirement的类并由AuthorizationHandler负责处理相应的授权逻辑。简单地理解授权需求表示什么样的用户才能够满足被授权的要求或者说什么样的用户才能够通过授权去访问资源。一个授权需求往往仅定义并处理一种特定的授权逻辑ASP.NET Core允许将多个授权需求组合成授权策略Authorization Policy然后应用到被访问的资源上这样的设计可以保证授权需求的设计与实现都是小粒度的从而分离不同授权需求的关注点。在授权策略的层面通过组合不同授权需求从而达到灵活实现授权业务的目的。比如假设app中有的API只允许管理员访问而有的API只允许满18周岁的用户访问而另外的一些API需要用户既是超级管理员又满18岁。那么就可以定义两种Authorization RequirementGreaterThan18Requirement和SuperAdminRequirement然后设计三种Policy第一种只包含GreaterThan18Requirement第二种只包含SuperAdminRequirement第三种则同时包含这两种Requirement最后将这些不同的Policy应用到不同的API上就可以了。回到我们的案例代码首先定义两个RequirementSuperAdminRequirement和GreaterThan18Requirement123456public class SuperAdminRequirement : IAuthorizationRequirement{}public class GreaterThan18Requirement : IAuthorizationRequirement{}然后分别实现SuperAdminAuthorizationHandle和GreaterThan18AuthorizationHandler实现逻辑也非常清晰在GreaterThan18AuthorizationHandler中通过UserData claim获得年龄信息如果年龄大于18则授权成功在SuperAdminAuthorizationHandler中通过Role claim获得用户所处的角色如果角色中包含super_admin则授权成功。接下来就需要将这两个Requirement加到所需的Policy中然后注册到应用程序里123456789101112131415161718192021222324252627public void ConfigureServices(IServiceCollection services){ services.AddControllers(); services.AddSwaggerGen(c { c.SwaggerDoc(v1, new OpenApiInfo { Title WebAPIAuthSample, Version v1 }); }); services.AddAuthentication(Basic).AddBasicAuthentication(); services.AddAuthorization(options { options.AddPolicy(AgeMustBeGreaterThan18, builder { builder.Requirements.Add(new GreaterThan18Requirement()); }); options.AddPolicy(UserMustBeSuperAdmin, builder { builder.Requirements.Add(new SuperAdminRequirement()); }); }); services.AddSingletonIAuthorizationHandler, GreaterThan18AuthorizationHandler(); services.AddSingletonIAuthorizationHandler, SuperAdminAuthorizationHandler();}public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c c.SwaggerEndpoint(/swagger/v1/swagger.json, WebAPIAuthSample v1)); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints { endpoints.MapControllers(); });}在ConfigureServices方法中我们定义了两种PolicyAgeMustBeGreaterThan18和UserMustBeSuperAdmin最后在API Controller或者Action上应用AuthorizeAttribute从而指定所需的Policy即可。比如如果希望WeatherForecase API只有年龄大于18岁的用户才能访问那么就可以这样做12345678910111213[HttpGet][Authorize(Policy AgeMustBeGreaterThan18)]public IEnumerableWeatherForecast Get(){ var rng new Random(); return Enumerable.Range(1, 5).Select(index new WeatherForecast { Date DateTime.Now.AddDays(index), TemperatureC rng.Next(-20, 55), Summary Summaries[rng.Next(Summaries.Length)] }) .ToArray();}运行程序假设有三个用户daxnet、admin和foo它们的BASE64认证信息分别为daxnetZGF4bmV0OnBhc3N3b3JkadminYWRtaW46YWRtaW4fooZm9vOmJhcg那么相同的curl命令指定不同的用户认证信息时得到的结果是不一样的daxnet用户年龄小于18岁所以访问API不成功服务端返回403admin用户满足年龄大于18岁的条件所以可以成功访问API而foo用户本身没有在系统中注册所以服务端返回401表示用户没有认证成功小结本文简要介绍了ASP.NET Core中用户身份认证与授权的基本实现方法帮助初学者或者需要使用这些功能的开发人员快速理解这部分内容。ASP.NET Core的认证与授权体系非常灵活能够集成各种不同的认证机制与授权方式文章也无法进行全面详细的介绍。不过无论何种框架哪种实现它的实现基础也就是本文所介绍的这些内容如果打算自己开发一套认证和授权的框架也可以参考本文。