上篇文章介绍了基于
Ids4
客户端受权的原理及如何实现自定义的客户端受权,并配合网关实现了统一的受权异常返回值和权限配置等相关功能,本篇将介绍密码受权模式,从使用场景、源码剖析到具体实现详细讲解密码受权模式的相关应用。javascript.netcore项目实战交流群(637326624),有兴趣的朋友能够在群里交流讨论。java
因为密码受权模式须要用户在业务系统输入帐号密码,为了安全起见,对于使用密码模式的业务系统,咱们认为是绝对可靠的,不存在泄漏用户名和密码的风险,因此使用场景定位为公司内部系统或集团内部系统或公司内部app等内部应用,非内部应用,尽可能不要开启密码受权模式,防止用户帐户泄漏。sql
在咱们使用密码受权模式以前,咱们须要理解密码模式是如何实现的,在上一篇中,我介绍了客户端受权的实现及源码剖析,相信咱们已经对Ids4
客户端受权已经熟悉,今天继续分析密码模式是如何获取到令牌的。数据库
Ids4
的全部受权都在TokenEndpoint
方法中,密码模式受权也是先校验客户端受权,若是客户端校验失败,直接返回删除信息,若是客户端校验成功,继续校验用户名和密码,详细实现代码以下。json
一、校验是否存在grantType
,而后根据不一样的类型启用不一样的校验方式。c#
// 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
是否存在,而后校验用户名密码是否准确,最后校验用户是否可用。安全
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); }
三、运行自定义上下文验证app
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校验时检查。less
四、最终生成Tokenasync
根据不一样的受权模式,生成不一样的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 Proce***efreshTokenRequestAsync(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方式,是否是都关联起来了。
至此,咱们整个密码受权模式所有讲解完成,相信你们跟我同样彻底掌握了受权的整个流程,若是须要持久化如何进行持久化流程。
理解了完整的密码受权模式流程后,使用自定义的用户体系就驾轻就熟了,下面就开始完整的实现自定义账户受权。
为了演示方便,我这里就设计简单的用户账户信息,做为自定义的哦账户基础,若是正式环境中使用,请根据各自业务使用各自的账户体系便可。
-- 建立用户表 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
呢?经过前面的介绍,咱们须要增长scope
为offline_access
,而且须要设置客户端支持,所以AllowOfflineAccess
属性须要设置为True
,如今来测试下获取的受权结果。
最终完成了refresh_token
的获取,至此整个密码受权模式所有讲解并实现完成。
本篇文章咱们从密码受权模式使用场景、源码剖析、自定义用户受权来说解了密码受权模式的详细思路和代码实现,从中不难发现Ids4
设计的巧妙,在默认实现的同时也预留了不少自定义扩展,本篇的自定义用户体系也是从新实现接口而后注入就完成集成工做。本篇主要难点就是要理解Ids4
的实现思路和数据库的相关配置,但愿经过本篇的讲解让咱们熟练掌握密码验证的流程,便于应用到实际生产环境。
上篇的客户端受权模式和本篇的密码受权模式都讲解完可能有人会存在如下几个疑问。
下篇文章我将会从这3个疑问出发,来详细讲解下这三个问题的实现思路和代码。