【.NET Core項目實戰-統一認證平台】開篇及目錄索引
上篇文章我介紹了如何強制令牌過期的實現,相信大家對
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
來區分授權類型,不同的業務系統使用不同的認證方式,然后統一集成到認證平台即可。
四、總結與思考
本篇我介紹了自定義授權方式,從源碼解析到最后的實現詳細講解了實現原理,並使用測試的用戶來實現自定義的認證流程,本篇涉及的知識點不多,但是非常重要,因為我們在使用統一身份認證時經常會遇到多種認證方式的結合,和多套不同應用用戶的使用,在掌握了授權原理后,就能在不同的授權方式中切換的游刃有余。
思考下,有了這些知識后,關於短信驗證碼登錄和掃碼登錄是不是有心理有底了呢?如果自己實現這類登錄應該都知道從哪里下手了吧。
下篇我將介紹常用登錄的短信驗證碼授權方式,盡情期待吧。