浅谈 asp.net core 中 Authentication 和 Authorization
在开始之前我们得搞清楚这两者的区别. 认证是我们在访问某数据资源的时候, 需要提供一个身份identity, 然后server拿着这个identity, 去某个存储容器中去匹配, 如果匹配上了, 证明认证成功.
至于是否你有权限访问这个资源, 需要看是否你对这个资源有权限, 想获取权限, 就必须给你的identity授权, 也就是让你有权限去访问资源.所以两个动作描述的阶段时不一样的.
所以简单点来说, 这两者一结合, 就相当于访问者访问 web server资源的一个过程. 首先访问者得持有一个login user, 用于登录web server. 然后web server这面也会持有一个访问者清单. 只有当
login user与清单中的user相匹配才能访问web server. 但是这个login user如果想访问的资源必须得到相应的权限级别. 有的机密文件则需要申请, 得到admin的 approve. 这个过程叫授权.
关于 Authentication
services.AddAuthentication
可以在 startup.cs 中的 configureservice 方法内部注入 IAuthenticationService 中间件. 这个 Authentication service 会使用注册到程序的 Authentication handler 进行相应的认证逻辑. 这些注册的 Authentication handelers 被称为 schemas. 所以我们通常见在 Startup.ConfigureServices
见到这样的配置:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => Configuration.Bind("JwtSettings", options))
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => Configuration.Bind("CookieSettings", options);
此处有多个 schema 被注册了进来. 所以在后面进行身份认证时, 就可以根据实际需要, 去使用不同的 schema 去进行认证. 使用方式也很简单, 比如对某个 Controller 使用 jwt 认证:
[Authorize(AuthenticationSchemes =
JwtBearerDefaults.AuthenticationScheme)]
public class MixedController : Controller
具体可以参考微软文档:
Authorize with a specific scheme in ASP.NET Core
services.AddDefaultIdentity
如果 asp.net core 项目是 web MVC 项目, 并且搭配了 individual users 模板, 我们可能会在 startup.configureservice 中看到这样的注入行为:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddRazorPages();
而 AddAuthentication
实际上是内置在 AddDefaultIdentity
里面了. 所以不用额外去再加一次.
app.UseAuthentication();
无论项目使用哪种方式将 Authentication Service 注入进来, 都需要在 startup.configure 中执行:
app.UseRouting()
app.UseAuthentication();
aspp.UseEndpoints();
将其添加到 http 请求响应的管道中去. 并且配置的相对顺序不能变.
Authentication 相关的术语
- Schema
一般是根据你添加的认证方式, 以及配置的 options, 去对 http request 请求进行身份认证. 所以
AddJwtBearer
, 就是添加了 jwt 认证方式,AddCookie
其实就是添加了 cookie 认证.
- Challenge
这里是指认证的过程, 比如当匿名用户请求登录或点击受限的资源链接。Authentication Service 会根据相应的或者默认的 Schema 进行认证的过程。 通常情况下, 基于 cookie 的认证会将用户重定向到 login 界面. 而基于 jwt 的认证, 则会返回 401 的 code.
- Forbid
Forbid 发生在身份认证通过以后的鉴权阶段, 由 Authorization service 判定 user 是否有权限访问资源. 当用户无权限访问资源时, 基于 cookie 的认证会在此阶段将用户重定向到一个显示 'user 无权限访问 ' 的page. 而基于 jwt 的认证会返回一个 403 code.
关于 Authorization
Authorization 机制是必须在 Authentication 结合下才能工作的. 现在我们需要简单了解一些关于鉴权的一些基本知识.
app.UseAuthorization();
我们通常会看到在 startup.configure 中, 会有一段代码:
app.UseAuthorization();
然后在某个 Controller 或者 Action 会看到:
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class PostController : DennisController
{
然后这个 Controller 和 Action 就是一个不允许匿名访问的资源. 也就是说 http 请求如果想访问这个 api, 需要携带认证信息, 经过身份认证后才能通过. 这一过程被称为简单鉴权
Simple authorization in ASP.NET Core
简单鉴权不能为我们系统提供细分领域的权限划分.
基于 role 的鉴权
当用户在系统的账户中心注册了一个账户后, 除了账户 id 和 password 之外, 一般还会为这个用户加入到系统的某个权限组中, 比如是 Admin, Manager, Member 等等. 不同的角色可以访问的资源也不同. 比如 小明注册了账户, 然后分配的权限是 Manager:
访问 get action 的权限是 ContactAdministrators. 我们分别用以下两个账户登录系统后访问该资源.
使用 manager 用户访问:
可以看到访问的 api/accounts/ 资源, 但是返回的结果是 access denied page, code 为 200.
而使用 admin 用户登录, 身份认证通过后, 鉴权也得以通过, 可以得到 api 中的资源内容:
更多关于 role-based 的鉴权和设定, 可以参考微软文档: Role-based authorization in ASP.NET Core
基于 claims 的鉴权
当一个身份证(identity)被创建出来后, 一般会由发证机关(identity server)对这个身份证添加一些条目(claims)来表明这个身份证的身份, 比如姓名, 性别, 出生年月, 身份证 id 等等. 这些条目组合到一起成为你的身份信息.
基于 claims 的鉴权例子也很多, 比如买 CBA 球票的时候, 球迷需要出示身份证买票, 如果发现你的身份证属于客队家乡的所在地(claim), 则只允许你买座位会被限定于球场的客队球迷的座位的票. 再比如在国外买酒的时候需要出示身份证, 验证你的身份后, 还需看你的年龄(claim)是否超过18岁, 才能合法买酒等等.
但是这种基于 claims 的鉴权策略一般是受到政策限制的. 就拿上面的例子来说, 如果没有这种政策, 则不会有这种鉴权行为, 所以在 asp.net core 程序中如果使用 claims 鉴权, 也需要在 startup.configureservice 中注册 policy.
更多关于 claims-based 的鉴权, 可参考微软文档: Claims-based authorization in ASP.NET Core
基于 policy 的鉴权
说完上面两种鉴权方式后, 继续说明, 当 asp.net core 程序中需要定制一种策略(policy) 去作为程序的鉴权原则时, 往往需要用到这个小节提到的基于 policy 的鉴权. 首先我们看看怎么添加 policy 鉴权的代码示例, 在 startup.configureservice 中:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast18", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
}
上面的例子展示了我们的程序要颁布一条规定(policy), 某些资源需要年满18岁的 user 才可以访问. 然后再针对某个 Controller 或者 action, 添加标签即可:
[Authorize(Policy = "AtLeast18")]
public IActionResult GetCangLaoShiVideos()
{
/// ....
return View();
}
在实际的复杂场景中, 可能不仅仅是制定一个策略标签, 挂载标签到资源上那么简单. 需要我们专门制定一些 Authorization Handler, 在 http 消息传递给 server 后, 使用这些 handler 来去专门鉴定 user 权限. 所以下面跟随一个demo 来帮助理解这部分内容.
首先创建一个 requirement 类, 继承 IAuthorizationRequirement
:
using Microsoft.AspNetCore.Authorization;
namespace DennisWu.Account.Service.Requirements
{
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
}
然后创建 MinimumAgeHandler, 继承 AuthorizationHandler<MinimumAgeRequirement>
:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using DennisWu.Account.Service.AuthorizationRequirements;
using Microsoft.AspNetCore.Authorization;
namespace DennisWu.Account.Service.AuthorizationHandlers
{
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
if (!context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth &&
c.Issuer == "http://dennisidentityserver.com"))
{
return Task.CompletedTask;
}
var dateOfBirth = Convert.ToDateTime(
context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth &&
c.Issuer == "http://dennisidentityserver.com").Value);
int calculatedAge = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth > DateTime.Today.AddYears(-calculatedAge))
{
calculatedAge--;
}
if (calculatedAge >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}
这个 handler 主要目的是验证 user 的 claims 是否符合要求: 来自受到信任的 issuer: dennisidentityserver.com 并且包含 claim: dateofbirth. 所以如果不符合要求的话, 就会直接进入Task.CompletedTask
返回. 如果符合要求, 并且 age 年满 18. 会调用 context.Succeed(requirement);