当前位置: 移动技术网 > IT编程>开发语言>.net > Asp.Net Core Authorize你不知道的那些事(源码解读)

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

2020年03月25日  | 移动技术网IT编程  | 我要评论

yy97,谷牧之女,静海县邮编

一、前言

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网