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

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

程序员文章站 2022-05-16 09:03:35
" 【.NET Core项目实战 统一认证平台】开篇及目录索引 " 上篇文章我介绍了如何强制令牌过期的实现,相信大家对 的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。 .netcore项目实战交流群(6 ......

【.net core项目实战-统一认证平台】开篇及目录索引

上篇文章我介绍了如何强制令牌过期的实现,相信大家对identityserver4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、自定授权源码剖析

当我们需要使用开源项目的某些功能时,最好了解实现的原理,才能正确和熟练使用功能,避免出现各种未知bug问题和出现问题无法解决的被动场面。

在使用此功能前,我们需要了解完整的实现流程,下面我将从源码开始讲解identityserver4是如何实现自定义的授权方式。

从我之前的文章中我们知道授权方式是通过grant_type的值来判断的,所以我们自定义的授权方式,也是通过此值来区分,所以需要了解自定义的值处理流程。tokenrequestvalidator是请求验证的方法,除了常规验证外,还增加了自定义的验证方式。

public async task<tokenrequestvalidationresult> validaterequestasync(namevaluecollection parameters, clientsecretvalidationresult clientvalidationresult)
{
    _logger.logdebug("start token request validation");

    _validatedrequest = new validatedtokenrequest
    {
        raw = parameters ?? throw new argumentnullexception(nameof(parameters)),
        options = _options
    };

    if (clientvalidationresult == null) throw new argumentnullexception(nameof(clientvalidationresult));

    _validatedrequest.setclient(clientvalidationresult.client, clientvalidationresult.secret, clientvalidationresult.confirmation);

    /////////////////////////////////////////////
    // check client protocol type
    /////////////////////////////////////////////
    if (_validatedrequest.client.protocoltype != identityserverconstants.protocoltypes.openidconnect)
    {
        logerror("client {clientid} has invalid protocol type for token endpoint: expected {expectedprotocoltype} but found {protocoltype}",
                 _validatedrequest.client.clientid,
                 identityserverconstants.protocoltypes.openidconnect,
                 _validatedrequest.client.protocoltype);
        return invalid(oidcconstants.tokenerrors.invalidclient);
    }

    /////////////////////////////////////////////
    // check grant type
    /////////////////////////////////////////////
    var granttype = parameters.get(oidcconstants.tokenrequest.granttype);
    if (granttype.ismissing())
    {
        logerror("grant type is missing");
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    if (granttype.length > _options.inputlengthrestrictions.granttype)
    {
        logerror("grant type is too long");
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    _validatedrequest.granttype = granttype;

    switch (granttype)
    {
        case oidcconstants.granttypes.authorizationcode:
            return await runvalidationasync(validateauthorizationcoderequestasync, parameters);
        case oidcconstants.granttypes.clientcredentials:
            return await runvalidationasync(validateclientcredentialsrequestasync, parameters);
        case oidcconstants.granttypes.password:
            return await runvalidationasync(validateresourceownercredentialrequestasync, parameters);
        case oidcconstants.granttypes.refreshtoken:
            return await runvalidationasync(validaterefreshtokenrequestasync, parameters);
        default://统一的自定义的验证方式
            return await runvalidationasync(validateextensiongrantrequestasync, parameters);
    }
}

从上面代码可以看出,除了内置的授权方式,其他的都是用validateextensiongrantrequestasync来进行验证,详细的验证规则继续分析实现过程。

private async task<tokenrequestvalidationresult> validateextensiongrantrequestasync(namevaluecollection parameters)
{
    _logger.logdebug("start validation of custom grant token request");

    /////////////////////////////////////////////
    // 校验客户端是否开启了此授权方式
    /////////////////////////////////////////////
    if (!_validatedrequest.client.allowedgranttypes.contains(_validatedrequest.granttype))
    {
        logerror("{clientid} does not have the custom grant type in the allowed list, therefore requested grant is not allowed", _validatedrequest.client.clientid);
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    /////////////////////////////////////////////
    // 判断是否注入了此自定义的授权实现
    /////////////////////////////////////////////
    if (!_extensiongrantvalidator.getavailablegranttypes().contains(_validatedrequest.granttype, stringcomparer.ordinal))
    {
        logerror("no validator is registered for the grant type: {granttype}", _validatedrequest.granttype);
        return invalid(oidcconstants.tokenerrors.unsupportedgranttype);
    }

    /////////////////////////////////////////////
    // 校验是否支持scope
    /////////////////////////////////////////////
    if (!await validaterequestedscopesasync(parameters))
    {
        return invalid(oidcconstants.tokenerrors.invalidscope);
    }

    /////////////////////////////////////////////
    // 调用自定义的验证实现方法
    /////////////////////////////////////////////
    var result = await _extensiongrantvalidator.validateasync(_validatedrequest);

    if (result == null)
    {
        logerror("invalid extension grant");
        return invalid(oidcconstants.tokenerrors.invalidgrant);
    }

    if (result.iserror)
    {
        if (result.error.ispresent())
        {
            logerror("invalid extension grant: {error}", result.error);
            return invalid(result.error, result.errordescription, result.customresponse);
        }
        else
        {
            logerror("invalid extension grant");
            return invalid(oidcconstants.tokenerrors.invalidgrant, customresponse: result.customresponse);
        }
    }

    if (result.subject != null)
    {
        /////////////////////////////////////////////
        // 判断当前的用户是否可用
        /////////////////////////////////////////////
        var isactivectx = new isactivecontext(
            result.subject,
            _validatedrequest.client,
            identityserverconstants.profileisactivecallers.extensiongrantvalidation);

        await _profile.isactiveasync(isactivectx);

        if (isactivectx.isactive == false)
        {
            // todo: raise event?

            logerror("user has been disabled: {subjectid}", result.subject.getsubjectid());
            return invalid(oidcconstants.tokenerrors.invalidgrant);
        }

        _validatedrequest.subject = result.subject;
    }

    _logger.logdebug("validation of extension grant token request success");
    return valid(result.customresponse);
}

从代码中可以看出,实现流程如下:

  • 1、客户端是否配置了自定义的授权方式。
  • 2、是否注入了自定义的授权实现。
  • 3、授权的scope客户端是否有权限。
  • 4、使用自定义的授权验证方式校验请求数据是否合法。
  • 5、判断是否有有效数据信息,可自行实现接口。

从源码中,可以发现流程已经非常清晰了,核心类extensiongrantvalidator实现了自定义授权的校验过程,进一步分析下此类的代码实现。

using identityserver4.models;
using microsoft.extensions.logging;
using system;
using system.collections.generic;
using system.linq;
using system.threading.tasks;

namespace identityserver4.validation
{
    /// <summary>
    /// validates an extension grant request using the registered validators
    /// </summary>
    public class extensiongrantvalidator
    {
        private readonly ilogger _logger;
        private readonly ienumerable<iextensiongrantvalidator> _validators;

        /// <summary>
        /// initializes a new instance of the <see cref="extensiongrantvalidator"/> class.
        /// </summary>
        /// <param name="validators">the validators.</param>
        /// <param name="logger">the logger.</param>
        public extensiongrantvalidator(ienumerable<iextensiongrantvalidator> validators, ilogger<extensiongrantvalidator> logger)
        {
            if (validators == null)
            {
                _validators = enumerable.empty<iextensiongrantvalidator>();
            }
            else
            {
                _validators = validators;
            }

            _logger = logger;
        }

        /// <summary>
        /// gets the available grant types.
        /// </summary>
        /// <returns></returns>
        public ienumerable<string> getavailablegranttypes()
        {
            return _validators.select(v => v.granttype);
        }

        /// <summary>
        /// validates the request.
        /// </summary>
        /// <param name="request">the request.</param>
        /// <returns></returns>
        public async task<grantvalidationresult> validateasync(validatedtokenrequest request)
        {
            var validator = _validators.firstordefault(v => v.granttype.equals(request.granttype, stringcomparison.ordinal));

            if (validator == null)
            {
                _logger.logerror("no validator found for grant type");
                return new grantvalidationresult(tokenrequesterrors.unsupportedgranttype);
            }

            try
            {
                _logger.logtrace("calling into custom grant validator: {type}", validator.gettype().fullname);

                var context = new extensiongrantvalidationcontext
                {
                    request = request
                };
            
                await validator.validateasync(context);
                return context.result;
            }
            catch (exception e)
            {
                _logger.logerror(1, e, "grant validation error: {message}", e.message);
                return new grantvalidationresult(tokenrequesterrors.invalidgrant);
            }
        }
    }
}

从上面代码可以发现,自定义授权方式,只需要实现iextensiongrantvalidator接口即可,然后支持多个自定义授权方式的共同使用。

到此整个验证过程解析完毕了,然后再查看下生成token流程,实现方法为tokenresponsegenerator,这个方法并不陌生,前几篇介绍不同的授权方式都介绍了,所以直接看实现代码。

public virtual async task<tokenresponse> processasync(tokenrequestvalidationresult request)
{
    switch (request.validatedrequest.granttype)
    {
        case oidcconstants.granttypes.clientcredentials:
            return await processclientcredentialsrequestasync(request);
        case oidcconstants.granttypes.password:
            return await processpasswordrequestasync(request);
        case oidcconstants.granttypes.authorizationcode:
            return await processauthorizationcoderequestasync(request);
        case oidcconstants.granttypes.refreshtoken:
            return await processrefreshtokenrequestasync(request);
        default://自定义授权生成token的方式
            return await processextensiongrantrequestasync(request);
    }
}

protected virtual task<tokenresponse> processextensiongrantrequestasync(tokenrequestvalidationresult request)
{
    logger.logtrace("creating response for extension grant request");
    return processtokenrequestasync(request);
}

实现的代码方式和客户端模式及密码模式一样,这里就不多介绍了。

最后我们查看下是如何注入iextensiongrantvalidator,是否对外提供接入方式,发现identityserver4提供了addextensiongrantvalidator扩展方法,我们自己实现自定义授权后添加即可,详细实现代码如下。

public static iidentityserverbuilder addextensiongrantvalidator<t>(this iidentityserverbuilder builder)
            where t : class, iextensiongrantvalidator
        {
            builder.services.addtransient<iextensiongrantvalidator, t>();
            return builder;
        }

二、自定义授权实现

现在开始开发第一个自定义授权方式,granttype定义为czarcustomuser,然后实现iextensiongrantvalidator接口,为了演示方便,我新建一个测试用户表,用来模拟老系统的登录方式。

create table czarcustomuser
(
    iid int identity,
    username varchar(50),
    usertruename varchar(50),
    userpwd varchar(100)
)
--插入测试用户密码信息,测试数据密码不加密
insert into czarcustomuser values('jinyancao','金焰的世界','777777')

然后把实现验证的方法,由于代码太简单,我就直接贴代码如下。

namespace czar.authplatform.web.application.irepository
{
    public interface iczarcustomuserrepository
    {
        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        czarcustomuser finduserbyuaccount(string uaccount, string upassword);
    }
}

namespace czar.authplatform.web.application.repository
{
    public class czarcustomuserrepository : iczarcustomuserrepository
    {
        private readonly string dbconn = "";
        public czarcustomuserrepository(ioptions<czarconfig> czarconfig)
        {
            dbconn = czarconfig.value.dbconnectionstrings;
        }

        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        public czarcustomuser finduserbyuaccount(string uaccount, string upassword)
        {
            using (var connection = new sqlconnection(dbconn))
            {
                string sql = @"select * from czarcustomuser where username=@uaccount and userpwd=upassword ";
                var result = connection.queryfirstordefault<czarcustomuser>(sql, new { uaccount, upassword });
                return result;
            }
        }
    }
}

namespace czar.authplatform.web.application.iservices
{
    public interface iczarcustomuserservices
    {
        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        czarcustomuser finduserbyuaccount(string uaccount, string upassword);
    }
}

namespace czar.authplatform.web.application.services
{
    public class czarcustomuserservices: iczarcustomuserservices
    {
        private readonly iczarcustomuserrepository czarcustomuserrepository;
        public czarcustomuserservices(iczarcustomuserrepository czarcustomuserrepository)
        {
            this.czarcustomuserrepository = czarcustomuserrepository;
        }

        /// <summary>
        /// 根据账号密码获取用户实体
        /// </summary>
        /// <param name="uaccount">账号</param>
        /// <param name="upassword">密码</param>
        /// <returns></returns>
        public czarcustomuser finduserbyuaccount(string uaccount, string upassword)
        {
            return czarcustomuserrepository.finduserbyuaccount(uaccount, upassword);
        }
    }
}

现在可以定义自定义的授权类型了,我起名为czarcustomusergrantvalidator,实现代码如下。

using czar.authplatform.web.application.iservices;
using identityserver4.models;
using identityserver4.validation;
using system.threading.tasks;

namespace czar.authplatform.web.application.ids4
{
    /// <summary>
    /// 金焰的世界
    /// 2019-01-28
    /// 自定义用户授权
    /// </summary>
    public class czarcustomusergrantvalidator : iextensiongrantvalidator
    {
        public string granttype => "czarcustomuser";

        private readonly iczarcustomuserservices czarcustomuserservices;

        public czarcustomusergrantvalidator(iczarcustomuserservices czarcustomuserservices)
        {
            this.czarcustomuserservices = czarcustomuserservices;
        }

        public task validateasync(extensiongrantvalidationcontext context)
        {
            var username = context.request.raw.get("czar_name");
            var userpassword = context.request.raw.get("czar_password");

            if (string.isnullorempty(username) || string.isnullorempty(userpassword))
            {
                context.result = new grantvalidationresult(tokenrequesterrors.invalidgrant);
            }
            //校验登录
            var result = czarcustomuserservices.finduserbyuaccount(username, userpassword);
            if (result==null)
            {
                context.result = new grantvalidationresult(tokenrequesterrors.invalidgrant);
            }
            //添加指定的claims
            context.result = new grantvalidationresult(
                         subject: result.iid.tostring(),
                         authenticationmethod: granttype,
                         claims: result.claims);
            return task.completedtask;
        }
    }
}

这就实现了自定义授权的功能,是不是很简单呢?然后添加此扩展方法。

services.addidentityserver(option =>
            {
                option.publicorigin = configuration["czarconfig:publicorigin"];
            })
                .adddevelopersigningcredential()
                .adddapperstore(option =>
                {
                    option.dbconnectionstrings = configuration["czarconfig:dbconnectionstrings"];
                })
                .addresourceownervalidator<czarresourceownerpasswordvalidator>()
                .addprofileservice<czarprofileservice>()
                .addsecretvalidator<jwtsecretvalidator>()
                //添加自定义授权
                .addextensiongrantvalidator<czarcustomusergrantvalidator>();

现在是不是就可以使用自定义授权的方式了呢?打开postman测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。
【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

三、客户端权限配置

在使用identityserver4时我们一定要理解整个验证流程。根据这次配置,我再梳理下流程如下:

  • 1、校验客户端client_id和client_secret。
  • 2、校验客户端是否有当前的授权方式。
  • 3、校验是否有请求scope权限。
  • 4、如果非客户端验证,校验账号密码或自定义规则是否正确。
  • 5、非客户端验证,校验授权信息是否有效。

通过此流程会发现我们缺少授权方式配置,所以请求时提示上面的提示,既然知道原因了,那就很简单的来实现,添加客户端自定义授权模式。此信息是在clientgranttypes表中,字段为客户端id和授权方式。我测试的客户端id为21,授权方式为czarcustomuser,那直接使用sql语句插入关系,然后再测试。

insert into clientgranttypes values(21,'czarcustomuser');

【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。
【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

显示的信息和我们定义的信息相同,而且可以通过amr来区分授权类型,不同的业务系统使用不同的认证方式,然后统一集成到认证平台即可。

四、总结与思考

本篇我介绍了自定义授权方式,从源码解析到最后的实现详细讲解了实现原理,并使用测试的用户来实现自定义的认证流程,本篇涉及的知识点不多,但是非常重要,因为我们在使用统一身份认证时经常会遇到多种认证方式的结合,和多套不同应用用户的使用,在掌握了授权原理后,就能在不同的授权方式中切换的游刃有余。

思考下,有了这些知识后,关于短信验证码登录和扫码登录是不是有心理有底了呢?如果自己实现这类登录应该都知道从哪里下手了吧。

下篇我将介绍常用登录的短信验证码授权方式,尽情期待吧。