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 命令来拜访这个 API:
1curl -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 的用户名和明码时,你可能须要应用上面的命令行来调用 WeatherForecast:
1curl -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 对象的代码如下:
public class User
{
public string UserName {get; set;}
public string Password {get; set;}
public IEnumerable<string> 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 中的用户信息,并对用户信息进行验证,代码如下:
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationSchemeOptions>
{
public BasicAuthenticationHandler(
IOptionsMonitor<BasicAuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> 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 类,它的实现非常简单:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen(c =>
{c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1"});
});
services.AddAuthentication("Basic")
.AddScheme<BasicAuthenticationSchemeOptions, 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 属性变为了 true,Name 属性也被设置为用户名:
大多数身份认证框架会提供一些辅助办法来帮忙开发人员将 AuthenticationHandler 注册到应用程序中,例如,基于 JWT 持有者身份认证的框架会提供一个 AddJwtBearer 的办法,将 JWT 身份认证机制退出到应用程序中,它实质上也是调用 AddScheme 办法来实现 AuthenticationHandler 的注册。在这里,咱们也能够自定义一个 AddBasicAuthentication 的扩大办法:
public static class Extensions
{
public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder)
=> builder.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(
"Basic",
options => {});
}
而后批改 Starup.cs 文件,将 ConfigureServices 办法改为上面这个样子:
public 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 资源拜访的受权需要是不同的。最为常见的一种受权形式就是 RBAC(Role Based Access Control,基于角色的访问控制),它定义了什么样的角色对于什么资源具备怎么的拜访权限。在 RBAC 中,不同的用户都被赋予了不同的角色,而为了治理不便,又为具备雷同资源拜访权限的用户设计了用户组,而将访问控制设置在用户组上,更进一步,组和组之间还能够有父子关系。
请留神下面的粗体字,每一个粗体标注的词语都是受权相干的概念,在 ASP.NET Core 中,每一个受权需要(Authorization Requirement)对应一个实现 IAuthorizationRequirement 的类,并由 AuthorizationHandler 负责解决相应的受权逻辑。简略地了解,受权需要示意什么样的用户才可能满足被受权的要求,或者说什么样的用户才可能通过受权去拜访资源。一个受权需要往往仅定义并解决一种特定的受权逻辑,ASP.NET Core 容许将多个受权需要组合成受权策略(Authorization Policy)而后利用到被拜访的资源上,这样的设计能够保障受权需要的设计与实现都是小粒度的,从而拆散不同受权需要的关注点。在受权策略的层面,通过组合不同受权需要从而达到灵便实现受权业务的目标。
比方:假如 app 中有的 API 只容许管理员拜访,而有的 API 只容许满 18 周岁的用户拜访,而另外的一些 API 须要用户既是超级管理员又满 18 岁。那么就能够定义两种 Authorization Requirement:GreaterThan18Requirement 和 SuperAdminRequirement,而后设计三种 Policy:第一种只蕴含 GreaterThan18Requirement,第二种只蕴含 SuperAdminRequirement,第三种则同时蕴含这两种 Requirement,最初将这些不同的 Policy 利用到不同的 API 上就能够了。
回到咱们的案例代码,首先定义两个 Requirement:SuperAdminRequirement 和 GreaterThan18Requirement:
public class SuperAdminRequirement : IAuthorizationRequirement
{
}
public class GreaterThan18Requirement : IAuthorizationRequirement
{
}
而后别离实现 SuperAdminAuthorizationHandle 和 GreaterThan18AuthorizationHandler:
实现逻辑也十分清晰:在 GreaterThan18AuthorizationHandler 中,通过 UserData claim 取得年龄信息,如果年龄大于 18,则受权胜利;在 SuperAdminAuthorizationHandler 中,通过 Role claim 取得用户所处的角色,如果角色中蕴含 super_admin,则受权胜利。接下来就须要将这两个 Requirement 加到所需的 Policy 中,而后注册到应用程序里:
public 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.AddSingleton<IAuthorizationHandler, GreaterThan18AuthorizationHandler>();
services.AddSingleton<IAuthorizationHandler, 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 办法中,咱们定义了两种 Policy:AgeMustBeGreaterThan18 和 UserMustBeSuperAdmin,最初,在 API Controller 或者 Action 上,利用 AuthorizeAttribute,从而指定所需的 Policy 即可。比方,如果心愿 WeatherForecase API 只有年龄大于 18 岁的用户能力拜访,那么就能够这样做:
[HttpGet]
[Authorize(Policy = “AgeMustBeGreaterThan18”)]
public IEnumerable<WeatherForecast> 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 认证信息别离为:
- daxnet:ZGF4bmV0OnBhc3N3b3Jk
- admin:YWRtaW46YWRtaW4=
- foo:Zm9vOmJhcg==
那么,雷同的 curl 命令,指定不同的用户认证信息时,失去的后果是不一样的:
daxnet 用户年龄小于 18 岁,所以拜访 API 不胜利,服务端返回 403:
admin 用户满足年龄大于 18 岁的条件,所以能够胜利拜访 API:
而 foo 用户自身没有在零碎中注册,所以服务端返回 401,示意用户没有认证胜利:
小结
本文简要介绍了 ASP.NET Core 中用户身份认证与受权的根本实现办法,帮忙初学者或者须要应用这些性能的开发人员疾速了解这部分内容。ASP.NET Core 的认证与受权体系非常灵活,可能集成各种不同的认证机制与受权形式,文章也无奈进行全面具体的介绍。不过无论何种框架哪种实现,它的实现根底也就是本文所介绍的这些内容,如果打算本人开发一套认证和受权的框架,也能够参考本文。
点击 Get ASP.NET Core 超全材料