1. 程式人生 > >【.NET Core專案實戰-統一認證平臺】第十一章 授權篇-密碼授權模式

【.NET Core專案實戰-統一認證平臺】第十一章 授權篇-密碼授權模式

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

上篇文章介紹了基於Ids4客戶端授權的原理及如何實現自定義的客戶端授權,並配合閘道器實現了統一的授權異常返回值和許可權配置等相關功能,本篇將介紹密碼授權模式,從使用場景、原始碼剖析到具體實現詳細講解密碼授權模式的相關應用。

.netcore專案實戰交流群(637326624),有興趣的朋友可以在群裡交流討論。

一、使用場景?

由於密碼授權模式需要使用者在業務系統輸入賬號密碼,為了安全起見,對於使用密碼模式的業務系統,我們認為是絕對可靠的,不存在洩漏使用者名稱和密碼的風險,所以使用場景定位為公司內部系統或集團內部系統或公司內部app等內部應用,非內部應用,儘量不要開啟密碼授權模式,防止使用者賬戶洩漏。

  • 這種模式適用於使用者對應用程式高度信任的情況。比如是使用者系統的一部分。

二、Ids4密碼模式的預設實現剖析

在我們使用密碼授權模式之前,我們需要理解密碼模式是如何實現的,在上一篇中,我介紹了客戶端授權的實現及原始碼剖析,相信我們已經對Ids4客戶端授權已經熟悉,今天繼續分析密碼模式是如何獲取到令牌的。

