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

AspNetCore3.1_Secutiry源码解析_8_Authorization_授权框架

程序员文章站 2022-04-14 16:11:24
目录 "AspNetCore3.1_Secutiry源码解析_1_目录" "AspNetCore3.1_Secutiry源码解析_2_Authentication_核心流程" "AspNetCore3.1_Secutiry源码解析_3_Authentication_Cookies" "AspNetC ......

目录

简介

开篇提到过,认证主要解决的是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;
    }

授权时序图

授权项目还是比较好理解的,微软提供了一个基于策略的授权模型,大部门的具体的业务代码还是需要自己去实现的。

classdiagram class authorizationpolicy{ requirements } class requirement{ } class authorizationhandler{ } class iauthorizationhandler{ +handleasync(authorizationhandlercontext context) } class iauthorizationrequirement{ } requirement-->authorizationhandler authorizationhandler-->iauthorizationhandler requirement-->iauthorizationhandler requirement-->iauthorizationrequirement

中间件去哪了?

开发不需要编写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());
            }
        }
    }
}