上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种方式存储,本篇将介绍如何使用ids4进行客户端授权。
.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、如何添加客户端授权?
在了解如何进行客户端授权时,我们需要了解详细的授权流程,在【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析一篇中我大概介绍了客户端的授权方式,本篇再次回忆下客户端的授权方式,老规则,上源码。
首先查看获取token的方式,核心代码如下。
private async Task
ProcessTokenRequestAsync(HttpContext context)
{
_logger.LogDebug("Start token request.");
// 1、验证客户端及授权信息结果
var clientResult = await _clientValidator.ValidateAsync(context);
if (clientResult.Client == null)
{
return Error(OidcConstants.TokenErrors.InvalidClient);
}
// 2、验证请求结果
var form = (await context.Request.ReadFormAsync()).AsNameValueCollection();
_logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName);
var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult);
if (requestResult.IsError)
{
await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult));
return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse);
}
// 3、创建输出结果
_logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName);
var response = await _responseGenerator.ProcessAsync(requestResult);
await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
LogTokens(response, requestResult);
// 4、返回结果
_logger.LogDebug("Token request success.");
return new TokenResult(response);
}
我们需要详细分析下第一步客户端授权信息是如何验证的?核心代码如下。
///
///验证客户端授权结果
///
/// 请求上下文
///
public async Task ValidateAsync(HttpContext context)
{
_logger.LogDebug("Start client validation");
var fail = new ClientSecretValidationResult
{
IsError = true
};
//通过请求上下文和配置信息获取校验方式,从这里我们可以知道客户端请求的几种方式。
var parsedSecret = await _parser.ParseAsync(context);
if (parsedSecret == null)
{
await RaiseFailureEventAsync("unknown", "No client id found");
_logger.LogError("No client identifier found");
return fail;
}
// 根据客户端ID获取客户端相关信息。(配合持久化篇查看)
var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id);
if (client == null)
{
await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client");
_logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id);
return fail;
}
SecretValidationResult secretValidationResult = null;
if (!client.RequireClientSecret || client.IsImplicitOnly())
{
_logger.LogDebug("Public Client - skipping secret validation success");
}
else
{
//校验客户端授权和请求的是否一致
secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);
if (secretValidationResult.Success == false)
{
await RaiseFailureEventAsync(client.ClientId, "Invalid client secret");
_logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId);
return fail;
}
}
_logger.LogDebug("Client validation success");
var success = new ClientSecretValidationResult
{
IsError = false,
Client = client,
Secret = parsedSecret,
Confirmation = secretValidationResult?.Confirmation
};
await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type);
return success;
}
这里几个方法可以从写的说明备注里就可以知道什么意思,但是 var parsedSecret = await _parser.ParseAsync(context);这句话可能不少人有疑问,这段是做什么的?如何实现不同的授权方式?
这块就需要继续理解Ids4的实现思路,详细代码如下。
///
/// 检查上下文获取授权信息
///
/// The HTTP context.
///
public async Task ParseAsync(HttpContext context)
{
// 遍历所有的客户端授权获取方式,提取当前哪一个满足需求
ParsedSecret bestSecret = null;
foreach (var parser in _parsers)
{
var parsedSecret = await parser.ParseAsync(context);
if (parsedSecret != null)
{
_logger.LogDebug("Parser found secret: {type}", parser.GetType().Name);
bestSecret = parsedSecret;
if (parsedSecret.Type != IdentityServerConstants.ParsedSecretTypes.NoSecret)
{
break;
}
}
}
if (bestSecret != null)
{
_logger.LogDebug("Secret id found: {id}", bestSecret.Id);
return bestSecret;
}
_logger.LogDebug("Parser found no secret");
return null;
}
就是从注入的默认实现里检测任何一个实现ISecretParser接口方法,通过转到实现,可以发现有PostBodySecretParser、JwtBearerClientAssertionSecretParser、BasicAuthenticationSecretParser三种方式,然后再查看下注入方法,看那些实现被默认注入了,这样就清楚我们使用Ids4时支持哪几种客户端授权方式。
///
/// 添加默认的授权分析
///
/// The builder.
///
public static IIdentityServerBuilder AddDefaultSecretParsers(this IIdentityServerBuilder builder)
{
builder.Services.AddTransient();
builder.Services.AddTransient();
return builder;
}
从上面代码可以发现,默认注入了两种分析器,我们就可以通过这两个方式来做客户端的授权,下面会分别演示两种授权方式的实现。
BasicAuthenticationSecretParser
public Task ParseAsync(HttpContext context)
{
_logger.LogDebug("Start parsing Basic Authentication secret");
var notfound = Task.FromResult(null);
var authorizationHeader = context.Request.Headers["Authorization"].FirstOrDefault();
if (authorizationHeader.IsMissing())
{
return notfound;
}
if (!authorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return notfound;
}
var parameter = authorizationHeader.Substring("Basic ".Length);
string pair;
try
{
pair = Encoding.UTF8.GetString(
Convert.FromBase64String(parameter));
}
catch (FormatException)
{
_logger.LogWarning("Malformed Basic Authentication credential.");
return notfound;
}
catch (ArgumentException)
{
_logger.LogWarning("Malformed Basic Authentication credential.");
return notfound;
}
var ix = pair.IndexOf(':');
if (ix == -1)
{
_logger.LogWarning("Malformed Basic Authentication credential.");
return notfound;
}
var clientId = pair.Substring(0, ix);
var secret = pair.Substring(ix + 1);
if (clientId.IsPresent())
{
if (clientId.Length > _options.InputLengthRestrictions.ClientId ||
(secret.IsPresent() && secret.Length > _options.InputLengthRestrictions.ClientSecret))
{
_logger.LogWarning("Client ID or secret exceeds allowed length.");
return notfound;
}
var parsedSecret = new ParsedSecret
{
Id = Decode(clientId),
Credential = secret.IsMissing() ? null : Decode(secret),
Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret
};
return Task.FromResult(parsedSecret);
}
_logger.LogDebug("No Basic Authentication secret found");
return notfound;
}
由于代码比较简单,就不介绍了,这里直接模拟此种方式授权,打开PostMan,在Headers中增加Authorization的Key,并设置Value为Basic YXBwY2xpZW50JTNBc2VjcmV0,其中Basic后为client_id:client_secret值使用Base64加密。然后请求后显示如图所示结果,奈斯,得到我们授权的结果。
PostBodySecretParser
public async Task ParseAsync(HttpContext context)
{
_logger.LogDebug("Start parsing for secret in post body");
if (!context.Request.HasFormContentType)
{
_logger.LogDebug("Content type is not a form");
return null;
}
var body = await context.Request.ReadFormAsync();
if (body != null)
{
var id = body["client_id"].FirstOrDefault();
var secret = body["client_secret"].FirstOrDefault();
// client id must be present
if (id.IsPresent())
{
if (id.Length > _options.InputLengthRestrictions.ClientId)
{
_logger.LogError("Client ID exceeds maximum length.");
return null;
}
if (secret.IsPresent())
{
if (secret.Length > _options.InputLengthRestrictions.ClientSecret)
{
_logger.LogError("Client secret exceeds maximum length.");
return null;
}
return new ParsedSecret
{
Id = id,
Credential = secret,
Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret
};
}
else
{
// client secret is optional
_logger.LogDebug("client id without secret found");
return new ParsedSecret
{
Id = id,
Type = IdentityServerConstants.ParsedSecretTypes.NoSecret
};
}
}
}
_logger.LogDebug("No secret in post body found");
return null;
}
此种认证方式就是从form_data提取client_id和client_secret信息,我们使用PostMan继续模拟客户端授权,测试结果如下,也可以得到我们想要的结果。
有了前面的两个授权方式,我们清楚了首先验证客户端的授权信息是否一致,再继续观察后续的执行流程,这时会发现TokenRequestValidator中列出了客户端授权的其他信息验证,详细定义代码如下。
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);
}
详细的授权验证代码如下,校验客户端授权的一般规则。
private async Task ValidateClientCredentialsRequestAsync(NameValueCollection parameters)
{
_logger.LogDebug("Start client credentials token request validation");
/////////////////////////////////////////////
// 校验客户端Id是否开启了客户端授权
/////////////////////////////////////////////
if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials))
{
LogError("{clientId} not authorized for client credentials flow, check the AllowedGrantTypes of the client", _validatedRequest.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
}
/////////////////////////////////////////////
// 校验客户端是否有请求的scopes权限
/////////////////////////////////////////////
if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true))
{
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes)
{
LogError("{clientId} cannot request OpenID scopes in client credentials flow", _validatedRequest.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope)
{
LogError("{clientId} cannot request a refresh token in client credentials flow", _validatedRequest.Client.ClientId);
return Invalid(OidcConstants.TokenErrors.InvalidScope);
}
_logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId);
return Valid();
}
最终输出详细的校验结果数据,现在整个客户端授权的完整逻辑已经介绍完毕,那如何添加我们的自定义客户端授权呢?比如我要给客户端A开放一个访问接口访问权限,下面就开通客户端A为案例讲解。
开通客户端授权
根据前面介绍的验证流程,我们清楚首先需要增加客户端信息,这里起名叫clienta,密码设置成secreta。上一篇我们介绍了Dapper持久化IdentityServer4的授权信息,所以这里我就直接以SQL语句的方式来演示添加配置信息。详细的语句如下:
/*
添加客户端脚本
*/
--1、添加客户端信息
INSERT INTO Clients(AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(3600,'clienta','测试客户端A',1);
--2、添加客户端密钥,密码为(secreta) sha256
INSERT INTO ClientSecrets VALUES(21,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI=');
--3、增加客户端授权权限
INSERT INTO ClientGrantTypes VALUES(21,'client_credentials');
--4、增加客户端能够访问scope
INSERT INTO ClientScopes VALUES(21,'mpc_gateway');
然后我们来测试下新开通的客户端授权,如下图所示,可以正常获取授权信息了,另外一种Basic授权方式可自行测试。
二、如何配合网关认证和授权?
前面使用的是项目自己进行验证的,正式项目运行时,我们会把请求放到网关中,统一由网关进行认证和授权等操作,内部api无需再次进行认证和授权,那如何实现网关认证和授权呢?
我们可以回忆下之前介绍网关篇时认证篇章,里面介绍的非常清楚。这里我们参照刚才添加的客户端A为案例增加网关授权,因为我们对外暴露的是网关地址,而不是内部具体认证项目地址。
1、添加网关授权路由
本项目的网关端口为7777,所以网关授权的地址为http://localhost:7777/connect/token,由于为添加网关路由,直接访问报401,我们首先增加网关的路由信息。
-- 1、插入认证路由(使用默认分类)
insert into AhphReRoute values(1,'/connect/token','[ "POST" ]','','http','/connect/token','[{"Host": "localhost","Port": 6611 }]',
'','','','','','','',0,1);
--2、加入全局配置
INSERT INTO AhphConfigReRoutes VALUES(1,3)
--3、增加认证配置地址路由
insert into AhphReRoute values(1,'/.well-known/openid-configuration','[ "GET" ]','','http','/.well-known/openid-co