ASP.NET Core 认证与授权[5]:初识授权
经过前面几章的姗姗学步,我们了解了在 ASP.NET Core 中是如何认证的,终于来到了授权阶段。在认证阶段我们通过用户令牌获取到用户的Claims,而授权便是对这些的Claims的验证,如:是否拥有Admin的角色,姓名是否叫XXX等等。本章就来介绍一下 ASP.NET Core 的授权系统的简单使用。
目录
简单授权
在ASP.NET 4.x中,我们通常使用Authorize
过滤器来进行授权,它可以作用在Controller和Action上面,也可以添加到全局过滤器中。而在ASP.NET Core中也有一个Authorize
特性(但不是过滤器),用法类似:
[Authorize] // Controller级别
public class SampleDataController : Controller
{
[Authorize] // Action级别
public IActionResult SampleAction()
{
}
}
IAllowAnonymous
在ASP.NET 4.x中,我们最常用的另一个特性便是AllowAnonymous
,用来设置某个Controller或者Action跳过授权,它在 ASP.NET Core 中同样适用:
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login()
{
}
public ActionResult Logout()
{
}
}
如上,Login
Action便不再需要授权,同样,在 ASP.NET Core 中提供了一个统一的IAllowAnonymous
接口,在授权逻辑中都是通过该接口来判断是否跳过授权验证的。
public interface IAllowAnonymous
{
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AllowAnonymousAttribute : Attribute, IAllowAnonymous
{
}
IAuthorizeData
上面提到,在 ASP.NET Core 中,AuthorizeAttribute
不再是一个MVC中的Filter
了,而只是一个简单的实现了IAuthorizeData
接口的Attribute:
public interface IAuthorizeData
{
string Policy { get; set; }
string Roles { get; set; }
string AuthenticationSchemes { get; set; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class AuthorizeAttribute : Attribute, IAuthorizeData
{
public AuthorizeAttribute() { }
public AuthorizeAttribute(string policy)
{
Policy = policy;
}
public string Policy { get; set; }
public string Roles { get; set; }
public string AuthenticationSchemes { get; set; }
}
记得第一次在ASP.NET Core中实现自定义授权时,按照以前的经验,直接继承自AuthorizeAttribute
,然后准备重写OnAuthorization
方法,结果懵逼了。然后在MVC的源码中,苦苦搜寻AuthorizeAttribute
的踪迹,却毫无所获,后来才注意到它实现了IAuthorizeData
接口,该接口才是认证的源头,而Authorize特性只是认证信息的载体,并不包含任何逻辑。IAuthorizeData
中定义的Policy
, Roles
, AuthenticationSchemes
三个属性分别代表着 ASP.NET Core 授权系统中的三种授权方式。
基于角色的授权
基于角色的授权,我们都比较熟悉,使用方式如下:
[Authorize(Roles = "Admin")] // 多个Role可以使用,分割
public class SampleDataController : Controller
{
...
}
基于角色的授权的逻辑与ASP.NET 4.x类似,都是使用我在《初识认证》中介绍的IsInRole
方法来实现的。
基于Scheme的授权
对于AuthenticationScheme我在前面几章也都介绍过,比如Cookie认证默认使用的AuthenticationScheme就是Cookies
,在JwtBearer认证中,默认的Scheme就是Bearer
。
当初在学习认证时,还在疑惑,如何在使用Cookie认证的同时又支持Bearer认证呢?在认证中明明只能设置一个Scheme来执行。当看到这里时,豁然开朗,后面会详细介绍。
[Authorize(AuthenticationSchemes = "Cookies")] // 多个Scheme可以使用,分割
public class SampleDataController : Controller
{
...
}
当我们的应用程序中,同时使用了多种认证Scheme时,AuthenticationScheme授权就非常有用,在该授权模式下,会通过context.AuthenticateAsync(scheme)
重新获取Claims。
基于策略的授权
在ASP.NET Core中,重新设计了一种更加灵活的授权方式:基于策略的授权,也是授权的核心。
在使用基于策略的授权时,首先要定义授权策略,而授权策略本质上就是对Claims的一系列断言。
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthorization(options =>
{
options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
}
如上,我们定义了一个名称为EmployeeOnly
的授权策略,它要求用户的Claims中必须包含类型为EmployeeNumber
的Claim。
其实,基于角色的授权和基于Scheme的授权,只是一种语法上的便捷,最终都会生成授权策略,后文会详解介绍。
然后便可以在Authorize
特性中通过Policy
属性来指定授权策略:
[Authorize(Policy = "EmployeeOnly")]
public class SampleDataController : Controller
{
}
授权策略详解
AddAuthorization
授权策略的定义使用了AddAuthorization
扩展方法,我们来看看它的源码:
public static class AuthorizationServiceCollectionExtensions
{
public static IServiceCollection AddAuthorization(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationService, DefaultAuthorizationService>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationPolicyProvider, DefaultAuthorizationPolicyProvider>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerProvider, DefaultAuthorizationHandlerProvider>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationEvaluator, DefaultAuthorizationEvaluator>());
services.TryAdd(ServiceDescriptor.Transient<IAuthorizationHandlerContextFactory, DefaultAuthorizationHandlerContextFactory>());
services.TryAddEnumerable(ServiceDescriptor.Transient<IAuthorizationHandler, PassThroughAuthorizationHandler>());
return services;
}
public static IServiceCollection AddAuthorization(this IServiceCollection services, Action<AuthorizationOptions> configure)
{
services.Configure(configure);
return services.AddAuthorization();
}
}
首先,是对授权进行配置的AuthorizationOptions
,然后在DI系统中注册了几个核心对象的默认实现,我们一一来看。
AuthorizationOptions
对于Options模式,大家应该都比较熟悉了,AuthorizationOptions
是添加和获取授权策略的入口点:
public class AuthorizationOptions
{
private IDictionary<string, AuthorizationPolicy> PolicyMap { get; } = new Dictionary<string, AuthorizationPolicy>(StringComparer.OrdinalIgnoreCase);
// 在上一个策略验证失败后,是否继续执行下一个授权策略
public bool InvokeHandlersAfterFailure { get; set; } = true;
public AuthorizationPolicy DefaultPolicy { get; set; } = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
public void AddPolicy(string name, AuthorizationPolicy policy)
{
PolicyMap[name] = policy;
}
public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy)
{
var policyBuilder = new AuthorizationPolicyBuilder();
configurePolicy(policyBuilder);
AddPolicy(name,policyBuilder.Build());
}
public AuthorizationPolicy GetPolicy(string name)
{
return PolicyMap.ContainsKey(name) ? PolicyMap[name] : null;
}
}
首先是一个PolicyMap
字典,我们定义的策略都保存在其中,AddPolicy
方法只是简单的将策略添加到该字典中,而其DefaultPolicy
属性表示默认策略,初始值为:“已认证用户”。
在AuthorizationOptions
中主要涉及到AuthorizationPolicy
和AuthorizationPolicyBuilder
两个对象。
AuthorizationPolicy
在 ASP.NET Core 中,授权策略具体表现为一个AuthorizationPolicy
对象:
public class AuthorizationPolicy
{
public AuthorizationPolicy(IEnumerable<IAuthorizationRequirement> requirements, IEnumerable<string> authenticationSchemes) {}
public IReadOnlyList<IAuthorizationRequirement> Requirements { get; }
public IReadOnlyList<string> AuthenticationSchemes { get; }
public static AuthorizationPolicy Combine(params AuthorizationPolicy[] policies)
{
return Combine((IEnumerable<AuthorizationPolicy>)policies);
}
public static AuthorizationPolicy Combine(IEnumerable<AuthorizationPolicy> policies)
{
foreach (var policy in policies)
{
builder.Combine(policy);
}
return builder.Build();
}
public static async Task<AuthorizationPolicy> CombineAsync(IAuthorizationPolicyProvider policyProvider, IEnumerable<IAuthorizeData> authorizeData)
{
foreach (var authorizeDatum in authorizeData)
{
any = true;
var useDefaultPolicy = true;
if (!string.IsNullOrWhiteSpace(authorizeDatum.Policy))
{
policyBuilder.Combine(await policyProvider.GetPolicyAsync(authorizeDatum.Policy));
useDefaultPolicy = false;
}
var rolesSplit = authorizeDatum.Roles?.Split(',');
if (rolesSplit != null && rolesSplit.Any())
{
policyBuilder.RequireRole(rolesSplit.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()));
useDefaultPolicy = false;
}
var authTypesSplit = authorizeDatum.AuthenticationSchemes?.Split(',');
if (authTypesSplit != null && authTypesSplit.Any())
{
foreach (var authType in authTypesSplit)
{
if (!string.IsNullOrWhiteSpace(authType))
{
policyBuilder.AuthenticationSchemes.Add(authType.Trim());
}
}
}
if (useDefaultPolicy)
{
policyBuilder.Combine(await policyProvider.GetDefaultPolicyAsync());
}
}
return any ? policyBuilder.Build() : null;
}
}
如上,Combine
方法通过调用AuthorizationPolicyBuilder
来完成授权策略的合并,而CombineAsync
则是将我们上面介绍的IAuthorizeData
转换为授权策略,因此上面说基于角色/Scheme的授权本质上都是基于策略的授权。
对于AuthenticationSchemes
属性,我们在前几章介绍认证时经常看到,用来表示我们使用哪个认证Scheme来获取用户的Claims,如果指定多个,则会合并它们的Claims,其实现《下一章》中再来详细介绍。
而Requirements
属性则是策略的核心了,每一个Requirement都代表一个授权条件,我们就先来了解一下它。
IAuthorizationRequirement
Requirement使用IAuthorizationRequirement
接口来表示:
public interface IAuthorizationRequirement
{
}
IAuthorizationRequirement接口中并没有任何成员,在 ASP.NET Core 中内置了一些常用的实现:
AssertionRequirement :使用最原始的断言形式来声明授权策略。
DenyAnonymousAuthorizationRequirement :用于表示禁止匿名用户访问的授权策略,并在
AuthorizationOptions
中将其设置为默认策略。ClaimsAuthorizationRequirement :用于表示判断Cliams中是否包含预期的Claims的授权策略。
RolesAuthorizationRequirement :用于表示使用
ClaimsPrincipal.IsInRole
来判断是否包含预期的Role的授权策略。NameAuthorizationRequirement:用于表示使用
ClaimsPrincipal.Identities.Name
来判断是否包含预期的Name的授权策略。OperationAuthorizationRequirement:用于表示基于操作的授权策略。
其逻辑也都非常简单,我就不再一一介绍,只展示一下RolesAuthorizationRequirement
的代码片段:
public class RolesAuthorizationRequirement : AuthorizationHandler<RolesAuthorizationRequirement>, IAuthorizationRequirement
{
public IEnumerable<string> AllowedRoles { get; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RolesAuthorizationRequirement requirement)
{
...
if (requirement.AllowedRoles.Any(r => context.User.IsInRole(r)))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
其AllowedRoles
表示允许授权通过的角色,而它还实现了IAuthorizationHandler
接口,用来完成授权的逻辑。
public interface IAuthorizationHandler
{
Task HandleAsync(AuthorizationHandlerContext context);
}
AuthorizationRequirement并不是一定要实现
IAuthorizationHandler
接口,后文会详细介绍。
AuthorizationPolicyBuilder
在上面已经多次用到AuthorizationPolicyBuilder
,它提供了一系列创建AuthorizationPolicy
的快捷方法:
public class AuthorizationPolicyBuilder
{
public AuthorizationPolicyBuilder(params string[] authenticationSchemes);
public AuthorizationPolicyBuilder(AuthorizationPolicy policy);
public IList<IAuthorizationRequirement> Requirements { get; set; }
public IList<string> AuthenticationSchemes { get; set; }
public AuthorizationPolicyBuilder AddAuthenticationSchemes(params string[] schemes);
public AuthorizationPolicyBuilder AddRequirements(params IAuthorizationRequirement[] requirements);
public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, bool> handler);
public AuthorizationPolicyBuilder RequireAssertion(Func<AuthorizationHandlerContext, Task<bool>> handler)
{
Requirements.Add(new AssertionRequirement(handler));
return this;
}
public AuthorizationPolicyBuilder RequireAuthenticatedUser()
{
Requirements.Add(new DenyAnonymousAuthorizationRequirement());
return this;
}
public AuthorizationPolicyBuilder RequireClaim(string claimType);
public AuthorizationPolicyBuilder RequireClaim(string claimType, params string[] requiredValues);
public AuthorizationPolicyBuilder RequireClaim(string claimType, IEnumerable<string> requiredValues)
{
Requirements.Add(new ClaimsAuthorizationRequirement(claimType, requiredValues));
return this;
}
public AuthorizationPolicyBuilder RequireRole(params string[] roles);
public AuthorizationPolicyBuilder RequireRole(IEnumerable<string> roles)
{
Requirements.Add(new RolesAuthorizationRequirement(roles));
return this;
}
public AuthorizationPolicyBuilder RequireUserName(string userName)
{
Requirements.Add(new NameAuthorizationRequirement(userName));
return this;
}
public AuthorizationPolicy Build();
public AuthorizationPolicyBuilder Combine(AuthorizationPolicy policy);
}
在上面介绍的几个Requirement,除了OperationAuthorizationRequirement
外,都有对应的快捷添加方法,由于OperationAuthorizationRequirement
并不属于基于资源的授权,所以不在这里,其用法留在其后续章节再来介绍。
整个授权策略的内容也就这么多,并不复杂,整个结构大致如下:
基于策略的授权进阶
在上一小节,我们探索了一下授权策略的源码,现在就来实战一下。
我们使用AuthorizationPolicyBuilder
可以很容易的在策略定义中组合我们需要的Requirement:
public void ConfigureServices(IServiceCollection services)
{
var commonPolicy = new AuthorizationPolicyBuilder().RequireClaim("MyType").Build();
services.AddAuthorization(options =>
{
options.AddPolicy("User", policy => policy
.RequireAssertion(context => context.User.HasClaim(c => (c.Type == "EmployeeNumber" || c.Type == "Role")))
);
options.AddPolicy("Employee", policy => policy
.RequireRole("Admin")
.RequireUserName("Alice")
.RequireClaim("EmployeeNumber")
.Combine(commonPolicy));
});
}
如上,如果需要,我们还可以定义一个公共的策略对象,然后在策略定义中直接将其合并进来。
自定义策略
当内置的Requirement不能满足我们的需求时,我们也可以很容易的定义自己的Requirement:
public class MinimumAgeRequirement : AuthorizationHandler<NameAuthorizationRequirement>, IAuthorizationRequirement
{
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
public int MinimumAge { get; private set; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NameAuthorizationRequirement requirement)
{
if (context.User != null && context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth)
{
var dateOfBirth = Convert.ToDateTime(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).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;
}
}
然后就可以直接在AddPolicy
中使用了:
services.AddAuthorization(options =>
{
options.AddPolicy("Over21", policy => policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
我们自定义的 Requirement 若想得到 ASP.NET Core 授权系统的执行,除了上面示例中的实现IAuthorizationHandler
接口外,也可以单独定义AuthorizationHandler,这样可以更好的使用DI系统,并且还可以定义多个Handler,下面就来演示一下。
多Handler模式
授权策略中的多个Requirement,它们属于 & 的关系,只用全部验证通过,才能最终授权成功。但是在有些场景下,我们可能希望一个授权策略可以适用多种情况,比如,我们进入公司时需要出示员工卡才可以被授权进入,但是如果我们忘了带员工卡,可以去申请一个临时卡,同样可以授权成功:
public class EnterBuildingRequirement : IAuthorizationRequirement
{
}
public class BadgeEntryHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.BadgeId)
{
context.Succeed(requirement);
}
else
{
// context.Fail();
}
return Task.CompletedTask;
}
}
public class HasTemporaryStickerHandler : AuthorizationHandler<EnterBuildingRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EnterBuildingRequirement requirement)
{
if (context.User.HasClaim(c => c.Type == ClaimTypes.TemporaryBadgeId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
如上,我们定义了两个Handler,但是想让它们得到执行,还需要将其注册到DI系统中:
services.AddSingleton<IAuthorizationHandler, BadgeEntryHandler>();
services.AddSingleton<IAuthorizationHandler, HasTemporaryStickerHandler>();
此时,在我们的应该程序中使用EnterBuildingRequirement
的授权时,将会依次执行这两个Handler。而在上面介绍AuthorizationOptions
时,提到它还有一个InvokeHandlersAfterFailure
属性,在这里就派上用场了,只有其为true
时(默认为True),才会在当前 AuthorizationHandler 授权失败时,继续执行下一个 AuthorizationHandler。
在上面的示例中,我们使用context.Succeed(requirement)
将授权结果设置为成功,而失败时并没有做任何标记,正常情况下都是这样做的。但是如果需要,我们可以通过调用context.Fail()
方法显式的将授权结果设置为失败,那么,不管其他 AuthorizationHandler 是成功还是失败,最终结果都将是授权失败。
总结
ASP.NET Core 授权策略是一种非常强大、灵活的权限验证方案,提供了更丰富、更易表达的验证模型,能够满足大部分的授权场景。通过本文对授权策略的详细介绍,我们应该能够灵活的使用基于策略的授权了,但是授权策略到底是怎么执行的呢?在《下一章》中,就来完整的探索一下 ASP.NET Core 授权系统的执行流程。
ASP.NET Core 认证与授权[5]:初识授权的更多相关文章
- ASP.NET Core 认证与授权[2]:Cookie认证
由于HTTP协议是无状态的,但对于认证来说,必然要通过一种机制来保存用户状态,而最常用,也最简单的就是Cookie了,它由浏览器自动保存并在发送请求时自动附加到请求头中.尽管在现代Web应用中,Coo ...
- ASP.NET Core WebApi基于JWT实现接口授权验证
一.ASP.Net Core WebApi JWT课程前言 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再 ...
- 从零搭建一个IdentityServer——聊聊Asp.net core中的身份验证与授权
OpenIDConnect是一个身份验证服务,而Oauth2.0是一个授权框架,在前面几篇文章里通过IdentityServer4实现了基于Oauth2.0的客户端证书(Client_Credenti ...
- ASP.NET Core 认证与授权[1]:初识认证
在ASP.NET 4.X 中,我们最常用的是Forms认证,它既可以用于局域网环境,也可用于互联网环境,有着非常广泛的使用.但是它很难进行扩展,更无法与第三方认证集成,因此,在 ASP.NET Cor ...
- ASP.NET Core 认证与授权[1]:初识认证 (笔记)
原文链接: https://www.cnblogs.com/RainingNight/p/introduce-basic-authentication-in-asp-net-core.html 在A ...
- ASP.NET Core 认证与授权[3]:OAuth & OpenID Connect认证
在上一章中,我们了解到,Cookie认证是一种本地认证方式,通常认证与授权都在同一个服务中,也可以使用Cookie共享的方式分开部署,但局限性较大,而如今随着微服务的流行,更加偏向于将以前的单体应用拆 ...
- ASP.NET Core 认证与授权[4]:JwtBearer认证
在现代Web应用程序中,通常会使用Web, WebApp, NativeApp等多种呈现方式,而后端也由以前的Razor渲染HTML,转变为Stateless的RESTFulAPI,因此,我们需要一种 ...
- ASP.NET Core 认证与授权[6]:授权策略是怎么执行的?
在上一章中,详细介绍了 ASP.NET Core 中的授权策略,在需要授权时,只需要在对应的Controler或者Action上面打上[Authorize]特性,并指定要执行的策略名称即可,但是,授权 ...
- Asp.net Core认证和授权:Cookie认证
关于asp.net core 的文章,博客园已经有很多大牛写过了. 这里我只是记录下自己在学习中的点滴和一些不懂的地方 Cookie一般是用户网站授权,当用户访问需要授权(authorization) ...
随机推荐
- MVC调用部分视图PartialView
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace Demo2 ...
- 【转载】CSS direction属性简介与实际应用
文章转载自 张鑫旭-鑫空间-鑫生活 http://www.zhangxinxu.com/wordpress/ 原文链接:http://www.zhangxinxu.com/wordpress/?p=5 ...
- 我的第一个python web开发框架(8)——项目结构与RESTful接口风格说明
PS:再次说明一下,原本不想写的太啰嗦的,可之前那个系列发布后发现,好多朋友都想马上拿到代码立即能上手开发自己的项目,对代码结构.基础常识.分类目录与文件功能结构.常用函数......等等什么都不懂, ...
- mac 通过brew安装php70 +php-fpm+ phalcon3.0.3
安装php7.0.15 brew install homebrew/php/php70 brew install homebrew/php/php70-mcrypt brew install home ...
- Mongodb 认证鉴权那点事
[TOC] 一.Mongodb 的权限管理 认识权限管理,说明主要概念及关系 与大多数数据库一样,Mongodb同样提供了一套权限管理机制. 为了体验Mongodb 的权限管理,我们找一台已经安装好的 ...
- OpenWRT添加模块 Makefile和Config.in
添加模块编译 在网上找了一下,很多关于编译Openwrt系统的资料,不过这些事情芯片厂商提供的开发包都已经办得妥妥了,但是没有找到系统介绍的资料,添加一个包的介绍有不多,其中有两个很有参考价值: ht ...
- JavaScript函数之实际参数对象(arguments) / callee属性 / caller属性 / 递归调用 / 获取函数名称的方法
函数的作用域:调用对象 JavaScript中函数的主体是在局部作用域中执行的,该作用域不同于全局作用域.这个新的作用域是通过将调用对象添加到作用域链的头部而创建的(没怎么理解这句话,有理解的亲可以留 ...
- HandlerMapping 和 HandlerAdapter
HandlerMapping 负责根据request请求找到对应的Handler处理器及Interceptor拦截器,将它们封装在HandlerExecutionChain 对象中给前端控制器返回. ...
- 本地连接 vmware服务器
在本机中装载虚拟机,安装redhat.需要调试使用redhat可以与Windows进行通讯. 分为多步,在此前提下,默认你已经安装好且可以vm 和虚拟机 1:点击虚拟机>设置>添加网络适配 ...
- Java基础总结--泛型总结
-----泛型------JDK1.5出现的机制1.泛型出现的原因--简化书写,提高安全性技术的由来是为了解决问题,现在存在该问题,所有的容器定义类型为Object,所以任何对 象均可以放入容器--进 ...