欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Asp.Net Core Authorize你不知道的那些事(源码解读)

程序员文章站 2023-02-08 13:46:24
AuthorizeAttribute 和AuthorizeFilter是怎么样的一个关系?他们跟中间件又是怎样协同工作的?本文一起来探索Asp.Net Core 3.x 的源代码,深入解读他们的关系和中间件之间的那些你不知道的事。 ......

一、前言

identityserver4已经分享了一些应用实战的文章,从架构到授权中心的落地应用,也伴随着对identityserver4掌握了一些使用规则,但是很多原理性东西还是一知半解,故我这里持续性来带大家一起来解读它的相关源代码,本文先来看看为什么controller或者action中添加authorize或者全局中添加authorizefilter过滤器就可以实现该资源受到保护,需要通过access_token才能通过相关的授权呢?今天我带大家来了解authorizeattributeauthorizefilter的关系及代码解读。

二、代码解读

解读之前我们先来看看下面两种标注授权方式的代码:

标注方式
 [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终结点路由来获取controlleraction中的attribute特性标注,这里也是通过该方法来拦截获取对于的authorizeattribute的.
而获取到相关authorizedata授权数据后,下面的一系列代码都是通过判断来进行authorizeasync授权执行的方法,这里就不详细分享它的授权认证的过程了。
细心的同学应该已经发现上面的代码有一个比较特殊的代码:

if (endpoint?.metadata.getmetadata<iallowanonymous>() != null)
{
      await _next(context);
      return;
}

代码中通过endpoint终结点路由来获取是否标注有allowanonymous的特性,如果有则直接执行下一个中间件,不进行下面的authorizeasync授权认证方法,
这也是为什么controlleraction上标注allowanonymous可以跳过授权认证的原因了。

authorizefilter 源码

有的人会问authorizeattirbuteauthorizefilter有什么关系呢?它们是一个东西吗?
我们再来看看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 源代码中,该源代码中提供了四个构造初始化方法同时包含了authorizedatapolicy属性,我们看看它的默认构造方法代码

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来获得对于的授权策略进行授权认证.