Ids4的所有授權都在TokenEndpoint方法中,密碼模式授權也是先校驗客戶端授權,如果客戶端校驗失敗,直接返回刪除資訊,如果客戶端校驗成功,繼續校驗使用者名稱和密碼,詳細實現程式碼如下。

  • 1、校驗是否存在grantType,然後根據不同的型別啟用不同的校驗方式。

    // TokenRequestValidator.cs
    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:  //1、密碼授權模式呼叫方法
              return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
          case OidcConstants.GrantTypes.RefreshToken:
              return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
          default:
              return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
      }
    }
  • 2、啟用密碼授權模式校驗規則,首先校驗傳輸的引數和scope是否存在,然後校驗使用者名稱密碼是否準確,最後校驗使用者是否可用。

    private async Task<TokenRequestValidationResult> ValidateResourceOwnerCredentialRequestAsync(NameValueCollection parameters)
    {
        _logger.LogDebug("Start resource owner password token request validation");
    
        /////////////////////////////////////////////
        // 校驗授權模式
        /////////////////////////////////////////////
        if (!_validatedRequest.Client.AllowedGrantTypes.Contains(GrantType.ResourceOwnerPassword))
        {
            LogError("{clientId} not authorized for resource owner flow, check the AllowedGrantTypes of client", _validatedRequest.Client.ClientId);
            return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
        }
    
        /////////////////////////////////////////////
        // 校驗客戶端是否允許這些scope
        /////////////////////////////////////////////
        if (!(await ValidateRequestedScopesAsync(parameters)))
        {
            return Invalid(OidcConstants.TokenErrors.InvalidScope);
        }
    
        /////////////////////////////////////////////
        // 校驗引數是否為定義的使用者名稱或密碼引數
        /////////////////////////////////////////////
        var userName = parameters.Get(OidcConstants.TokenRequest.UserName);
        var password = parameters.Get(OidcConstants.TokenRequest.Password);
    
        if (userName.IsMissing() || password.IsMissing())
        {
            LogError("Username or password missing");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }
    
        if (userName.Length > _options.InputLengthRestrictions.UserName ||
            password.Length > _options.InputLengthRestrictions.Password)
        {
            LogError("Username or password too long");
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }
    
        _validatedRequest.UserName = userName;
    
    
        /////////////////////////////////////////////
        // 校驗使用者名稱和密碼是否準確
        /////////////////////////////////////////////
        var resourceOwnerContext = new ResourceOwnerPasswordValidationContext
        {
            UserName = userName,
            Password = password,
            Request = _validatedRequest
        };
        //預設使用的是 TestUserResourceOwnerPasswordValidator
        await _resourceOwnerValidator.ValidateAsync(resourceOwnerContext);
    
        if (resourceOwnerContext.Result.IsError)
        {
            if (resourceOwnerContext.Result.Error == OidcConstants.TokenErrors.UnsupportedGrantType)
            {
                LogError("Resource owner password credential grant type not supported");
                await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "password grant type not supported");
    
                return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType, customResponse: resourceOwnerContext.Result.CustomResponse);
            }
    
            var errorDescription = "invalid_username_or_password";
    
            if (resourceOwnerContext.Result.ErrorDescription.IsPresent())
            {
                errorDescription = resourceOwnerContext.Result.ErrorDescription;
            }
    
            LogInfo("User authentication failed: {error}", errorDescription ?? resourceOwnerContext.Result.Error);
            await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, errorDescription);
    
            return Invalid(resourceOwnerContext.Result.Error, errorDescription, resourceOwnerContext.Result.CustomResponse);
        }
    
        if (resourceOwnerContext.Result.Subject == null)
        {
            var error = "User authentication failed: no principal returned";
            LogError(error);
            await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, error);
    
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }
    
        /////////////////////////////////////////////
        // 設定使用者可用,比如使用者授權後被鎖定,可以通過此方法實現 預設實現 TestUserProfileService
        /////////////////////////////////////////////
        var isActiveCtx = new IsActiveContext(resourceOwnerContext.Result.Subject, _validatedRequest.Client, IdentityServerConstants.ProfileIsActiveCallers.ResourceOwnerValidation);
        await _profile.IsActiveAsync(isActiveCtx);
    
        if (isActiveCtx.IsActive == false)
        {
            LogError("User has been disabled: {subjectId}", resourceOwnerContext.Result.Subject.GetSubjectId());
            await RaiseFailedResourceOwnerAuthenticationEventAsync(userName, "user is inactive");
    
            return Invalid(OidcConstants.TokenErrors.InvalidGrant);
        }
    
        _validatedRequest.UserName = userName;
        _validatedRequest.Subject = resourceOwnerContext.Result.Subject;
    
        await RaiseSuccessfulResourceOwnerAuthenticationEventAsync(userName, resourceOwnerContext.Result.Subject.GetSubjectId());
        _logger.LogDebug("Resource owner password token request validation success.");
        return Valid(resourceOwnerContext.Result.CustomResponse);
    }
  • 3、執行自定義上下文驗證

    private async Task<TokenRequestValidationResult> RunValidationAsync(Func<NameValueCollection, Task<TokenRequestValidationResult>> validationFunc, NameValueCollection parameters)
    {
        // 執行步驟2驗證
        var result = await validationFunc(parameters);
        if (result.IsError)
        {
            return result;
        }
    
        // 執行自定義驗證,Ids4 預設有個 DefaultCustomTokenRequestValidator 實現,如果需要擴充其他驗證,可以整合ICustomTokenRequestValidator單獨實現。
        _logger.LogTrace("Calling into custom request validator: {type}", _customRequestValidator.GetType().FullName);
    
        var customValidationContext = new CustomTokenRequestValidationContext { Result = result };
        await _customRequestValidator.ValidateAsync(customValidationContext);
    
        if (customValidationContext.Result.IsError)
        {
            if (customValidationContext.Result.Error.IsPresent())
            {
                LogError("Custom token request validator error {error}", customValidationContext.Result.Error);
            }
            else
            {
                LogError("Custom token request validator error");
            }
    
            return customValidationContext.Result;
        }
    
        LogSuccess();
        return customValidationContext.Result;
    }

    通過原始碼剖析可以發現,Ids4給了我們很多的驗證方式,並且預設也實現的驗證和自定義的擴充套件,這樣如果我們需要使用密碼授權模式,就可以重寫IResourceOwnerPasswordValidator來實現系統內部使用者系統的驗證需求。如果需要確認使用者在登入以後是否被登出時,可以重寫IProfileService介面實現,這個驗證主要是生成token校驗時檢查。

  • 4、最終生成Token

    根據不同的授權模式,生成不同的token記錄。

    /// <summary>
    /// Processes the response.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <returns></returns>
    public virtual async Task<TokenResponse> ProcessAsync(TokenRequestValidationResult request)
    {
        switch (request.ValidatedRequest.GrantType)
        {
            case OidcConstants.GrantTypes.ClientCredentials:
                return await ProcessClientCredentialsRequestAsync(request);
            case OidcConstants.GrantTypes.Password: //生成密碼授權模式token
                return await ProcessPasswordRequestAsync(request);
            case OidcConstants.GrantTypes.AuthorizationCode:
                return await ProcessAuthorizationCodeRequestAsync(request);
            case OidcConstants.GrantTypes.RefreshToken:
                return await ProcessRefreshTokenRequestAsync(request);
            default:
                return await ProcessExtensionGrantRequestAsync(request);
        }
    }
    
    /// <summary>
    /// Creates the response for a password request.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <returns></returns>
    protected virtual Task<TokenResponse> ProcessPasswordRequestAsync(TokenRequestValidationResult request)
    {
        Logger.LogTrace("Creating response for password request");
    
        return ProcessTokenRequestAsync(request);
    }
    
    /// <summary>
    /// Creates the response for a token request.
    /// </summary>
    /// <param name="validationResult">The validation result.</param>
    /// <returns></returns>
    protected virtual async Task<TokenResponse> ProcessTokenRequestAsync(TokenRequestValidationResult validationResult)
    {
        (var accessToken, var refreshToken) = await CreateAccessTokenAsync(validationResult.ValidatedRequest);
        var response = new TokenResponse
        {
            AccessToken = accessToken,
            AccessTokenLifetime = validationResult.ValidatedRequest.AccessTokenLifetime,
            Custom = validationResult.CustomResponse
        };
    
        if (refreshToken.IsPresent())
        {
            response.RefreshToken = refreshToken;
        }
    
        return response;
    }

    根據請求的scope判斷是否生成refreshToken,如果標記了offline_access,則生成refreshToken,否則不生成。

    /// <summary>
    /// Creates the access/refresh token.
    /// </summary>
    /// <param name="request">The request.</param>
    /// <returns></returns>
    /// <exception cref="System.InvalidOperationException">Client does not exist anymore.</exception>
    protected virtual async Task<(string accessToken, string refreshToken)> CreateAccessTokenAsync(ValidatedTokenRequest request)
    {
        TokenCreationRequest tokenRequest;
        bool createRefreshToken;
      //授權碼模式
        if (request.AuthorizationCode != null)
        {//是否包含RefreshToken
            createRefreshToken = request.AuthorizationCode.RequestedScopes.Contains(IdentityServerConstants.StandardScopes.OfflineAccess);
    
            // load the client that belongs to the authorization code
            Client client = null;
            if (request.AuthorizationCode.ClientId != null)
            {
                client = await Clients.FindEnabledClientByIdAsync(request.AuthorizationCode.ClientId);
            }
            if (client == null)
            {
                throw new InvalidOperationException("Client does not exist anymore.");
            }
    
            var resources = await Resources.FindEnabledResourcesByScopeAsync(request.AuthorizationCode.RequestedScopes);
    
            tokenRequest = new TokenCreationRequest
            {
                Subject = request.AuthorizationCode.Subject,
                Resources = resources,
                ValidatedRequest = request
            };
        }
        else
        {//是否包含RefreshToken
            createRefreshToken = request.ValidatedScopes.ContainsOfflineAccessScope;
    
            tokenRequest = new TokenCreationRequest
            {
                Subject = request.Subject,
                Resources = request.ValidatedScopes.GrantedResources,
                ValidatedRequest = request
            };
        }
    
        var at = await TokenService.CreateAccessTokenAsync(tokenRequest);
        var accessToken = await TokenService.CreateSecurityTokenAsync(at);
    
        if (createRefreshToken)
        {
            var refreshToken = await RefreshTokenService.CreateRefreshTokenAsync(tokenRequest.Subject, at, request.Client);
            return (accessToken, refreshToken);
        }
    
        return (accessToken, null);
    }
  • 5、RefreshToken持久化

    當我們使用了offline_access時,就需要生成RefreshToken並進行持久化,詳細的實現程式碼如下。

    public virtual async Task<string> CreateRefreshTokenAsync(ClaimsPrincipal subject, Token accessToken, Client client)
    {
        _logger.LogDebug("Creating refresh token");
    
        int lifetime;
        if (client.RefreshTokenExpiration == TokenExpiration.Absolute)
        {
            _logger.LogDebug("Setting an absolute lifetime: " + client.AbsoluteRefreshTokenLifetime);
            lifetime = client.AbsoluteRefreshTokenLifetime;
        }
        else
        {
            _logger.LogDebug("Setting a sliding lifetime: " + client.SlidingRefreshTokenLifetime);
            lifetime = client.SlidingRefreshTokenLifetime;
        }
    
        var refreshToken = new RefreshToken
        {
            CreationTime = Clock.UtcNow.UtcDateTime,
            Lifetime = lifetime,
            AccessToken = accessToken
        };
      //儲存RefreshToken並返回值
        var handle = await RefreshTokenStore.StoreRefreshTokenAsync(refreshToken);
        return handle;
    }
    
    /// <summary>
    /// 儲存RefreshToken並返回
    /// </summary>
    /// <param name="refreshToken">The refresh token.</param>
    /// <returns></returns>
    public async Task<string> StoreRefreshTokenAsync(RefreshToken refreshToken)
    {
        return await CreateItemAsync(refreshToken, refreshToken.ClientId, refreshToken.SubjectId, refreshToken.CreationTime, refreshToken.Lifetime);
    }
    
    /// <summary>
    /// 建立Item
    /// </summary>
    /// <param name="item">The item.</param>
    /// <param name="clientId">The client identifier.</param>
    /// <param name="subjectId">The subject identifier.</param>
    /// <param name="created">The created.</param>
    /// <param name="lifetime">The lifetime.</param>
    /// <returns></returns>
    protected virtual async Task<string> CreateItemAsync(T item, string clientId, string subjectId, DateTime created, int lifetime)
    {
        var handle = await HandleGenerationService.GenerateAsync(); //生成隨機值
        await StoreItemAsync(handle, item, clientId, subjectId, created, created.AddSeconds(lifetime)); //儲存
        return handle;
    }
    
    /// <summary>
    /// 儲存RefreshToken
    /// </summary>
    /// <param name="key">The key.</param>
    /// <param name="item">The item.</param>
    /// <param name="clientId">The client identifier.</param>
    /// <param name="subjectId">The subject identifier.</param>
    /// <param name="created">The created.</param>
    /// <param name="expiration">The expiration.</param>
    /// <returns></returns>
    protected virtual async Task StoreItemAsync(string key, T item, string clientId, string subjectId, DateTime created, DateTime? expiration)
    {
        key = GetHashedKey(key);
    
        var json = Serializer.Serialize(item);
    
        var grant = new PersistedGrant
        {
            Key = key,
            Type = GrantType,
            ClientId = clientId,
            SubjectId = subjectId,
            CreationTime = created,
            Expiration = expiration,
            Data = json
        };
    
        await Store.StoreAsync(grant);
    }
    
    //IPersistedGrantStore 我們在dapper持久化時已經實現了StoreAsync方式,是不是都關聯起來了。

    至此,我們整個密碼授權模式全部講解完成,相信大家跟我一樣完全掌握了授權的整個流程,如果需要持久化如何進行持久化流程。

