【.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
呢?通过前面的介绍,我们需要增加scope
为offline_access
,并且需要设置客户端支持,因此AllowOfflineAccess
属性需要设置为True
,现在来测试下获取的授权结果。
最终完成了refresh_token
的获取,至此整个密码授权模式全部讲解并实现完成。
五、总结及思考
本篇文章我们从密码授权模式使用场景、源码剖析、自定义用户授权来讲解了密码授权模式的详细思路和代码实现,从中不难发现Ids4
设计的巧妙,在默认实现的同时也预留了很多自定义扩展,本篇的自定义用户体系也是重新实现接口然后注入就完成集成工作。本篇主要难点就是要理解Ids4
的实现思路和数据库的相关配置,希望通过本篇的讲解让我们熟练掌握密码验证的流程,便于应用到实际生产环境。
上篇的客户端授权模式和本篇的密码授权模式都讲解完可能有人会存在以下几个疑问。
- 1、如何校验令牌信息的有效性?
- 2、如何强制有效令牌过期?
- 3、如何实现单机登录?
下篇文章我将会从这3个疑问出发,来详细讲解下这三个问题的实现思路和代码。
【.NET Core项目实战-统一认证平台】第十一章 授权篇-密码授权模式的更多相关文章
- 【.NET Core项目实战-统一认证平台】第二章网关篇-定制Ocelot来满足需求
[.NET Core项目实战-统一认证平台]开篇及目录索引 这篇文章,我们将从Ocelot的中间件源码分析,目前Ocelot已经实现那些功能,还有那些功能在我们实际项目中暂时还未实现,如果我们要使用这 ...
- 【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了IdentityServer4的源码分析的内容,让我们知道了IdentityServer4的一些运行原理,这篇将介绍如何使用d ...
- 【.NET Core项目实战-统一认证平台】第一章 功能及架构分析
[.NET Core项目实战-统一认证平台]开篇及目录索引 从本文开始,我们正式进入项目研发阶段,首先我们分析下统一认证平台应该具备哪些功能性需求和非功能性需求,在梳理完这些需求后,设计好系统采用的架 ...
- 【.NET Core项目实战-统一认证平台】第三章 网关篇-数据库存储配置(1)
[.NET Core项目实战-统一认证平台]开篇及目录索引 本篇将介绍如何扩展Ocelot中间件实现自定义网关,并使用2种不同数据库来演示Ocelot配置信息存储和动态更新功能,内容也是从实际设计出发 ...
- 【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务
[.NET Core项目实战-统一认证平台]开篇及目录索引 一.什么是RPC RPC是"远程调用(Remote Procedure Call)"的一个名称的缩写,并不是任何规范化的 ...
- 【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能
[.NET Core项目实战-统一认证平台]开篇及目录索引 一.背景 首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2 ...
- 【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方 ...
- 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期
[.NET Core项目实战-统一认证平台]开篇及目录索引 上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. ...
- 【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4密码授权模式,从使用场景.原理分析.自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本 ...
随机推荐
- Android系统架构及内核简介
(来源于ThinkPHP) Android是Google公司开发的基于Linux平台的开源手机操作系统,它包括操作系统.中间件.用户界面和应用程序,而且不存在任何以往阻碍移 动产业创新的专利权障碍,并 ...
- vue组件里定时器销毁问题
我在a页面写一个定时,让他每秒钟打印一个1,然后跳转到b页面,此时可以看到,定时器依然在执行.这样是非常消耗性能的.如下图所示: 解决方法1: 首先我在data函数里面进行定义定时器名称: data( ...
- 菜鸡谈OO 第一单元总结
“OOP永远是我的好朋友爸爸!” ——来自某无能狂怒的菜鸡 身处在OO的第一个摸鱼黄金周中的我,感觉到了巨大的满足感.如果写博客这种充满意义的事情可以代替我们亲爱的作业,那么我提议每周来两个:)下面开 ...
- Bulk API
承接上文,使用Java High Level REST Client操作elasticsearch Bulk API 高级客户端提供了批量处理器以协助批量请求 Bulk Request BulkReq ...
- git 本地同步分支数,删除远程已经删除掉的多余分支
git remote show orgin (展示当前本地分支和远程上的分支差异,多余分支后会被标注 use 'git remote prune' to remove.) git remote pr ...
- 整理4种Vue组件通信方式
整理4种Vue组件通信方式 重点是梳理了前两个,父子组件通信和eventBus通信,我觉得Vue文档里的说明还是有一些简易,我自己第一遍是没看明白. 父子组件的通信 非父子组件的eventBus通信 ...
- [Swift]LeetCode29. 两数相除 | Divide Two Integers
Given two integers dividend and divisor, divide two integers without using multiplication, division ...
- [Swift]LeetCode213. 打家劫舍 II | House Robber II
You are a professional robber planning to rob houses along a street. Each house has a certain amount ...
- [Swift]LeetCode806. 写字符串需要的行数 | Number of Lines To Write String
We are to write the letters of a given string S, from left to right into lines. Each line has maximu ...
- CoCos2dx开发:更换导出的app名称和图标
要处理的文件路径如下: 1.更换图标: drawable-hdpi.drawable-ldpi.drawable-mdpi三个文件夹分别代表大.小.中三个不同宽高的图片,为了应对手机的不同分辨率,来采 ...