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


    【.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测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。

    三、客户端权限配置

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

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

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

    INSERT INTO ClientGrantTypes VALUES(21,'CzarCustomUser');
    

    发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。

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

    四、总结与思考

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

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

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

  • 相关阅读:
    Linux内核源码分析方法
    OVS处理upcall流程分析
    Linux内核源码目录结构分析
    理解OpenStack中的OpenvSwitch的几个要点
    OVS源码connmgr_run分析
    ovs-appctl 命令合集
    云计算底层技术-使用openvswitch
    OVS架构
    Open vSwitch Datapath浅析
    Openvswitch原理与代码分析(4):网络包的处理过程
  • 原文地址:https://www.cnblogs.com/jackcao/p/10330397.html
Copyright © 2020-2023  润新知