理解了完整的密碼授權模式流程後,使用自定義的使用者體系就得心應手了,下面就開始完整的實現自定義帳戶授權。

三、設計自定義的賬戶資訊並應用

為了演示方便,我這裡就設計簡單的使用者帳戶資訊,作為自定義的哦帳戶基礎,如果正式環境中使用,請根據各自業務使用各自的帳戶體系即可。

-- 建立使用者表
CREATE TABLE CzarUsers
(
    Uid INT IDENTITY(1,1),            --使用者主鍵    
    uAccount varchar(11),             --使用者賬號
    uPassword varchar(200),           --使用者密碼
    uNickName varchar(50),            --使用者暱稱
    uMobile varchar(11),              --使用者手機號
    uEmail varchar(100),              --使用者郵箱
    uStatus int not null default(1)   -- 使用者狀態 1 正常 0 不可用
)

新增使用者實體程式碼如下所示。

/// <summary>
/// 授權使用者資訊
/// </summary>
public class CzarUsers
{
    public CzarUsers() { }

    public int Uid { get; set; }
    public string uAccount { get; set; }
    public string uPassword { get; set; }
    public string uNickName { get; set; }
    public string uMobile { get; set; }
    public string uEmail { get; set; }
    public string uStatus { get; set; }
}

下面開始密碼授權模式開發,首先需要重新實現IResourceOwnerPasswordValidator介面,使用我們定義的使用者表來驗證請求的使用者名稱和密碼資訊。

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 自定義使用者名稱密碼校驗
/// </summary>
public class CzarResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
    {
        private readonly ICzarUsersServices _czarUsersServices;
        public CzarResourceOwnerPasswordValidator(ICzarUsersServices czarUsersServices)
        {
            _czarUsersServices = czarUsersServices;
        }
        /// <summary>
        /// 驗證使用者身份
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
        {
            var user = _czarUsersServices.FindUserByuAccount(context.UserName, context.Password);
            if (user != null)
            {
                context.Result = new GrantValidationResult(
                    user.Uid.ToString(),
                    OidcConstants.AuthenticationMethods.Password, 
                    DateTime.UtcNow);
            }
            return Task.CompletedTask;
        }
    }

