【.NET Core項目實戰-統一認證平臺】第十四章 受權篇-自定義受權方式

【.NET Core項目實戰-統一認證平臺】開篇及目錄索引

上篇文章我介紹瞭如何強制令牌過時的實現,相信你們對IdentityServer4的驗證流程有了更深的瞭解,本篇我將介紹如何使用自定義的受權方式集成老的業務系統驗證,而後根據不一樣的客戶端使用不一樣的認證方式來集成到統一認證平臺。html

.netcore項目實戰交流羣(637326624),有興趣的朋友能夠在羣裏交流討論。sql

1、自定受權源碼剖析

當咱們須要使用開源項目的某些功能時,最好了解實現的原理,才能正確和熟練使用功能,避免出現各類未知bug問題和出現問題沒法解決的被動場面。c#

在使用此功能前,咱們須要瞭解完整的實現流程,下面我將從源碼開始講解IdentityServer4是如何實現自定義的受權方式。app

從我以前的文章中咱們知道受權方式是經過Grant_Type的值來判斷的,因此咱們自定義的受權方式,也是經過此值來區分,因此須要瞭解自定義的值處理流程。TokenRequestValidator是請求驗證的方法,除了常規驗證外,還增長了自定義的驗證方式。async

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來進行驗證,詳細的驗證規則繼續分析實現過程。ide

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);
}

從代碼中能夠看出,實現流程以下:測試

  • 一、客戶端是否配置了自定義的受權方式。
  • 二、是否注入了自定義的受權實現。
  • 三、受權的scope客戶端是否有權限。
  • 四、使用自定義的受權驗證方式校驗請求數據是否合法。
  • 五、判斷是否有有效數據信息,可自行實現接口。

從源碼中,能夠發現流程已經很是清晰了,核心類ExtensionGrantValidator實現了自定義受權的校驗過程,進一步分析下此類的代碼實現。ui

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接口便可,而後支持多個自定義受權方式的共同使用。this

到此整個驗證過程解析完畢了,而後再查看下生成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;
        }

2、自定義受權實現

如今開始開發第一個自定義受權方式,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測試,按照源碼解析和設計參數,測試信息以下,發現報錯,原來是還未配置好客戶端訪問權限,開啓權限測試以下。

3、客戶端權限配置

在使用IdentityServer4時咱們必定要理解整個驗證流程。根據此次配置,我再梳理下流程以下:

  • 一、校驗客戶端client_id和Client_Secret。
  • 二、校驗客戶端是否有當前的受權方式。
  • 三、校驗是否有請求scope權限。
  • 四、若是非客戶端驗證,校驗帳號密碼或自定義規則是否正確。
  • 五、非客戶端驗證,校驗受權信息是否有效。

經過此流程會發現咱們缺乏受權方式配置,因此請求時提示上面的提示,既然知道緣由了,那就很簡單的來實現,添加客戶端自定義受權模式。此信息是在ClientGrantTypes表中,字段爲客戶端ID和受權方式。我測試的客戶端ID爲21,受權方式爲CzarCustomUser,那直接使用SQL語句插入關係,而後再測試。

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

發現能夠獲取到預期結果,而後查看access_token是什麼內容,顯示以下。

顯示的信息和咱們定義的信息相同,並且能夠經過amr來區分受權類型,不一樣的業務系統使用不一樣的認證方式,而後統一集成到認證平臺便可。

4、總結與思考

本篇我介紹了自定義受權方式,從源碼解析到最後的實現詳細講解了實現原理,並使用測試的用戶來實現自定義的認證流程,本篇涉及的知識點很少,可是很是重要,由於咱們在使用統一身份認證時常常會遇到多種認證方式的結合,和多套不一樣應用用戶的使用,在掌握了受權原理後,就能在不一樣的受權方式中切換的遊刃有餘。

思考下,有了這些知識後,關於短信驗證碼登陸和掃碼登陸是否是有心理有底了呢?若是本身實現這類登陸應該都知道從哪裏下手了吧。

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

相關文章
相關標籤/搜索