【.NET Core项目实战-统一认证平台】开篇及目录索引 <https://www.cnblogs.com/jackcao/p/9928879.html>
上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4
的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方式集成老的业务系统验证,然后根据不同的客户端使用不同的认证方式来集成到统一认证平台。
.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、自定授权源码剖析
当我们需要使用开源项目的某些功能时,最好了解实现的原理,才能正确和熟练使用功能,避免出现各种未知bug问题和出现问题无法解决的被动场面。
在使用此功能前,我们需要了解完整的实现流程,下面我将从源码开始讲解IdentityServer4是如何实现自定义的授权方式。
从我之前的文章中我们知道授权方式是通过Grant_Type的值来判断的,所以我们自定义的授权方式,也是通过此值来区分,所以需要了解自定义的值处理流程。
TokenRequestValidator是请求验证的方法,除了常规验证外,还增加了自定义的验证方式。
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); } } 
从上面代码可以看出,除了内置的授权方式,其他的都是用ValidateExtensionGrantRequestAsync
来进行验证,详细的验证规则继续分析实现过程。
private async Task<TokenRequestValidationResult> 
ValidateExtensionGrantRequestAsync(NameValueCollection parameters) { 
_logger.LogDebug("Start validation of custom grant token request"); 
///////////////////////////////////////////// // 校验客户端是否开启了此授权方式 
///////////////////////////////////////////// if 
(!_validatedRequest.Client.AllowedGrantTypes.Contains(_validatedRequest.GrantType)) 
{ LogError("{clientId} does not have the custom grant type in the allowed list, 
therefore requested grant is not allowed", _validatedRequest.Client.ClientId); 
return Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); } 
///////////////////////////////////////////// // 判断是否注入了此自定义的授权实现 
///////////////////////////////////////////// if 
(!_extensionGrantValidator.GetAvailableGrantTypes().Contains(_validatedRequest.GrantType, 
StringComparer.Ordinal)) { LogError("No validator is registered for the grant 
type: {grantType}", _validatedRequest.GrantType); return 
Invalid(OidcConstants.TokenErrors.UnsupportedGrantType); } 
///////////////////////////////////////////// // 校验是否支持scope 
///////////////////////////////////////////// if (!await 
ValidateRequestedScopesAsync(parameters)) { return 
Invalid(OidcConstants.TokenErrors.InvalidScope); } 
///////////////////////////////////////////// // 调用自定义的验证实现方法 
///////////////////////////////////////////// var result = await 
_extensionGrantValidator.ValidateAsync(_validatedRequest); if (result == null) 
{ LogError("Invalid extension grant"); return 
Invalid(OidcConstants.TokenErrors.InvalidGrant); } if (result.IsError) { if 
(result.Error.IsPresent()) { LogError("Invalid extension grant: {error}", 
result.Error); return Invalid(result.Error, result.ErrorDescription, 
result.CustomResponse); } else { LogError("Invalid extension grant"); return 
Invalid(OidcConstants.TokenErrors.InvalidGrant, customResponse: 
result.CustomResponse); } } if (result.Subject != null) { 
///////////////////////////////////////////// // 判断当前的用户是否可用 
///////////////////////////////////////////// var isActiveCtx = new 
IsActiveContext( result.Subject, _validatedRequest.Client, 
IdentityServerConstants.ProfileIsActiveCallers.ExtensionGrantValidation); await 
_profile.IsActiveAsync(isActiveCtx); if (isActiveCtx.IsActive == false) { // 
todo: raise event? LogError("User has been disabled: {subjectId}", 
result.Subject.GetSubjectId()); return 
Invalid(OidcConstants.TokenErrors.InvalidGrant); } _validatedRequest.Subject = 
result.Subject; } _logger.LogDebug("Validation of extension grant token request 
success"); return Valid(result.CustomResponse); } 
从代码中可以看出,实现流程如下:
 * 1、客户端是否配置了自定义的授权方式。 
 * 2、是否注入了自定义的授权实现。 
 * 3、授权的scope客户端是否有权限。 
 * 4、使用自定义的授权验证方式校验请求数据是否合法。 
 * 5、判断是否有有效数据信息,可自行实现接口。 
从源码中,可以发现流程已经非常清晰了,核心类ExtensionGrantValidator实现了自定义授权的校验过程,进一步分析下此类的代码实现。
using IdentityServer4.Models; using Microsoft.Extensions.Logging; using 
System; using System.Collections.Generic; using System.Linq; using 
System.Threading.Tasks; namespace IdentityServer4.Validation { /// <summary> 
/// Validates an extension grant request using the registered validators /// 
</summary> public class ExtensionGrantValidator { private readonly ILogger 
_logger; private readonly IEnumerable<IExtensionGrantValidator> _validators; 
/// <summary> /// Initializes a new instance of the <see 
cref="ExtensionGrantValidator"/> class. /// </summary> /// <param 
name="validators">The validators.</param> /// <param name="logger">The 
logger.</param> public 
ExtensionGrantValidator(IEnumerable<IExtensionGrantValidator> validators, 
ILogger<ExtensionGrantValidator> logger) { if (validators == null) { 
_validators = Enumerable.Empty<IExtensionGrantValidator>(); } else { 
_validators = validators; } _logger = logger; } /// <summary> /// Gets the 
available grant types. /// </summary> /// <returns></returns> public 
IEnumerable<string> GetAvailableGrantTypes() { return _validators.Select(v => 
v.GrantType); } /// <summary> /// Validates the request. /// </summary> /// 
<param name="request">The request.</param> /// <returns></returns> public async 
Task<GrantValidationResult> ValidateAsync(ValidatedTokenRequest request) { var 
validator = _validators.FirstOrDefault(v => 
v.GrantType.Equals(request.GrantType, StringComparison.Ordinal)); if (validator 
== null) { _logger.LogError("No validator found for grant type"); return new 
GrantValidationResult(TokenRequestErrors.UnsupportedGrantType); } try { 
_logger.LogTrace("Calling into custom grant validator: {type}", 
validator.GetType().FullName); var context = new 
ExtensionGrantValidationContext { Request = request }; await 
validator.ValidateAsync(context); return context.Result; } catch (Exception e) 
{ _logger.LogError(1, e, "Grant validation error: {message}", e.Message); 
return new GrantValidationResult(TokenRequestErrors.InvalidGrant); } } } } 
从上面代码可以发现,自定义授权方式,只需要实现IExtensionGrantValidator接口即可,然后支持多个自定义授权方式的共同使用。
到此整个验证过程解析完毕了,然后再查看下生成Token流程,实现方法为TokenResponseGenerator
,这个方法并不陌生,前几篇介绍不同的授权方式都介绍了,所以直接看实现代码。
public virtual async Task<TokenResponse> 
ProcessAsync(TokenRequestValidationResult request) { switch 
(request.ValidatedRequest.GrantType) { case 
OidcConstants.GrantTypes.ClientCredentials: return await 
ProcessClientCredentialsRequestAsync(request); case 
OidcConstants.GrantTypes.Password: return await 
ProcessPasswordRequestAsync(request); case 
OidcConstants.GrantTypes.AuthorizationCode: return await 
ProcessAuthorizationCodeRequestAsync(request); case 
OidcConstants.GrantTypes.RefreshToken: return await 
ProcessRefreshTokenRequestAsync(request); default://自定义授权生成Token的方式 return 
await ProcessExtensionGrantRequestAsync(request); } } protected virtual 
Task<TokenResponse> 
ProcessExtensionGrantRequestAsync(TokenRequestValidationResult request) { 
Logger.LogTrace("Creating response for extension grant request"); return 
ProcessTokenRequestAsync(request); } 
实现的代码方式和客户端模式及密码模式一样,这里就不多介绍了。
最后我们查看下是如何注入IExtensionGrantValidator,是否对外提供接入方式,发现IdentityServer4提供了
AddExtensionGrantValidator扩展方法,我们自己实现自定义授权后添加即可,详细实现代码如下。
public static IIdentityServerBuilder AddExtensionGrantValidator<T>(this 
IIdentityServerBuilder builder) where T : class, IExtensionGrantValidator { 
builder.Services.AddTransient<IExtensionGrantValidator, T>(); return builder; } 
二、自定义授权实现
现在开始开发第一个自定义授权方式,GrantType定义为CzarCustomUser,然后实现IExtensionGrantValidator
接口,为了演示方便,我新建一个测试用户表,用来模拟老系统的登录方式。
Create Table CzarCustomUser ( iid int identity, username varchar(50), 
usertruename varchar(50), userpwd varchar(100) ) --插入测试用户密码信息,测试数据密码不加密 insert 
into CzarCustomUser values('jinyancao','金焰的世界','777777') 
然后把实现验证的方法,由于代码太简单,我就直接贴代码如下。
namespace Czar.AuthPlatform.Web.Application.IRepository { public interface 
ICzarCustomUserRepository { /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// 
<param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// 
<returns></returns> CzarCustomUser FindUserByuAccount(string uaccount, string 
upassword); } } namespace Czar.AuthPlatform.Web.Application.Repository { public 
class CzarCustomUserRepository : ICzarCustomUserRepository { private readonly 
string DbConn = ""; public CzarCustomUserRepository(IOptions<CzarConfig> 
czarConfig) { DbConn = czarConfig.Value.DbConnectionStrings; } /// <summary> 
/// 根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// 
<param name="upassword">密码</param> /// <returns></returns> public 
CzarCustomUser FindUserByuAccount(string uaccount, string upassword) { using 
(var connection = new SqlConnection(DbConn)) { string sql = @"SELECT * from 
CzarCustomUser where username=@uaccount and userpwd=upassword "; var result = 
connection.QueryFirstOrDefault<CzarCustomUser>(sql, new { uaccount, upassword 
}); return result; } } } } namespace 
Czar.AuthPlatform.Web.Application.IServices { public interface 
ICzarCustomUserServices { /// <summary> /// 根据账号密码获取用户实体 /// </summary> /// 
<param name="uaccount">账号</param> /// <param name="upassword">密码</param> /// 
<returns></returns> CzarCustomUser FindUserByuAccount(string uaccount, string 
upassword); } } namespace Czar.AuthPlatform.Web.Application.Services { public 
class CzarCustomUserServices: ICzarCustomUserServices { private readonly 
ICzarCustomUserRepository czarCustomUserRepository; public 
CzarCustomUserServices(ICzarCustomUserRepository czarCustomUserRepository) { 
this.czarCustomUserRepository = czarCustomUserRepository; } /// <summary> /// 
根据账号密码获取用户实体 /// </summary> /// <param name="uaccount">账号</param> /// <param 
name="upassword">密码</param> /// <returns></returns> public CzarCustomUser 
FindUserByuAccount(string uaccount, string upassword) { return 
czarCustomUserRepository.FindUserByuAccount(uaccount, upassword); } } } 
现在可以定义自定义的授权类型了,我起名为CzarCustomUserGrantValidator,实现代码如下。
using Czar.AuthPlatform.Web.Application.IServices; using 
IdentityServer4.Models; using IdentityServer4.Validation; using 
System.Threading.Tasks; namespace Czar.AuthPlatform.Web.Application.Ids4 { /// 
<summary> /// 金焰的世界 /// 2019-01-28 /// 自定义用户授权 /// </summary> public class 
CzarCustomUserGrantValidator : IExtensionGrantValidator { public string 
GrantType => "CzarCustomUser"; private readonly ICzarCustomUserServices 
czarCustomUserServices; public 
CzarCustomUserGrantValidator(ICzarCustomUserServices czarCustomUserServices) { 
this.czarCustomUserServices = czarCustomUserServices; } public Task 
ValidateAsync(ExtensionGrantValidationContext context) { var userName = 
context.Request.Raw.Get("czar_name"); var userPassword = 
context.Request.Raw.Get("czar_password"); if (string.IsNullOrEmpty(userName) || 
string.IsNullOrEmpty(userPassword)) { context.Result = new 
GrantValidationResult(TokenRequestErrors.InvalidGrant); } //校验登录 var result = 
czarCustomUserServices.FindUserByuAccount(userName, userPassword); if 
(result==null) { context.Result = new 
GrantValidationResult(TokenRequestErrors.InvalidGrant); } //添加指定的claims 
context.Result = new GrantValidationResult( subject: result.iid.ToString(), 
authenticationMethod: GrantType, claims: result.Claims); return 
Task.CompletedTask; } } } 
这就实现了自定义授权的功能,是不是很简单呢?然后添加此扩展方法。
services.AddIdentityServer(option => { option.PublicOrigin = 
Configuration["CzarConfig:PublicOrigin"]; }) .AddDeveloperSigningCredential() 
.AddDapperStore(option => { option.DbConnectionStrings = 
Configuration["CzarConfig:DbConnectionStrings"]; }) 
.AddResourceOwnerValidator<CzarResourceOwnerPasswordValidator>() 
.AddProfileService<CzarProfileService>() 
.AddSecretValidator<JwtSecretValidator>() //添加自定义授权 
.AddExtensionGrantValidator<CzarCustomUserGrantValidator>(); 
现在是不是就可以使用自定义授权的方式了呢?打开PostMan
测试,按照源码解析和设计参数,测试信息如下,发现报错,原来是还未配置好客户端访问权限,开启权限测试如下。
三、客户端权限配置
在使用IdentityServer4时我们一定要理解整个验证流程。根据这次配置,我再梳理下流程如下:
 * 1、校验客户端client_id和Client_Secret。 
 * 2、校验客户端是否有当前的授权方式。 
 * 3、校验是否有请求scope权限。 
 * 4、如果非客户端验证,校验账号密码或自定义规则是否正确。 
 * 5、非客户端验证,校验授权信息是否有效。 
通过此流程会发现我们缺少授权方式配置,所以请求时提示上面的提示,既然知道原因了,那就很简单的来实现,添加客户端自定义授权模式。此信息是在
ClientGrantTypes表中,字段为客户端ID和授权方式。我测试的客户端ID为21,授权方式为CzarCustomUser
,那直接使用SQL语句插入关系,然后再测试。
INSERT INTO ClientGrantTypes VALUES(21,'CzarCustomUser'); 
发现可以获取到预期结果,然后查看access_token是什么内容,显示如下。
显示的信息和我们定义的信息相同,而且可以通过amr来区分授权类型,不同的业务系统使用不同的认证方式,然后统一集成到认证平台即可。
四、总结与思考
本篇我介绍了自定义授权方式,从源码解析到最后的实现详细讲解了实现原理,并使用测试的用户来实现自定义的认证流程,本篇涉及的知识点不多,但是非常重要,因为我们在使用统一身份认证时经常会遇到多种认证方式的结合,和多套不同应用用户的使用,在掌握了授权原理后,就能在不同的授权方式中切换的游刃有余。
思考下,有了这些知识后,关于短信验证码登录和扫码登录是不是有心理有底了呢?如果自己实现这类登录应该都知道从哪里下手了吧。
下篇我将介绍常用登录的短信验证码授权方式,尽情期待吧。
热门工具 换一换