編寫完自定義校驗後,我們需要注入到具體的實現,詳細程式碼如下。

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(Configuration);
    services.Configure<CzarConfig>(Configuration.GetSection("CzarConfig"));
    services.AddIdentityServer(option=> {
        option.PublicOrigin = Configuration["CzarConfig:PublicOrigin"];
    })
        .AddDeveloperSigningCredential()
        .AddDapperStore(option =>
                        {
                            option.DbConnectionStrings = Configuration["CzarConfig:DbConnectionStrings"];
                        })
        //使用自定義的密碼校驗
        .AddResourceOwnerValidator<CzarResourceOwnerPasswordValidator>()
        ;
    //  .UseMySql();


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

剩下的就是把ICzarUsersServices介面實現並注入即可。詳細程式碼如下。

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 使用者服務介面
/// </summary>
public interface ICzarUsersServices
{
    /// <summary>
    /// 根據賬號密碼獲取使用者實體
    /// </summary>
    /// <param name="uaccount">賬號</param>
    /// <param name="upassword">密碼</param>
    /// <returns></returns>
    CzarUsers FindUserByuAccount(string uaccount, string upassword);

    /// <summary>
    /// 根據使用者主鍵獲取使用者實體
    /// </summary>
    /// <param name="sub">使用者標識</param>
    /// <returns></returns>
    CzarUsers FindUserByUid(string sub);
}

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 使用者服務實現
/// </summary>
public class CzarUsersServices : ICzarUsersServices
    {
        private readonly ICzarUsersRepository _czarUsersRepository;
        public CzarUsersServices(ICzarUsersRepository czarUsersRepository)
        {
            _czarUsersRepository = czarUsersRepository;
        }

        /// <summary>
        /// 根據賬號密碼獲取使用者實體
        /// </summary>
        /// <param name="uaccount">賬號</param>
        /// <param name="upassword">密碼</param>
        /// <returns></returns>
        public CzarUsers FindUserByuAccount(string uaccount, string upassword)
        {
            return _czarUsersRepository.FindUserByuAccount(uaccount, upassword);
        }

        /// <summary>
        /// 根據使用者主鍵獲取使用者實體
        /// </summary>
        /// <param name="sub">使用者標識</param>
        /// <returns></returns>
        public CzarUsers FindUserByUid(string sub)
        {
            return _czarUsersRepository.FindUserByUid(sub);
        }
    }

