" 【.NET Core項目實戰 統一認證平臺】開篇及目錄索引 " 上篇文章介紹了基於 客戶端授權的原理及如何實現自定義的客戶端授權,並配合網關實現了統一的授權異常返回值和許可權配置等相關功能,本篇將介紹密碼授權模式,從使用場景、源碼剖析到具體實現詳細講解密碼授權模式的相關應用。 .netcore項目 ...
【.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 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','[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
呢?通過前面的介紹,我們需要增加scope
為offline_access
,並且需要設置客戶端支持,因此AllowOfflineAccess
屬性需要設置為True
,現在來測試下獲取的授權結果。
最終完成了refresh_token
的獲取,至此整個密碼授權模式全部講解並實現完成。
五、總結及思考
本篇文章我們從密碼授權模式使用場景、源碼剖析、自定義用戶授權來講解了密碼授權模式的詳細思路和代碼實現,從中不難發現Ids4
設計的巧妙,在預設實現的同時也預留了很多自定義擴展,本篇的自定義用戶體系也是重新實現介面然後註入就完成集成工作。本篇主要難點就是要理解Ids4
的實現思路和資料庫的相關配置,希望通過本篇的講解讓我們熟練掌握密碼驗證的流程,便於應用到實際生產環境。
上篇的客戶端授權模式和本篇的密碼授權模式都講解完可能有人會存在以下幾個疑問。
- 1、如何校驗令牌信息的有效性?
- 2、如何強制有效令牌過期?
- 3、如何實現單機登錄?
下篇文章我將會從這3個疑問出發,來詳細講解下這三個問題的實現思路和代碼。