Asp.Net Core Authorize你不知道的那些事(源码解读)
一、前言
identityserver4
已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对identityserver4
掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持续性来带大家一起来解读它的相关源代码,本文先来看看为什么controller
或者action
中添加authorize
或者全局中添加authorizefilter
过滤器就可以实现该资源受到保护,需要通过access_token
才能通过相关的授权呢?今天我带大家来了解authorizeattribute
和authorizefilter
的关系及代码解读。
二、代码解读
解读之前我们先来看看下面两种标注授权方式的代码:
标注方式
[authorize] [httpget] public async task<object> get() { var userid = user.userid(); return new { name = user.name(), userid = userid, displayname = user.displayname(), merchantid = user.merchantid(), }; }
代码中通过[authorize]
标注来限制该api资源的访问
全局方式
public void configureservices(iservicecollection services) { //全局添加authorizefilter 过滤器方式 services.addcontrollers(options=>options.filters.add(new authorizefilter())); services.addauthorization(); services.addauthentication("bearer") .addidentityserverauthentication(options => { options.authority = "http://localhost:5000"; //配置identityserver的授权地址 options.requirehttpsmetadata = false; //不需要https options.apiname = oauthconfig.userapi.apiname; //api的name,需要和config的名称相同 }); }
全局通过添加authorizefilter
过滤器方式进行全局api资源的限制
authorizeattribute
先来看看authorizeattribute
源代码:
[attributeusage(attributetargets.class | attributetargets.method, allowmultiple = true, inherited = true)] public class authorizeattribute : attribute, iauthorizedata { /// <summary> /// initializes a new instance of the <see cref="authorizeattribute"/> class. /// </summary> public authorizeattribute() { } /// <summary> /// initializes a new instance of the <see cref="authorizeattribute"/> class with the specified policy. /// </summary> /// <param name="policy">the name of the policy to require for authorization.</param> public authorizeattribute(string policy) { policy = policy; } /// <summary> /// 收取策略 /// </summary> public string policy { get; set; } /// <summary> /// 授权角色 /// </summary> public string roles { get; set; } /// <summary> /// 授权schemes /// </summary> public string authenticationschemes { get; set; } }
代码中可以看到authorizeattribute
继承了iauthorizedata
抽象接口,该接口主要是授权数据的约束定义,定义了三个数据属性
- prolicy :授权策略
- roles : 授权角色
- authenticationschemes :授权schemes 的支持
asp.net core 中的http中间件会根据iauthorizedata
这个来获取有哪些授权过滤器,来实现过滤器的拦截并执行相关代码。
我们看看authorizeattribute
代码如下:
public interface iauthorizedata { /// <summary> /// gets or sets the policy name that determines access to the resource. /// </summary> string policy { get; set; } /// <summary> /// gets or sets a comma delimited list of roles that are allowed to access the resource. /// </summary> string roles { get; set; } /// <summary> /// gets or sets a comma delimited list of schemes from which user information is constructed. /// </summary> string authenticationschemes { get; set; } }
我们再来看看授权中间件
(useauthorization
)的核心代码:
public static iapplicationbuilder useauthorization(this iapplicationbuilder app) { if (app == null) { throw new argumentnullexception(nameof(app)); } verifyservicesregistered(app); return app.usemiddleware<authorizationmiddleware>(); }
代码中注册了authorizationmiddleware
这个中间件,authorizationmiddleware
中间件源代码如下:
public class authorizationmiddleware { // property key is used by endpoint routing to determine if authorization has run private const string authorizationmiddlewareinvokedwithendpointkey = "__authorizationmiddlewarewithendpointinvoked"; private static readonly object authorizationmiddlewarewithendpointinvokedvalue = new object(); private readonly requestdelegate _next; private readonly iauthorizationpolicyprovider _policyprovider; public authorizationmiddleware(requestdelegate next, iauthorizationpolicyprovider policyprovider) { _next = next ?? throw new argumentnullexception(nameof(next)); _policyprovider = policyprovider ?? throw new argumentnullexception(nameof(policyprovider)); } public async task invoke(httpcontext context) { if (context == null) { throw new argumentnullexception(nameof(context)); } var endpoint = context.getendpoint(); if (endpoint != null) { // endpointroutingmiddleware uses this flag to check if the authorization middleware processed auth metadata on the endpoint. // the authorization middleware can only make this claim if it observes an actual endpoint. context.items[authorizationmiddlewareinvokedwithendpointkey] = authorizationmiddlewarewithendpointinvokedvalue; } // 通过终结点路由元素iauthorizedata来获得对于的authorizeattribute并关联到authorizefilter中 var authorizedata = endpoint?.metadata.getorderedmetadata<iauthorizedata>() ?? array.empty<iauthorizedata>(); var policy = await authorizationpolicy.combineasync(_policyprovider, authorizedata); if (policy == null) { await _next(context); return; } // policy evaluator has transient lifetime so it fetched from request services instead of injecting in constructor var policyevaluator = context.requestservices.getrequiredservice<ipolicyevaluator>(); var authenticateresult = await policyevaluator.authenticateasync(policy, context); // allow anonymous skips all authorization if (endpoint?.metadata.getmetadata<iallowanonymous>() != null) { await _next(context); return; } // note that the resource will be null if there is no matched endpoint var authorizeresult = await policyevaluator.authorizeasync(policy, authenticateresult, context, resource: endpoint); if (authorizeresult.challenged) { if (policy.authenticationschemes.any()) { foreach (var scheme in policy.authenticationschemes) { await context.challengeasync(scheme); } } else { await context.challengeasync(); } return; } else if (authorizeresult.forbidden) { if (policy.authenticationschemes.any()) { foreach (var scheme in policy.authenticationschemes) { await context.forbidasync(scheme); } } else { await context.forbidasync(); } return; } await _next(context); } }
代码中核心拦截并获得authorizefilter
过滤器的代码
var authorizedata = endpoint?.metadata.getorderedmetadata<iauthorizedata>() ?? array.empty<iauthorizedata>();
前面我分享过一篇关于 asp.net core endpoint 终结点路由工作原理解读 的文章里面讲解到通过endpoint
终结点路由来获取controller
和action
中的attribute
特性标注,这里也是通过该方法来拦截获取对于的authorizeattribute
的.
而获取到相关authorizedata
授权数据后,下面的一系列代码都是通过判断来进行authorizeasync
授权执行的方法,这里就不详细分享它的授权认证的过程了。
细心的同学应该已经发现上面的代码有一个比较特殊的代码:
if (endpoint?.metadata.getmetadata<iallowanonymous>() != null) { await _next(context); return; }
代码中通过endpoint
终结点路由来获取是否标注有allowanonymous
的特性,如果有则直接执行下一个中间件,不进行下面的authorizeasync
授权认证方法,
这也是为什么controller
和action
上标注allowanonymous
可以跳过授权认证
的原因了。
authorizefilter 源码
有的人会问authorizeattirbute
和authorizefilter
有什么关系呢?它们是一个东西吗?
我们再来看看authorizefilter
源代码,代码如下:
public class authorizefilter : iasyncauthorizationfilter, ifilterfactory { /// <summary> /// initializes a new <see cref="authorizefilter"/> instance. /// </summary> public authorizefilter() : this(authorizedata: new[] { new authorizeattribute() }) { } /// <summary> /// initialize a new <see cref="authorizefilter"/> instance. /// </summary> /// <param name="policy">authorization policy to be used.</param> public authorizefilter(authorizationpolicy policy) { if (policy == null) { throw new argumentnullexception(nameof(policy)); } policy = policy; } /// <summary> /// initialize a new <see cref="authorizefilter"/> instance. /// </summary> /// <param name="policyprovider">the <see cref="iauthorizationpolicyprovider"/> to use to resolve policy names.</param> /// <param name="authorizedata">the <see cref="iauthorizedata"/> to combine into an <see cref="iauthorizedata"/>.</param> public authorizefilter(iauthorizationpolicyprovider policyprovider, ienumerable<iauthorizedata> authorizedata) : this(authorizedata) { if (policyprovider == null) { throw new argumentnullexception(nameof(policyprovider)); } policyprovider = policyprovider; } /// <summary> /// initializes a new instance of <see cref="authorizefilter"/>. /// </summary> /// <param name="authorizedata">the <see cref="iauthorizedata"/> to combine into an <see cref="iauthorizedata"/>.</param> public authorizefilter(ienumerable<iauthorizedata> authorizedata) { if (authorizedata == null) { throw new argumentnullexception(nameof(authorizedata)); } authorizedata = authorizedata; } /// <summary> /// initializes a new instance of <see cref="authorizefilter"/>. /// </summary> /// <param name="policy">the name of the policy to require for authorization.</param> public authorizefilter(string policy) : this(new[] { new authorizeattribute(policy) }) { } /// <summary> /// the <see cref="iauthorizationpolicyprovider"/> to use to resolve policy names. /// </summary> public iauthorizationpolicyprovider policyprovider { get; } /// <summary> /// the <see cref="iauthorizedata"/> to combine into an <see cref="iauthorizedata"/>. /// </summary> public ienumerable<iauthorizedata> authorizedata { get; } /// <summary> /// gets the authorization policy to be used. /// </summary> /// <remarks> /// if<c>null</c>, the policy will be constructed using /// <see cref="authorizationpolicy.combineasync(iauthorizationpolicyprovider, ienumerable{iauthorizedata})"/>. /// </remarks> public authorizationpolicy policy { get; } bool ifilterfactory.isreusable => true; // computes the actual policy for this filter using either policy or policyprovider + authorizedata private task<authorizationpolicy> computepolicyasync() { if (policy != null) { return task.fromresult(policy); } if (policyprovider == null) { throw new invalidoperationexception( resources.formatauthorizefilter_authorizationpolicycannotbecreated( nameof(authorizationpolicy), nameof(iauthorizationpolicyprovider))); } return authorizationpolicy.combineasync(policyprovider, authorizedata); } internal async task<authorizationpolicy> geteffectivepolicyasync(authorizationfiltercontext context) { // combine all authorize filters into single effective policy that's only run on the closest filter var builder = new authorizationpolicybuilder(await computepolicyasync()); for (var i = 0; i < context.filters.count; i++) { if (referenceequals(this, context.filters[i])) { continue; } if (context.filters[i] is authorizefilter authorizefilter) { // combine using the explicit policy, or the dynamic policy provider builder.combine(await authorizefilter.computepolicyasync()); } } var endpoint = context.httpcontext.getendpoint(); if (endpoint != null) { // when doing endpoint routing, mvc does not create filters for any authorization specific metadata i.e [authorize] does not // get translated into authorizefilter. consequently, there are some rough edges when an application uses a mix of authorizefilter // explicilty configured by the user (e.g. global auth filter), and uses endpoint metadata. // to keep the behavior of authfilter identical to pre-endpoint routing, we will gather auth data from endpoint metadata // and produce a policy using this. this would mean we would have effectively run some auth twice, but it maintains compat. var policyprovider = policyprovider ?? context.httpcontext.requestservices.getrequiredservice<iauthorizationpolicyprovider>(); var endpointauthorizedata = endpoint.metadata.getorderedmetadata<iauthorizedata>() ?? array.empty<iauthorizedata>(); var endpointpolicy = await authorizationpolicy.combineasync(policyprovider, endpointauthorizedata); if (endpointpolicy != null) { builder.combine(endpointpolicy); } } return builder.build(); } /// <inheritdoc /> public virtual async task onauthorizationasync(authorizationfiltercontext context) { if (context == null) { throw new argumentnullexception(nameof(context)); } if (!context.iseffectivepolicy(this)) { return; } // important: changes to authorization logic should be mirrored in security's authorizationmiddleware var effectivepolicy = await geteffectivepolicyasync(context); if (effectivepolicy == null) { return; } var policyevaluator = context.httpcontext.requestservices.getrequiredservice<ipolicyevaluator>(); var authenticateresult = await policyevaluator.authenticateasync(effectivepolicy, context.httpcontext); // allow anonymous skips all authorization if (hasallowanonymous(context)) { return; } var authorizeresult = await policyevaluator.authorizeasync(effectivepolicy, authenticateresult, context.httpcontext, context); if (authorizeresult.challenged) { context.result = new challengeresult(effectivepolicy.authenticationschemes.toarray()); } else if (authorizeresult.forbidden) { context.result = new forbidresult(effectivepolicy.authenticationschemes.toarray()); } } ifiltermetadata ifilterfactory.createinstance(iserviceprovider serviceprovider) { if (policy != null || policyprovider != null) { // the filter is fully constructed. use the current instance to authorize. return this; } debug.assert(authorizedata != null); var policyprovider = serviceprovider.getrequiredservice<iauthorizationpolicyprovider>(); return authorizationapplicationmodelprovider.getfilter(policyprovider, authorizedata); } private static bool hasallowanonymous(authorizationfiltercontext context) { var filters = context.filters; for (var i = 0; i < filters.count; i++) { if (filters[i] is iallowanonymousfilter) { return true; } } // when doing endpoint routing, mvc does not add allowanonymousfilters for allowanonymousattributes that // were discovered on controllers and actions. to maintain compat with 2.x, // we'll check for the presence of iallowanonymous in endpoint metadata. var endpoint = context.httpcontext.getendpoint(); if (endpoint?.metadata?.getmetadata<iallowanonymous>() != null) { return true; } return false; } }
代码中继承了 iasyncauthorizationfilter
, ifilterfactory
两个抽象接口,分别来看看这两个抽象接口的源代码
iasyncauthorizationfilter
源代码如下:
/// <summary> /// a filter that asynchronously confirms request authorization. /// </summary> public interface iasyncauthorizationfilter : ifiltermetadata { ///定义了授权的方法 task onauthorizationasync(authorizationfiltercontext context); }
iasyncauthorizationfilter
代码中继承了ifiltermetadata
接口,同时定义了onauthorizationasync
抽象方法,子类需要实现该方法,然而authorizefilter
中也已经实现了该方法,稍后再来详细讲解该方法,我们再继续看看ifilterfactory
抽象接口,代码如下:
public interface ifilterfactory : ifiltermetadata { bool isreusable { get; } //创建ifiltermetadata 对象方法 ifiltermetadata createinstance(iserviceprovider serviceprovider); }
我们回到authorizefilter
源代码中,该源代码中提供了四个构造初始化方法同时包含了authorizedata
、policy
属性,我们看看它的默认构造方法代码
public class authorizefilter : iasyncauthorizationfilter, ifilterfactory { public ienumerable<iauthorizedata> authorizedata { get; } //默认构造函数中默认创建了authorizeattribute 对象 public authorizefilter() : this(authorizedata: new[] { new authorizeattribute() }) { } //赋值authorizedata public authorizefilter(ienumerable<iauthorizedata> authorizedata) { if (authorizedata == null) { throw new argumentnullexception(nameof(authorizedata)); } authorizedata = authorizedata; } }
上面的代码中默认的构造函数默认给构建了一个authorizeattribute
对象,并且赋值给了ienumerable<iauthorizedata>
的集合属性;
好了,看到这里authorizefilter
过滤器也是默认构造了一个authorizeattribute
的对象,也就是构造了授权所需要的iauthorizedata
信息.
同时authorizefilter
实现的onauthorizationasync
方法中通过geteffectivepolicyasync
这个方法获得有效的授权策略,并且进行下面的授权authenticateasync
的执行authorizefilter
代码中提供了hasallowanonymous
方法来实现是否controller
或者action
上标注了allowanonymous
特性,用于跳过授权hasallowanonymous
代码如下:
private static bool hasallowanonymous(authorizationfiltercontext context) { var filters = context.filters; for (var i = 0; i < filters.count; i++) { if (filters[i] is iallowanonymousfilter) { return true; } } //同样通过上下文的endpoint 来获取是否标注了allowanonymous特性 var endpoint = context.httpcontext.getendpoint(); if (endpoint?.metadata?.getmetadata<iallowanonymous>() != null) { return true; } return false; }
到这里我们再回到全局添加过滤器的方式代码:
services.addcontrollers(options=>options.filters.add(new authorizefilter()));
分析到这里 ,我很是好奇,它是怎么全局添加进去的呢?我打开源代码看了下,源代码如下:
public class mvcoptions : ienumerable<icompatibilityswitch> { public mvcoptions() { cacheprofiles = new dictionary<string, cacheprofile>(stringcomparer.ordinalignorecase); conventions = new list<iapplicationmodelconvention>(); filters = new filtercollection(); formattermappings = new formattermappings(); inputformatters = new formattercollection<iinputformatter>(); outputformatters = new formattercollection<ioutputformatter>(); modelbinderproviders = new list<imodelbinderprovider>(); modelbindingmessageprovider = new defaultmodelbindingmessageprovider(); modelmetadatadetailsproviders = new list<imetadatadetailsprovider>(); modelvalidatorproviders = new list<imodelvalidatorprovider>(); valueproviderfactories = new list<ivalueproviderfactory>(); } //过滤器集合 public filtercollection filters { get; } }
filtercollection
相关核心代码如下:
public class filtercollection : collection<ifiltermetadata> { public ifiltermetadata add<tfiltertype>() where tfiltertype : ifiltermetadata { return add(typeof(tfiltertype)); } //其他核心代码为贴出来 }
代码中提供了add
方法,约束了ifiltermetadata
类型的对象,这也是上面的过滤器中为什么都继承了ifiltermetadata
的原因。
到这里代码解读和实现原理已经分析完了,如果有分析不到位之处还请多多指教!!!
结论:授权中间件通过获取iauthorizedata
来获取authorizeattribute
对象相关的授权信息,并构造授权策略
对象进行授权认证的,而authorizefilter
过滤器也会默认添加authorizeattribute
的授权相关数据iauthorizedata
并实现onauthorizationasync
方法,同时中间件中通过授权策略提供者iauthorizationpolicyprovider
来获得对于的授权策略进行授权认证.