最後我們實現倉儲介面和方法,即可完成校驗流程。

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 使用者倉儲介面
/// </summary>
public interface ICzarUsersRepository
{
    /// <summary>
    /// 根據賬號密碼獲取使用者實體
    /// </summary>
    /// <param name="uaccount">賬號</param>
    /// <param name="upassword">密碼</param>
    /// <returns></returns>
    CzarUsers FindUserByuAccount(string uaccount, string upassword);

    /// <summary>
    /// 根據使用者主鍵獲取使用者實體
    /// </summary>
    /// <param name="sub">使用者標識</param>
    /// <returns></returns>
    CzarUsers FindUserByUid(string sub);
}

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 使用者實體基於SQLSERVER的實現
/// </summary>
public class CzarUsersRepository : ICzarUsersRepository
    {
        private readonly string DbConn = "";
        public CzarUsersRepository(IOptions<CzarConfig> czarConfig)
        {
            DbConn = czarConfig.Value.DbConnectionStrings;
        }
        /// <summary>
        /// 根據賬號密碼獲取使用者實體
        /// </summary>
        /// <param name="uaccount">賬號</param>
        /// <param name="upassword">密碼</param>
        /// <returns></returns>
        public CzarUsers FindUserByuAccount(string uaccount, string upassword)
        {
            using (var connection = new SqlConnection(DbConn))
            {
                string sql = @"SELECT * from CzarUsers where [email protected] and uPassword=upassword and uStatus=1";
                var result = connection.QueryFirstOrDefault<CzarUsers>(sql, new { uaccount, upassword = SecretHelper.ToMD5(upassword) });
                return result;
            }
        }

        /// <summary>
        /// 根據使用者主鍵獲取使用者實體
        /// </summary>
        /// <param name="sub">使用者標識</param>
        /// <returns></returns>
        public CzarUsers FindUserByUid(string sub)
        {
            using (var connection = new SqlConnection(DbConn))
            {
                string sql = @"SELECT * from CzarUsers where [email protected]";
                var result = connection.QueryFirstOrDefault<CzarUsers>(sql, new { uid=sub });
                return result;
            }
        }
    }

