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


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

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

.netcore項目實戰交流群(637326624),有興趣的朋友可以在群里交流討論。

一、使用場景?

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

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

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

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

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

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

// TokenRequestValidator.cs
public async Task 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`是否存在,然后校驗用戶名密碼是否准確,最后校驗用戶是否可用。

```c#
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 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); 

四、測試密碼授權模式

注意:測試密碼授權模式之前,我們需要對測試的客戶端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個疑問出發,來詳細講解下這三個問題的實現思路和代碼。


免責聲明!

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



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