引言
无论做什么应用,除非是完全公开的静态官网,总是会接触到认证与授权这两个概念.也许在小项目中这二者经常被混淆甚至误用,但是在现代化应用中,二者职责已相当分明,前者判断你是否合法,比如你登录爱奇艺时,认证中间件只判断账号密码是否正确,而当你点进一部新剧时,授权中间件则会对你账号的进行鉴权,比如是否有会员,是否有试看卷,通俗点来说,认证是鉴别用户是谁,而授权则是判断用户能做什么.
在ASP.NET core中认证是Authentication,授权是Authorization.也就是在项目入口中经常添加的
1 2
| app.UseAuthentication() app.UseAuthorization();
|
不同于以前在PHP中随便写写Cookie login,现在对安全的提高和权限的细分,已经使传统的基于Cookie和Session的认证方式无法完成部分环境要求(比如前后端分离后,部分接口认证转向JWT,或者Oauth2的授权码模式),传统ASP.NET MVC应用中,通常使用IdentityServier或者直接在Controller校验后HttpClient.Sigin(),现在这种方式仍然予以保留.在ASP.NET Core中由于框架被细分(例如Blazor、WebAPI、grpc),对这块权限跨分极为精密,能做的更广也更为精确.
Cliam(标识)和ClaimsIdentity(证件)
首先,ASP.NET Core中定义的最小身份信息是Cliam
,原意是声明或者主张,不必在意翻译是否准确,可以将它理解为证件上的某一项标识,比如身份证上的姓名:张三、性别:男或者身份证号:101XXXXXXXXXXXX等等,这些都可以看成一个个键值对,Key是类型(CliamType/string),Value是具体值(string),并且这种键值对是可以重用的,比如你身份证和你驾照上的身份证上就有相同的标识,我们就不必去重复定义标识,不同标识可以组合成不同的证件,而这个”证件”在ASP.NET Core中是ClaimsIdentity
,如果把前者理解为键值对,后者可以理解为SortedList
(可重复键值对).有时候一个证件不足以证明一个人的身份,一个人也可以拥有多张证件.而这个证件持有人就是ClaimsPrincipal
.
想象一下,写字楼大门保安只看你的工牌是否属于写字楼里面某个楼层的公司,那他就没有必要在乎你上面的名字,而等你进入公司后,公司又需要判断你的级别,不至于把普通员工安排到总经理办公室.等你下班后,你小区的保安又需要你小区的门禁卡,即便你们公司的工牌上和门禁卡上的姓名标识是一样的,保安也不会看你的工牌就放你进去,但是如果你恰巧没带门禁卡,却带着房产证的话,看房产证的信息判断你是小区业主,依然可以放行.
这几个场景下,要求的验证信息和方式各不相同不同,写字楼保安只在乎你的工牌上是否标识了某个公司,不管你是男是女;而公司需要你的工牌属于公司且需要查看工牌的身份信息;小区保安则要求你出示的是小区的门禁卡,至于你进入小区后怎么刷卡坐电梯那就是另一回事.应用到上面的Cliam
和ClaimsIdentity
,写字楼保安需要你的Cliam
的Type是公司,Key是写字楼的某一项公司,并不关心你的ClaimsIdentity.而公司则需要你的ClaimsIdentity
是XXX公司的工牌,并会查看你的ClaimsIdentity
中Cliam Type
为部门的值.而小区保安则判断你的ClaimsPrincipal
是否为业主,而不在乎你拿什么证件来证明.
身份认证
想象一下,如果需要自定义一个从数据库中验证并且有状态的Token认证,这和使用jwt或者其它token方案都不相同,那么需要如何操作呢?
在ASP.NET Core中,无论是认证还是授权,都是方案对应一个处理程序.其中,认证时的认证方案由Authorize
特性的AuthenticationSchemes
指定,所以,我们先需要添加一个认证方案:
1 2 3 4
| builder.Services.AddAuthentication(opt => { opt.AddScheme<TokenAuthentication>("CustomToken", "Token"); });
|
认证方案添加了,在定义认证处理程序,认证方案需要实现IAuthenticationHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| public class TokenAuthentication : IAuthenticationHandler { private HttpContext _context = default!; private AppDbContext _dbContext = default!; private AuthenticationScheme _scheme = default!; public async Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { _context = context; _scheme = scheme; await Task.FromResult(_dbContext = context.RequestServices.GetRequiredService<AppDbContext>()); }
public async Task<AuthenticateResult> AuthenticateAsync() { bool haveToken = _context.Request.Headers.TryGetValue("Authorization", out var tokenHeader); string token = haveToken ? tokenHeader.ToString()["Bearer ".Length..] : string.Empty; if (haveToken && await _dbContext.Users.SingleOrDefaultAsync(t => t.Token == token) is User user) { ClaimsIdentity claimsIdentity = new ClaimsIdentity(nameof(TokenAuthentication)); claimsIdentity.AddClaims(new Claim[] { new (ClaimTypes.Name, user.Name!), new (ClaimTypes.Sid, user.Id.ToString()!) }); return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), _scheme.Name)); } return AuthenticateResult.NoResult(); }
public async Task ChallengeAsync(AuthenticationProperties? properties) { await Task.FromResult(_context.Response.StatusCode = 401); }
public async Task ForbidAsync(AuthenticationProperties? properties) { await Task.FromResult(_context.Response.StatusCode = 403); } }
|
完事在控制器或者Action中指定Schemes即可:
1
| [Authorize(AuthenticationSchemes = "CustomToken")]
|
角色授权
在传统应用中RBAC(基于角色的访问控制)大行其道,在现如今的后台管理中依然适用,比如上面励志中,只需要定义公司员工和小区业主身份,并对应添加允许通行、坐电梯、停地下停车场等权限,就可以根据不同身份控制权限,并且同一用户可以同时拥有多个角色.具有一定的灵活性.
在ASP.NET Core中仍旧支持此种授权方式,添加认证和鉴权中间件后,我们可以在控制器或者具体方法上使用[Authorize]
特性的Role来控制其角色要求.
1 2 3 4 5 6 7 8 9 10 11 12
| [Authorize(Roles = "Administrator, PowerUser")] public class ControlAllPanelController : Controller { public IActionResult SetTime() => Content("Administrator || PowerUser");
[Authorize(Roles = "Administrator")] public IActionResult ShutDown() => Content("Administrator only"); }
|
登录的时候在ClaimsPrincipal中写入角色claim:
1 2 3 4 5 6 7 8 9 10 11 12
| var identity = new ClaimsIdentity(new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme));
identity.AddClaim(new Claim(ClaimTypes.Role, "Administrator")); identity.AddClaim(new Claim(ClaimTypes.Role, "PowerUser")); AuthenticationProperties properties = new AuthenticationProperties() { ExpiresUtc = DateTime.Now.AddDays(7), RedirectUri = model.ReturnUrl }; await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(identity), properties);
|
由于ClaimsIdentity内的键值对是可重复的,所以可以直接Add多个同Type的Claim(ClaimTypes.Role
).
RBAC大行其道,但是在某些情况下它缺乏一定的灵活性,比如现在新开了个网吧,需要18岁以上的成年人才可进入,如果在这里加一个可进入酒吧的权限,那后面再多一个只准12岁以下进入的游乐场或者只准60岁以下进入的鬼屋,难不成都要逐个加么?
基于此种应用需求,微软引入了声明和策略授权.
声明授权
回到上面所述的,小区保安只关心你有没有小区业主的身份,也不关心具体在几栋在几楼,我们可以做如下定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages(); builder.Services.AddControllersWithViews();
builder.Services.AddAuthorization(options => { options.AddPolicy("Owner", policy => policy.RequireClaim("Residential")); });
var app = builder.Build();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
app.Run();
|
在控制器或者Action上方标注授权要求:
1 2 3 4 5
| [Authorize(Policy = "EnsureSafety")] public IActionResult EnterCommunity() { return View(); }
|
当然,也是支持多重策略应用的,比如进入小区保安只管你业主身份,但是单元楼楼下有个大妈防着小偷,一定要熟面孔才让进:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| [Authorize(Policy = "Owner")] public class CommunityController : Controller { public IActionResult EnterCommunity() { return View(); }
[Authorize(Policy = "Acquaintance")] public IActionResult EnterUnitBuilding() { return View(); } }
|
策略授权
上述声明授权中,虽然说的是声明授权,但是定义却是用的AddPolicy,这不是添加策略的意思么?
其实,策略可以看作声明和角色的并集,所以使用策略授权定义声明也就没什么好奇怪的了.
基于角色的授权和基于声明的授权,只是一种语法上的便捷,最终都会生成授权策略
接着是上面所说的网吧最低年龄这种限制,我们可以添加一个自定义授权策略:
1 2 3 4 5 6 7 8
| public class MinimumAgeRequirement : IAuthorizationRequirement { public MinimumAgeRequirement(int minimumAge) => MinimumAge = minimumAge;
public int MinimumAge { get; } }
|
针对授权要求使用处理程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement> { protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, MinimumAgeRequirement requirement) { var dateOfBirthClaim = context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim is null) { return Task.CompletedTask; }
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.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; } }
|
- 处理程序通过调用 context.Succeed(IAuthorizationRequirement requirement) 并传递已成功验证的要求来指示成功。
- 处理程序通常不需要处理失败,因为针对相同要求的其他处理程序可能会成功。
- 为了保证失败,即使其他要求处理程序成功,也需调用 context.Fail。
添加自定义授权要求,并注入对应处理程序
1 2 3 4 5 6 7
| builder.Services.AddAuthorization(options => { options.AddPolicy("AtLeast18", policy => policy.Requirements.Add(new MinimumAgeRequirement(18))); });
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
|
一个授权策略可以添加多个要求,可以按照需求添加多个要求甚至直接组合.
参考