AspNetCore3.1_Secutiry源码解析_8_Authorization_授权框架
目录
- aspnetcore3.1_secutiry源码解析_1_目录
- aspnetcore3.1_secutiry源码解析_2_authentication_核心流程
- aspnetcore3.1_secutiry源码解析_3_authentication_cookies
- aspnetcore3.1_secutiry源码解析_4_authentication_jwtbear
- aspnetcore3.1_secutiry源码解析_5_authentication_oauth
- aspnetcore3.1_secutiry源码解析_6_authentication_openidconnect
- aspnetcore3.1_secutiry源码解析_7_authentication_其他
- aspnetcore3.1_secutiry源码解析_8_authorization_授权框架
简介
开篇提到过,认证主要解决的是who are you,授权解决的是 are you allowed的问题。各种认证架构可以帮我们知道用户身份(claims),oauth等架构的scope字段能够控制api服务级别的访问权限,但是更加细化和多变的功能授权不是它们的处理范围。
微软的authorization项目提供了基于策略的灵活的授权框架。
推荐看下面博客了解,我主要学习和梳理源码。
https://www.cnblogs.com/rainingnight/p/authorization-in-asp-net-core.html
依赖注入
注入了以下接口,提供了默认实现
- iauthorizationservice :授权服务,主干服务
- iauthorizationpolicyprovider : 策略提供类
- iauthorizationhandlerprovider:处理器提供类
- iauthorizationevaluator:校验类
- iauthorizationhandlercontextfactory:授权上下文工厂
- iauthorizationhandler:授权处理器,这个是注入的集合,一个策略可以有多个授权处理器,依次执行
- 配置类:authorizationoptions
微软的命名风格还是比较一致的
service:服务
provider:某类的提供者
evaluator:校验预处理类
factory:工厂
handler:处理器
context:上下文
看源码的过程,不仅可以学习框架背后原理,还可以学习编码风格和设计模式,还是挺有用处的。
/// <summary> /// adds authorization services to the specified <see cref="iservicecollection" />. /// </summary> /// <param name="services">the <see cref="iservicecollection" /> to add services to.</param> /// <returns>the <see cref="iservicecollection"/> so that additional calls can be chained.</returns> public static iservicecollection addauthorizationcore(this iservicecollection services) { if (services == null) { throw new argumentnullexception(nameof(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; } /// <summary> /// adds authorization services to the specified <see cref="iservicecollection" />. /// </summary> /// <param name="services">the <see cref="iservicecollection" /> to add services to.</param> /// <param name="configure">an action delegate to configure the provided <see cref="authorizationoptions"/>.</param> /// <returns>the <see cref="iservicecollection"/> so that additional calls can be chained.</returns> public static iservicecollection addauthorizationcore(this iservicecollection services, action<authorizationoptions> configure) { if (services == null) { throw new argumentnullexception(nameof(services)); } if (configure != null) { services.configure(configure); } return services.addauthorizationcore(); }
配置类 - authorizationoptions
- policymap:策略名称&策略的字典数据
- invokehandlersafterfailure: 授权处理器失败后是否触发下一个处理器,默认true
- defaultpolicy:默认策略,构造了一个requireauthenticateduser策略,即需要认证用户,不允许匿名访问。现在有点线索了,为什么api一加上[authorize],就会校验授权。
- fallbackpolicy:保底策略。没有任何策略的时候会使用保底策略。感觉有点多此一举,不是给了个默认策略吗?
- addpolicy:添加策略
- getpolicy:获取策略
/// <summary> /// provides programmatic configuration used by <see cref="iauthorizationservice"/> and <see cref="iauthorizationpolicyprovider"/>. /// </summary> public class authorizationoptions { private idictionary<string, authorizationpolicy> policymap { get; } = new dictionary<string, authorizationpolicy>(stringcomparer.ordinalignorecase); /// <summary> /// determines whether authentication handlers should be invoked after a failure. /// defaults to true. /// </summary> public bool invokehandlersafterfailure { get; set; } = true; /// <summary> /// gets or sets the default authorization policy. defaults to require authenticated users. /// </summary> /// <remarks> /// the default policy used when evaluating <see cref="iauthorizedata"/> with no policy name specified. /// </remarks> public authorizationpolicy defaultpolicy { get; set; } = new authorizationpolicybuilder().requireauthenticateduser().build(); /// <summary> /// gets or sets the fallback authorization policy used by <see cref="authorizationpolicy.combineasync(iauthorizationpolicyprovider, ienumerable{iauthorizedata})"/> /// when no iauthorizedata have been provided. as a result, the authorizationmiddleware uses the fallback policy /// if there are no <see cref="iauthorizedata"/> instances for a resource. if a resource has any <see cref="iauthorizedata"/> /// then they are evaluated instead of the fallback policy. by default the fallback policy is null, and usually will have no /// effect unless you have the authorizationmiddleware in your pipeline. it is not used in any way by the /// default <see cref="iauthorizationservice"/>. /// </summary> public authorizationpolicy fallbackpolicy { get; set; } /// <summary> /// add an authorization policy with the provided name. /// </summary> /// <param name="name">the name of the policy.</param> /// <param name="policy">the authorization policy.</param> public void addpolicy(string name, authorizationpolicy policy) { if (name == null) { throw new argumentnullexception(nameof(name)); } if (policy == null) { throw new argumentnullexception(nameof(policy)); } policymap[name] = policy; } /// <summary> /// add a policy that is built from a delegate with the provided name. /// </summary> /// <param name="name">the name of the policy.</param> /// <param name="configurepolicy">the delegate that will be used to build the policy.</param> public void addpolicy(string name, action<authorizationpolicybuilder> configurepolicy) { if (name == null) { throw new argumentnullexception(nameof(name)); } if (configurepolicy == null) { throw new argumentnullexception(nameof(configurepolicy)); } var policybuilder = new authorizationpolicybuilder(); configurepolicy(policybuilder); policymap[name] = policybuilder.build(); } /// <summary> /// returns the policy for the specified name, or null if a policy with the name does not exist. /// </summary> /// <param name="name">the name of the policy to return.</param> /// <returns>the policy for the specified name, or null if a policy with the name does not exist.</returns> public authorizationpolicy getpolicy(string name) { if (name == null) { throw new argumentnullexception(nameof(name)); } return policymap.containskey(name) ? policymap[name] : null; } }
iauthorizationservice - 授权服务 - 主干逻辑
接口定义了授权方法,有两个重载,一个是基于requirements校验,一个是基于policyname校验。
task<authorizationresult> authorizeasync(claimsprincipal user, object resource, ienumerable<iauthorizationrequirement> requirements); task<authorizationresult> authorizeasync(claimsprincipal user, object resource, string policyname);
看下默认实现defaultauthorizationservice的处理,逻辑还是比较简单
- 获取策略
- 获取策略的授权条件
- 获取授权上下文
- 获取处理器集合
- 处理器依次执行,结果存入上下文
- 校验器验证上下文
- 返回授权结果类
/// <summary> /// the default implementation of an <see cref="iauthorizationservice"/>. /// </summary> public class defaultauthorizationservice : iauthorizationservice { private readonly authorizationoptions _options; private readonly iauthorizationhandlercontextfactory _contextfactory; private readonly iauthorizationhandlerprovider _handlers; private readonly iauthorizationevaluator _evaluator; private readonly iauthorizationpolicyprovider _policyprovider; private readonly ilogger _logger; /// <summary> /// creates a new instance of <see cref="defaultauthorizationservice"/>. /// </summary> /// <param name="policyprovider">the <see cref="iauthorizationpolicyprovider"/> used to provide policies.</param> /// <param name="handlers">the handlers used to fulfill <see cref="iauthorizationrequirement"/>s.</param> /// <param name="logger">the logger used to log messages, warnings and errors.</param> /// <param name="contextfactory">the <see cref="iauthorizationhandlercontextfactory"/> used to create the context to handle the authorization.</param> /// <param name="evaluator">the <see cref="iauthorizationevaluator"/> used to determine if authorization was successful.</param> /// <param name="options">the <see cref="authorizationoptions"/> used.</param> public defaultauthorizationservice(iauthorizationpolicyprovider policyprovider, iauthorizationhandlerprovider handlers, ilogger<defaultauthorizationservice> logger, iauthorizationhandlercontextfactory contextfactory, iauthorizationevaluator evaluator, ioptions<authorizationoptions> options) { if (options == null) { throw new argumentnullexception(nameof(options)); } if (policyprovider == null) { throw new argumentnullexception(nameof(policyprovider)); } if (handlers == null) { throw new argumentnullexception(nameof(handlers)); } if (logger == null) { throw new argumentnullexception(nameof(logger)); } if (contextfactory == null) { throw new argumentnullexception(nameof(contextfactory)); } if (evaluator == null) { throw new argumentnullexception(nameof(evaluator)); } _options = options.value; _handlers = handlers; _policyprovider = policyprovider; _logger = logger; _evaluator = evaluator; _contextfactory = contextfactory; } /// <summary> /// checks if a user meets a specific set of requirements for the specified resource. /// </summary> /// <param name="user">the user to evaluate the requirements against.</param> /// <param name="resource">the resource to evaluate the requirements against.</param> /// <param name="requirements">the requirements to evaluate.</param> /// <returns> /// a flag indicating whether authorization has succeeded. /// this value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>. /// </returns> public async task<authorizationresult> authorizeasync(claimsprincipal user, object resource, ienumerable<iauthorizationrequirement> requirements) { if (requirements == null) { throw new argumentnullexception(nameof(requirements)); } var authcontext = _contextfactory.createcontext(requirements, user, resource); var handlers = await _handlers.gethandlersasync(authcontext); foreach (var handler in handlers) { await handler.handleasync(authcontext); if (!_options.invokehandlersafterfailure && authcontext.hasfailed) { break; } } var result = _evaluator.evaluate(authcontext); if (result.succeeded) { _logger.userauthorizationsucceeded(); } else { _logger.userauthorizationfailed(); } return result; } /// <summary> /// checks if a user meets a specific authorization policy. /// </summary> /// <param name="user">the user to check the policy against.</param> /// <param name="resource">the resource the policy should be checked with.</param> /// <param name="policyname">the name of the policy to check against a specific context.</param> /// <returns> /// a flag indicating whether authorization has succeeded. /// this value is <value>true</value> when the user fulfills the policy otherwise <value>false</value>. /// </returns> public async task<authorizationresult> authorizeasync(claimsprincipal user, object resource, string policyname) { if (policyname == null) { throw new argumentnullexception(nameof(policyname)); } var policy = await _policyprovider.getpolicyasync(policyname); if (policy == null) { throw new invalidoperationexception($"no policy found: {policyname}."); } return await this.authorizeasync(user, resource, policy); } }
默认策略 - 需要认证用户
默认策略添加了校验条件denyanonymousauthorizationrequirement
public authorizationpolicybuilder requireauthenticateduser() { requirements.add(new denyanonymousauthorizationrequirement()); return this; }
校验上下文中是否存在认证用户信息,验证通过则在上下文中将校验条件标记为成功。
protected override task handlerequirementasync(authorizationhandlercontext context, denyanonymousauthorizationrequirement requirement) { var user = context.user; var userisanonymous = user?.identity == null || !user.identities.any(i => i.isauthenticated); if (!userisanonymous) { context.succeed(requirement); } return task.completedtask; }
授权时序图
授权项目还是比较好理解的,微软提供了一个基于策略的授权模型,大部门的具体的业务代码还是需要自己去实现的。
中间件去哪了?
开发不需要编写useauthorization类似代码,项目中也没发现中间件,甚至找不到 使用authorizeattribute的地方。那么问题来了,框架怎么知道某个方法标记了[authorize]特性,然后执行校验的呢?
答案是mvc框架处理的,它读取了节点的[authorize]和[allowanonymous]特性,并触发相应的逻辑。关于mvc的就不细说了,感兴趣可以翻看源码。
aspnetcore\src\mvc\mvc.core\src\applicationmodels\authorizationapplicationmodelprovider.cs。
public void onprovidersexecuting(applicationmodelprovidercontext context) { if (context == null) { throw new argumentnullexception(nameof(context)); } if (_mvcoptions.enableendpointrouting) { // when using endpoint routing, the authorizationmiddleware does the work that auth filters would otherwise perform. // consequently we do not need to convert authorization attributes to filters. return; } foreach (var controllermodel in context.result.controllers) { var controllermodelauthdata = controllermodel.attributes.oftype<iauthorizedata>().toarray(); if (controllermodelauthdata.length > 0) { controllermodel.filters.add(getfilter(_policyprovider, controllermodelauthdata)); } foreach (var attribute in controllermodel.attributes.oftype<iallowanonymous>()) { controllermodel.filters.add(new allowanonymousfilter()); } foreach (var actionmodel in controllermodel.actions) { var actionmodelauthdata = actionmodel.attributes.oftype<iauthorizedata>().toarray(); if (actionmodelauthdata.length > 0) { actionmodel.filters.add(getfilter(_policyprovider, actionmodelauthdata)); } foreach (var attribute in actionmodel.attributes.oftype<iallowanonymous>()) { actionmodel.filters.add(new allowanonymousfilter()); } } } }
推荐阅读
-
从源码解析Python的Flask框架中request对象的用法
-
Laravel框架源码解析之模型Model原理与用法解析
-
从源码解析Python的Flask框架中request对象的用法
-
Mybaits 源码解析 (十)----- 全网最详细,没有之一:Spring-Mybatis框架使用与源码解析
-
Laravel框架下路由的使用(源码解析)
-
Mybaits 源码解析 (一)----- 搭建一个mybatis框架(MyBatis HelloWorld)
-
Android源码解析之应用程序框架层和系统运行库层日志系统分析
-
Laravel框架源码解析之入口文件原理分析
-
Laravel框架源码解析之反射的使用详解
-
Java源码解析——集合框架(四)——LinkedListLinkedList原码分析