現在萬事俱備,之前注入和插入測試使用者資料進行測試了,為了方便注入,我們採用autofac程式集註冊。

/// <summary>
/// 金焰的世界
/// 2018-12-18
/// 使用程式集註冊
/// </summary>
public class CzarModule : Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            //註冊Repository程式集
            builder.RegisterAssemblyTypes(typeof(CzarUsersRepository).GetTypeInfo().Assembly).AsImplementedInterfaces().InstancePerLifetimeScope();
            //註冊Services程式集
            builder.RegisterAssemblyTypes(typeof(CzarUsersServices).GetTypeInfo().Assembly).AsImplementedInterfaces().InstancePerLifetimeScope();
        }
    }

然後需要修改ConfigureServices程式碼如下,就完成了倉儲和服務層的注入。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(Configuration);
    services.Configure<CzarConfig>(Configuration.GetSection("CzarConfig"));
    services.AddIdentityServer(option=> {
        option.PublicOrigin = Configuration["CzarConfig:PublicOrigin"];
    })
        .AddDeveloperSigningCredential()
        .AddDapperStore(option =>
                        {
                            option.DbConnectionStrings = Configuration["CzarConfig:DbConnectionStrings"];
                        })
        .AddResourceOwnerValidator<CzarResourceOwnerPasswordValidator>()
        ;
    //  .UseMySql();


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    //使用Autofac進行注入
    var container = new ContainerBuilder();
    container.RegisterModule(new CzarModule());
    container.Populate(services);
    return new AutofacServiceProvider(container.Build());
}

為了驗證密碼授權模式資訊,這裡需要往資料庫插入測試的使用者資料,插入指令碼如下。

--密碼123456  MD5加密結果
INSERT INTO CzarUsers VALUES('13888888888','E10ADC3949BA59ABBE56E057F20F883E','金焰的世界','13888888888','[email protected]',1); 

四、測試密碼授權模式

注意:測試密碼授權模式之前,我們需要對測試的客戶端ClientGrantTypes表新增password授權方式。

開啟我們的測試神器Postman,然後開始除錯密碼授權模式,測試結果如下圖所示。

是不是很完美,得到了我們想要的授權結果,那我們檢視下這個access_token是什麼資訊,可以使用https://jwt.io/檢視到詳細的內容,發現除了客戶端資訊和使用者主鍵無其他附加資訊,那如何新增自定義的Claim資訊呢?

先修改下CzarUsers實體,增加如下程式碼,如果有其他屬性可自行擴充套件。

public List<Claim> Claims
        {
            get
            {
                return new List<Claim>() {
                    new Claim("nickname",uNickName??""),
                    new Claim("email",uEmail??""),
                    new Claim("mobile",uMobile??"")
                };
            }
        }

再修改校驗方法,增加Claim輸出,CzarResourceOwnerPasswordValidator修改程式碼如下。

/// <summary>
/// 驗證使用者身份
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
    var user = _czarUsersServices.FindUserByuAccount(context.UserName, context.Password);
    if (user != null)
    {
        context.Result = new GrantValidationResult(
            user.Uid.ToString(),
            OidcConstants.AuthenticationMethods.Password, 
            DateTime.UtcNow,
            user.Claims);
    }
    return Task.CompletedTask;
}

然後需要把使用者的claims應用到Token,這裡我們需要重寫IProfileService,然後把使用者的claim輸出,實現程式碼如下。

public class CzarProfileService : IProfileService
    {
        public Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            //把使用者返回的Claims應用到返回
            context.IssuedClaims = context.Subject.Claims.ToList();
            return Task.CompletedTask;
        }

        /// <summary>
        /// 驗證使用者是否有效
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public Task IsActiveAsync(IsActiveContext context)
        {
            context.IsActive = true;
            return Task.CompletedTask;
        }
    }

然後別忘了注入.AddProfileService<CzarProfileService>(),好了現在我們再次測試下授權,最終得到的結果如下所示。

奈斯,得到了我們預期授權結果。

那如何獲取refresh_token呢?通過前面的介紹,我們需要增加scopeoffline_access,並且需要設定客戶端支援,因此AllowOfflineAccess屬性需要設定為True,現在來測試下獲取的授權結果。

最終完成了refresh_token的獲取,至此整個密碼授權模式全部講解並實現完成。

五、總結及思考

本篇文章我們從密碼授權模式使用場景、原始碼剖析、自定義使用者授權來講解了密碼授權模式的詳細思路和程式碼實現,從中不難發現Ids4設計的巧妙,在預設實現的同時也預留了很多自定義擴充套件,本篇的自定義使用者體系也是重新實現介面然後注入就完成整合工作。本篇主要難點就是要理解Ids4的實現思路和資料庫的相關配置,希望通過本篇的講解讓我們熟練掌握密碼驗證的流程,便於應用到實際生產環境。

上篇的客戶端授權模式和本篇的密碼授權模式都講解完可能有人會存在以下幾個疑問。

  • 1、如何校驗令牌資訊的有效性?
  • 2、如何強制有效令牌過期?
  • 3、如何實現單機登入?

下篇文章我將會從這3個疑問出發,來詳細講解下這三個問題的實現思路和程式碼。