【.NET Core項目實戰-統一認證平臺】第十一章 受權篇-密碼受權模式

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

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

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

1、使用場景?

因爲密碼受權模式須要用戶在業務系統輸入帳號密碼,爲了安全起見,對於使用密碼模式的業務系統,咱們認爲是絕對可靠的,不存在泄漏用戶名和密碼的風險,因此使用場景定位爲公司內部系統或集團內部系統或公司內部app等內部應用,非內部應用,儘可能不要開啓密碼受權模式,防止用戶帳戶泄漏。數據庫

  • 這種模式適用於用戶對應用程序高度信任的狀況。好比是用戶系統的一部分。

2、Ids4密碼模式的默認實現剖析

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

Ids4的全部受權都在TokenEndpoint方法中,密碼模式受權也是先校驗客戶端受權,若是客戶端校驗失敗,直接返回刪除信息,若是客戶端校驗成功,繼續校驗用戶名和密碼,詳細實現代碼以下。c#

  • 一、校驗是否存在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:  //一、密碼受權模式調用方法
              return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
          case OidcConstants.GrantTypes.RefreshToken:
              return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
          default:
              return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
      }
    }
  • 二、啓用密碼受權模式校驗規則,首先校驗傳輸的參數和scope是否存在,而後校驗用戶名密碼是否準確,最後校驗用戶是否可用。app

    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);
    }
  • 三、運行自定義上下文驗證async

    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校驗時檢查。ide

  • 四、最終生成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);
    }
  • 五、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方式,是否是都關聯起來了。

    至此,咱們整個密碼受權模式所有講解完成,相信你們跟我同樣徹底掌握了受權的整個流程,若是須要持久化如何進行持久化流程。

理解了完整的密碼受權模式流程後,使用自定義的用戶體系就駕輕就熟了,下面就開始完整的實現自定義賬戶受權。

3、設計自定義的帳戶信息並應用

爲了演示方便,我這裏就設計簡單的用戶賬戶信息,做爲自定義的哦賬戶基礎,若是正式環境中使用,請根據各自業務使用各自的賬戶體系便可。

-- 建立用戶表
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 uAccount=@uaccount 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 uid=@uid";
                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','541869544@qq.com',1);

4、測試密碼受權模式

注意:測試密碼受權模式以前,咱們須要對測試的客戶端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的獲取,至此整個密碼受權模式所有講解並實現完成。

5、總結及思考

本篇文章咱們從密碼受權模式使用場景、源碼剖析、自定義用戶受權來說解了密碼受權模式的詳細思路和代碼實現,從中不難發現Ids4設計的巧妙,在默認實現的同時也預留了不少自定義擴展,本篇的自定義用戶體系也是從新實現接口而後注入就完成集成工做。本篇主要難點就是要理解Ids4的實現思路和數據庫的相關配置,但願經過本篇的講解讓咱們熟練掌握密碼驗證的流程,便於應用到實際生產環境。

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

  • 一、如何校驗令牌信息的有效性?
  • 二、如何強制有效令牌過時?
  • 三、如何實現單機登陸?

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

相關文章
相關標籤/搜索