【.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來區分授權類型,不同的業務系統使用不同的認證方式,然后統一集成到認證平台即可。

四、總結與思考

本篇我介紹了自定義授權方式,從源碼解析到最后的實現詳細講解了實現原理,並使用測試的用戶來實現自定義的認證流程,本篇涉及的知識點不多,但是非常重要,因為我們在使用統一身份認證時經常會遇到多種認證方式的結合,和多套不同應用用戶的使用,在掌握了授權原理后,就能在不同的授權方式中切換的游刃有余。

思考下,有了這些知識后,關於短信驗證碼登錄和掃碼登錄是不是有心理有底了呢?如果自己實現這類登錄應該都知道從哪里下手了吧。

下篇我將介紹常用登錄的短信驗證碼授權方式,盡情期待吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM