【.NET Core项目实战-统一认证平台】开篇及目录索引

上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlservermysql两种方式存储,本篇将介绍如何使用ids4进行客户端授权。

.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。

一、如何添加客户端授权?

在了解如何进行客户端授权时,我们需要了解详细的授权流程,在【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析一篇中我大概介绍了客户端的授权方式,本篇再次回忆下客户端的授权方式,老规则,上源码。

首先查看获取token的方式,核心代码如下。

  1. private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context)
  2. {
  3. _logger.LogDebug("Start token request.");
  4. // 1、验证客户端及授权信息结果
  5. var clientResult = await _clientValidator.ValidateAsync(context);
  6. if (clientResult.Client == null)
  7. {
  8. return Error(OidcConstants.TokenErrors.InvalidClient);
  9. }
  10. // 2、验证请求结果
  11. var form = (await context.Request.ReadFormAsync()).AsNameValueCollection();
  12. _logger.LogTrace("Calling into token request validator: {type}", _requestValidator.GetType().FullName);
  13. var requestResult = await _requestValidator.ValidateRequestAsync(form, clientResult);
  14. if (requestResult.IsError)
  15. {
  16. await _events.RaiseAsync(new TokenIssuedFailureEvent(requestResult));
  17. return Error(requestResult.Error, requestResult.ErrorDescription, requestResult.CustomResponse);
  18. }
  19. // 3、创建输出结果
  20. _logger.LogTrace("Calling into token request response generator: {type}", _responseGenerator.GetType().FullName);
  21. var response = await _responseGenerator.ProcessAsync(requestResult);
  22. await _events.RaiseAsync(new TokenIssuedSuccessEvent(response, requestResult));
  23. LogTokens(response, requestResult);
  24. // 4、返回结果
  25. _logger.LogDebug("Token request success.");
  26. return new TokenResult(response);
  27. }

我们需要详细分析下第一步客户端授权信息是如何验证的?核心代码如下。

  1. /// <summary>
  2. ///验证客户端授权结果
  3. /// </summary>
  4. /// <param name="context">请求上下文</param>
  5. /// <returns></returns>
  6. public async Task<ClientSecretValidationResult> ValidateAsync(HttpContext context)
  7. {
  8. _logger.LogDebug("Start client validation");
  9. var fail = new ClientSecretValidationResult
  10. {
  11. IsError = true
  12. };
  13. //通过请求上下文和配置信息获取校验方式,从这里我们可以知道客户端请求的几种方式。
  14. var parsedSecret = await _parser.ParseAsync(context);
  15. if (parsedSecret == null)
  16. {
  17. await RaiseFailureEventAsync("unknown", "No client id found");
  18. _logger.LogError("No client identifier found");
  19. return fail;
  20. }
  21. // 根据客户端ID获取客户端相关信息。(配合持久化篇查看)
  22. var client = await _clients.FindEnabledClientByIdAsync(parsedSecret.Id);
  23. if (client == null)
  24. {
  25. await RaiseFailureEventAsync(parsedSecret.Id, "Unknown client");
  26. _logger.LogError("No client with id '{clientId}' found. aborting", parsedSecret.Id);
  27. return fail;
  28. }
  29. SecretValidationResult secretValidationResult = null;
  30. if (!client.RequireClientSecret || client.IsImplicitOnly())
  31. {
  32. _logger.LogDebug("Public Client - skipping secret validation success");
  33. }
  34. else
  35. {
  36. //校验客户端授权和请求的是否一致
  37. secretValidationResult = await _validator.ValidateAsync(parsedSecret, client.ClientSecrets);
  38. if (secretValidationResult.Success == false)
  39. {
  40. await RaiseFailureEventAsync(client.ClientId, "Invalid client secret");
  41. _logger.LogError("Client secret validation failed for client: {clientId}.", client.ClientId);
  42. return fail;
  43. }
  44. }
  45. _logger.LogDebug("Client validation success");
  46. var success = new ClientSecretValidationResult
  47. {
  48. IsError = false,
  49. Client = client,
  50. Secret = parsedSecret,
  51. Confirmation = secretValidationResult?.Confirmation
  52. };
  53. await RaiseSuccessEventAsync(client.ClientId, parsedSecret.Type);
  54. return success;
  55. }

这里几个方法可以从写的说明备注里就可以知道什么意思,但是 var parsedSecret = await _parser.ParseAsync(context);这句话可能不少人有疑问,这段是做什么的?如何实现不同的授权方式?

这块就需要继续理解Ids4的实现思路,详细代码如下。

  1. /// <summary>
  2. /// 检查上下文获取授权信息
  3. /// </summary>
  4. /// <param name="context">The HTTP context.</param>
  5. /// <returns></returns>
  6. public async Task<ParsedSecret> ParseAsync(HttpContext context)
  7. {
  8. // 遍历所有的客户端授权获取方式,提取当前哪一个满足需求
  9. ParsedSecret bestSecret = null;
  10. foreach (var parser in _parsers)
  11. {
  12. var parsedSecret = await parser.ParseAsync(context);
  13. if (parsedSecret != null)
  14. {
  15. _logger.LogDebug("Parser found secret: {type}", parser.GetType().Name);
  16. bestSecret = parsedSecret;
  17. if (parsedSecret.Type != IdentityServerConstants.ParsedSecretTypes.NoSecret)
  18. {
  19. break;
  20. }
  21. }
  22. }
  23. if (bestSecret != null)
  24. {
  25. _logger.LogDebug("Secret id found: {id}", bestSecret.Id);
  26. return bestSecret;
  27. }
  28. _logger.LogDebug("Parser found no secret");
  29. return null;
  30. }

就是从注入的默认实现里检测任何一个实现ISecretParser接口方法,通过转到实现,可以发现有PostBodySecretParser、JwtBearerClientAssertionSecretParser、BasicAuthenticationSecretParser三种方式,然后再查看下注入方法,看那些实现被默认注入了,这样就清楚我们使用Ids4时支持哪几种客户端授权方式。

  1. /// <summary>
  2. /// 添加默认的授权分析
  3. /// </summary>
  4. /// <param name="builder">The builder.</param>
  5. /// <returns></returns>
  6. public static IIdentityServerBuilder AddDefaultSecretParsers(this IIdentityServerBuilder builder)
  7. {
  8. builder.Services.AddTransient<ISecretParser, BasicAuthenticationSecretParser>();
  9. builder.Services.AddTransient<ISecretParser, PostBodySecretParser>();
  10. return builder;
  11. }

从上面代码可以发现,默认注入了两种分析器,我们就可以通过这两个方式来做客户端的授权,下面会分别演示两种授权方式的实现。

  1. BasicAuthenticationSecretParser

    1. public Task<ParsedSecret> ParseAsync(HttpContext context)
    2. {
    3. _logger.LogDebug("Start parsing Basic Authentication secret");
    4. var notfound = Task.FromResult<ParsedSecret>(null);
    5. var authorizationHeader = context.Request.Headers["Authorization"].FirstOrDefault();
    6. if (authorizationHeader.IsMissing())
    7. {
    8. return notfound;
    9. }
    10. if (!authorizationHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
    11. {
    12. return notfound;
    13. }
    14. var parameter = authorizationHeader.Substring("Basic ".Length);
    15. string pair;
    16. try
    17. {
    18. pair = Encoding.UTF8.GetString(
    19. Convert.FromBase64String(parameter));
    20. }
    21. catch (FormatException)
    22. {
    23. _logger.LogWarning("Malformed Basic Authentication credential.");
    24. return notfound;
    25. }
    26. catch (ArgumentException)
    27. {
    28. _logger.LogWarning("Malformed Basic Authentication credential.");
    29. return notfound;
    30. }
    31. var ix = pair.IndexOf(':');
    32. if (ix == -1)
    33. {
    34. _logger.LogWarning("Malformed Basic Authentication credential.");
    35. return notfound;
    36. }
    37. var clientId = pair.Substring(0, ix);
    38. var secret = pair.Substring(ix + 1);
    39. if (clientId.IsPresent())
    40. {
    41. if (clientId.Length > _options.InputLengthRestrictions.ClientId ||
    42. (secret.IsPresent() && secret.Length > _options.InputLengthRestrictions.ClientSecret))
    43. {
    44. _logger.LogWarning("Client ID or secret exceeds allowed length.");
    45. return notfound;
    46. }
    47. var parsedSecret = new ParsedSecret
    48. {
    49. Id = Decode(clientId),
    50. Credential = secret.IsMissing() ? null : Decode(secret),
    51. Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret
    52. };
    53. return Task.FromResult(parsedSecret);
    54. }
    55. _logger.LogDebug("No Basic Authentication secret found");
    56. return notfound;
    57. }

    由于代码比较简单,就不介绍了,这里直接模拟此种方式授权,打开PostMan,在Headers中增加Authorization的Key,并设置Value为Basic YXBwY2xpZW50JTNBc2VjcmV0,其中Basic后为client_id:client_secret值使用Base64加密。然后请求后显示如图所示结果,奈斯,得到我们授权的结果。

  2. PostBodySecretParser

    1. public async Task<ParsedSecret> ParseAsync(HttpContext context)
    2. {
    3. _logger.LogDebug("Start parsing for secret in post body");
    4. if (!context.Request.HasFormContentType)
    5. {
    6. _logger.LogDebug("Content type is not a form");
    7. return null;
    8. }
    9. var body = await context.Request.ReadFormAsync();
    10. if (body != null)
    11. {
    12. var id = body["client_id"].FirstOrDefault();
    13. var secret = body["client_secret"].FirstOrDefault();
    14. // client id must be present
    15. if (id.IsPresent())
    16. {
    17. if (id.Length > _options.InputLengthRestrictions.ClientId)
    18. {
    19. _logger.LogError("Client ID exceeds maximum length.");
    20. return null;
    21. }
    22. if (secret.IsPresent())
    23. {
    24. if (secret.Length > _options.InputLengthRestrictions.ClientSecret)
    25. {
    26. _logger.LogError("Client secret exceeds maximum length.");
    27. return null;
    28. }
    29. return new ParsedSecret
    30. {
    31. Id = id,
    32. Credential = secret,
    33. Type = IdentityServerConstants.ParsedSecretTypes.SharedSecret
    34. };
    35. }
    36. else
    37. {
    38. // client secret is optional
    39. _logger.LogDebug("client id without secret found");
    40. return new ParsedSecret
    41. {
    42. Id = id,
    43. Type = IdentityServerConstants.ParsedSecretTypes.NoSecret
    44. };
    45. }
    46. }
    47. }
    48. _logger.LogDebug("No secret in post body found");
    49. return null;
    50. }

    此种认证方式就是从form_data提取client_idclient_secret信息,我们使用PostMan继续模拟客户端授权,测试结果如下,也可以得到我们想要的结果。

有了前面的两个授权方式,我们清楚了首先验证客户端的授权信息是否一致,再继续观察后续的执行流程,这时会发现TokenRequestValidator中列出了客户端授权的其他信息验证,详细定义代码如下。

  1. switch (grantType)
  2. {
  3. case OidcConstants.GrantTypes.AuthorizationCode:
  4. return await RunValidationAsync(ValidateAuthorizationCodeRequestAsync, parameters);
  5. //客户端授权
  6. case OidcConstants.GrantTypes.ClientCredentials:
  7. return await RunValidationAsync(ValidateClientCredentialsRequestAsync, parameters);
  8. case OidcConstants.GrantTypes.Password:
  9. return await RunValidationAsync(ValidateResourceOwnerCredentialRequestAsync, parameters);
  10. case OidcConstants.GrantTypes.RefreshToken:
  11. return await RunValidationAsync(ValidateRefreshTokenRequestAsync, parameters);
  12. default:
  13. return await RunValidationAsync(ValidateExtensionGrantRequestAsync, parameters);
  14. }

详细的授权验证代码如下,校验客户端授权的一般规则。

  1. private async Task<TokenRequestValidationResult> ValidateClientCredentialsRequestAsync(NameValueCollection parameters)
  2. {
  3. _logger.LogDebug("Start client credentials token request validation");
  4. /////////////////////////////////////////////
  5. // 校验客户端Id是否开启了客户端授权
  6. /////////////////////////////////////////////
  7. if (!_validatedRequest.Client.AllowedGrantTypes.ToList().Contains(GrantType.ClientCredentials))
  8. {
  9. LogError("{clientId} not authorized for client credentials flow, check the AllowedGrantTypes of the client", _validatedRequest.Client.ClientId);
  10. return Invalid(OidcConstants.TokenErrors.UnauthorizedClient);
  11. }
  12. /////////////////////////////////////////////
  13. // 校验客户端是否有请求的scopes权限
  14. /////////////////////////////////////////////
  15. if (!await ValidateRequestedScopesAsync(parameters, ignoreImplicitIdentityScopes: true, ignoreImplicitOfflineAccess: true))
  16. {
  17. return Invalid(OidcConstants.TokenErrors.InvalidScope);
  18. }
  19. if (_validatedRequest.ValidatedScopes.ContainsOpenIdScopes)
  20. {
  21. LogError("{clientId} cannot request OpenID scopes in client credentials flow", _validatedRequest.Client.ClientId);
  22. return Invalid(OidcConstants.TokenErrors.InvalidScope);
  23. }
  24. if (_validatedRequest.ValidatedScopes.ContainsOfflineAccessScope)
  25. {
  26. LogError("{clientId} cannot request a refresh token in client credentials flow", _validatedRequest.Client.ClientId);
  27. return Invalid(OidcConstants.TokenErrors.InvalidScope);
  28. }
  29. _logger.LogDebug("{clientId} credentials token request validation success", _validatedRequest.Client.ClientId);
  30. return Valid();
  31. }

最终输出详细的校验结果数据,现在整个客户端授权的完整逻辑已经介绍完毕,那如何添加我们的自定义客户端授权呢?比如我要给客户端A开放一个访问接口访问权限,下面就开通客户端A为案例讲解。

开通客户端授权

根据前面介绍的验证流程,我们清楚首先需要增加客户端信息,这里起名叫clienta,密码设置成secreta。上一篇我们介绍了Dapper持久化IdentityServer4的授权信息,所以这里我就直接以SQL语句的方式来演示添加配置信息。详细的语句如下:

  1. /*
  2. 添加客户端脚本
  3. */
  4. --1、添加客户端信息
  5. INSERT INTO Clients(AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(3600,'clienta','测试客户端A',1);
  6. --2、添加客户端密钥,密码为(secreta) sha256
  7. INSERT INTO ClientSecrets VALUES(21,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI=');
  8. --3、增加客户端授权权限
  9. INSERT INTO ClientGrantTypes VALUES(21,'client_credentials');
  10. --4、增加客户端能够访问scope
  11. INSERT INTO ClientScopes VALUES(21,'mpc_gateway');

然后我们来测试下新开通的客户端授权,如下图所示,可以正常获取授权信息了,另外一种Basic授权方式可自行测试。

二、如何配合网关认证和授权?

前面使用的是项目自己进行验证的,正式项目运行时,我们会把请求放到网关中,统一由网关进行认证和授权等操作,内部api无需再次进行认证和授权,那如何实现网关认证和授权呢?

我们可以回忆下之前介绍网关篇时认证篇章,里面介绍的非常清楚。这里我们参照刚才添加的客户端A为案例增加网关授权,因为我们对外暴露的是网关地址,而不是内部具体认证项目地址。

1、添加网关授权路由

本项目的网关端口为7777,所以网关授权的地址为http://localhost:7777/connect/token,由于为添加网关路由,直接访问报401,我们首先增加网关的路由信息。

  1. -- 1、插入认证路由(使用默认分类)
  2. insert into AhphReRoute values(1,'/connect/token','[ "POST" ]','','http','/connect/token','[{"Host": "localhost","Port": 6611 }]',
  3. '','','','','','','',0,1);
  4. --2、加入全局配置
  5. INSERT INTO AhphConfigReRoutes VALUES(1,3)
  6. --3、增加认证配置地址路由
  7. insert into AhphReRoute values(1,'/.well-known/openid-configuration','[ "GET" ]','','http','/.well-known/openid-configuration','[{"Host": "localhost","Port": 6611 }]',
  8. '','','','','','','',0,1);
  9. --4、加入全局配置
  10. INSERT INTO AhphConfigReRoutes VALUES(1,4);
  11. --5、增加认证配置地址路由
  12. insert into AhphReRoute values(1,'/.well-known/openid-configuration/jwks','[ "GET" ]','','http','/.well-known/openid-configuration/jwks','[{"Host": "localhost","Port": 6611 }]',
  13. '','','','','','','',0,1);
  14. --6、加入全局配置
  15. INSERT INTO AhphConfigReRoutes VALUES(1,5);

通过PostMan测试,可以得到我们预期的授权信息结果。

然后继续访问我们之前配置的授权路由,提示401未授权,这块就涉及到前面网关篇的知识了,因为我们的网关增加了授权,所以需要增加客户端授权才能访问。

2、添加客户端授权访问

还记得是如何添加客户端授权的吗?详细介绍参考[【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权 ,我直接把授权的脚本编写如下:

  1. --7、插入把客户端加入测试路由组2
  2. INSERT INTO AhphClientGroup VALUES(21,2)

使用我们刚授权的信息,再次访问之前配置的需要认证的路由,可以得到我们预期的结果,奈斯,和网关篇的内容完全一致。

注意:在配置完信息后需要清理缓存,因为我们之前做网关时,很多配置信息的读取使用了缓存。

三、如何统一输出结果?

作为一块准备应用到生产环境的产品,可能为各种第三方提供应用支持,那么统一的输出结果是必须要实现的,比如我们使用微信sdk或其他第三方sdk时,会发现它们都会列出出现错误的统一提示,由标识代码和说明组成,这里我们就需要解决如何标准化输出问题,自己业务系统输出标准结果很容易,因为都是自己控制的结果输出,那么我们网关集成Ocelot、认证集成IdentityServer4,这两块如何进行标准化输出呢?

那开始我们的改造之旅吧,首先我们要明确如果遇到错误如何进行输出,我们定义一个输出基类BaseResult,详细的定义如下:

  1. /// <summary>
  2. /// 金焰的世界
  3. /// 2018-12-10
  4. /// 信息输出基类
  5. /// </summary>
  6. public class BaseResult
  7. {
  8. public BaseResult(int _errCode,string _errMsg)
  9. {
  10. errCode = _errCode;
  11. errMsg = _errMsg;
  12. }
  13. public BaseResult()
  14. {
  15. }
  16. /// <summary>
  17. /// 错误类型标识
  18. /// </summary>
  19. public int errCode { get; set; }
  20. /// <summary>
  21. /// 错误类型说明
  22. /// </summary>
  23. public string errMsg { get; set; }
  24. }
  25. /// <summary>
  26. /// 金焰的世界
  27. /// 2018-12-10
  28. /// 默认成功结果
  29. /// </summary>
  30. public class SuccessResult : BaseResult
  31. {
  32. public SuccessResult() : base(0, "成功")
  33. {
  34. }
  35. }

1、网关默认输出改造

网关这段需要改造错误提示的代码和内容以及异常的输出结果,首先改造错误情况的输出结果,使用BaseResult统一输出,这里就需要重写输出中间件ResponderMiddleware,下面就开始重写之旅吧。

新增自定义输出中间件CzarResponderMiddleware,详细代码如下:

  1. using Czar.Gateway.Configuration;
  2. using Microsoft.AspNetCore.Http;
  3. using Ocelot.Errors;
  4. using Ocelot.Logging;
  5. using Ocelot.Middleware;
  6. using Ocelot.Responder;
  7. using System.Collections.Generic;
  8. using System.Net;
  9. using System.Threading.Tasks;
  10. namespace Czar.Gateway.Responder.Middleware
  11. {
  12. /// <summary>
  13. /// 金焰的世界
  14. /// 2018-12-10
  15. /// 统一输出中间件
  16. /// </summary>
  17. public class CzarResponderMiddleware: OcelotMiddleware
  18. {
  19. private readonly OcelotRequestDelegate _next;
  20. private readonly IHttpResponder _responder;
  21. private readonly IErrorsToHttpStatusCodeMapper _codeMapper;
  22. public CzarResponderMiddleware(OcelotRequestDelegate next,
  23. IHttpResponder responder,
  24. IOcelotLoggerFactory loggerFactory,
  25. IErrorsToHttpStatusCodeMapper codeMapper
  26. )
  27. : base(loggerFactory.CreateLogger<CzarResponderMiddleware>())
  28. {
  29. _next = next;
  30. _responder = responder;
  31. _codeMapper = codeMapper;
  32. }
  33. public async Task Invoke(DownstreamContext context)
  34. {
  35. await _next.Invoke(context);
  36. if (context.IsError)
  37. {//自定义输出结果
  38. var errmsg = context.Errors[0].Message;
  39. int httpstatus = _codeMapper.Map(context.Errors);
  40. var errResult = new BaseResult() { errcode = httpstatus, errmsg = errmsg };
  41. var message = errResult.ToJson();
  42. context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
  43. await context.HttpContext.Response.WriteAsync(message);
  44. return;
  45. }
  46. else if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK)
  47. {//标记失败,不做任何处理,自定义扩展时预留
  48. }
  49. else if (context.DownstreamResponse == null)
  50. {//添加如果管道强制终止,不做任何处理,修复未将对象实例化错误
  51. }
  52. else
  53. {//继续请求下游地址返回
  54. Logger.LogDebug("no pipeline errors, setting and returning completed response");
  55. await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse);
  56. }
  57. }
  58. private void SetErrorResponse(HttpContext context, List<Error> errors)
  59. {
  60. var statusCode = _codeMapper.Map(errors);
  61. _responder.SetErrorResponseOnContext(context, statusCode);
  62. }
  63. }
  64. }

然后添加中间件扩展,代码如下。

  1. using Ocelot.Middleware.Pipeline;
  2. namespace Czar.Gateway.Responder.Middleware
  3. {
  4. /// <summary>
  5. /// 金焰的世界
  6. /// 2018-12-10
  7. /// 应用输出中间件扩展
  8. /// </summary>
  9. public static class CzarResponderMiddlewareExtensions
  10. {
  11. public static IOcelotPipelineBuilder UseCzarResponderMiddleware(this IOcelotPipelineBuilder builder)
  12. {
  13. return builder.UseMiddleware<CzarResponderMiddleware>();
  14. }
  15. }
  16. }

最后使用此扩展来接管默认的输出中间件,详细代码如下。

  1. //builder.UseResponderMiddleware();
  2. builder.UseCzarResponderMiddleware();

好了,网关统一输出中间件就完成了,是不是很简单呢?我们来测试下效果吧,PostMan闪亮登场,



奈斯,这才是我们需要的结果,那如何异常会输出什么呢??我们来模拟下结果,我直接在服务端抛出异常测试。

默认情况会支持输出异常的堆栈信息。那如何捕获服务端异常信息呢?我们需要了解在哪里发送了后端请求,通过源码分析,发现是由HttpRequesterMiddleware中间件做后端请求,这时我们只需要改造下此中间件即可完成统一异常捕获。改造核心代码如下:

  1. public async Task Invoke(DownstreamContext context)
  2. {
  3. var response = await _requester.GetResponse(context);
  4. if (response.IsError)
  5. {
  6. Logger.LogDebug("IHttpRequester returned an error, setting pipeline error");
  7. SetPipelineError(context, response.Errors);
  8. return;
  9. }
  10. else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK)
  11. {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息
  12. var error = new InternalServerError($"请求服务异常");
  13. Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}");
  14. SetPipelineError(context, error);
  15. return;
  16. }
  17. Logger.LogDebug("setting http response message");
  18. context.DownstreamResponse = new DownstreamResponse(response.Data);
  19. }

修改测试后端服务代码如下,

  1. // GET api/values/5
  2. [HttpGet("{id}")]
  3. public ActionResult<string> Get(int id)
  4. {
  5. throw new Exception("测试异常");
  6. }

然后通过网关访问路由地址http://localhost:7777/ctr/values/1,输出为{"errcode":500,"errmsg":"请求服务异常"},得到了预期的所有目标,网关统一输出全部改造完毕。

2、认证的统一输出改造

这里为了统一风格,我们先查看下Ids4的错误提示方式和输出结果,然后配合源码可以发现到输出都是继承IEndpointResult接口,并定义了各种方式的输出,且校验失败时,输出的状态码都不是200,那么我们可以从这里下手,在网关层增加独立的判断,来兼容自定义的输出。改造代码如下:

  1. using Czar.Gateway.Errors;
  2. using Newtonsoft.Json.Linq;
  3. using Ocelot.Logging;
  4. using Ocelot.Middleware;
  5. using Ocelot.Requester;
  6. using System.Threading.Tasks;
  7. namespace Czar.Gateway.Requester.Middleware
  8. {
  9. /// <summary>
  10. /// 金焰的世界
  11. /// 2018-12-10
  12. /// 自定义请求中间件
  13. /// </summary>
  14. public class CzarHttpRequesterMiddleware : OcelotMiddleware
  15. {
  16. private readonly OcelotRequestDelegate _next;
  17. private readonly IHttpRequester _requester;
  18. public CzarHttpRequesterMiddleware(OcelotRequestDelegate next,
  19. IOcelotLoggerFactory loggerFactory,
  20. IHttpRequester requester)
  21. : base(loggerFactory.CreateLogger<CzarHttpRequesterMiddleware>())
  22. {
  23. _next = next;
  24. _requester = requester;
  25. }
  26. public async Task Invoke(DownstreamContext context)
  27. {
  28. var response = await _requester.GetResponse(context);
  29. if (response.IsError)
  30. {
  31. Logger.LogDebug("IHttpRequester returned an error, setting pipeline error");
  32. SetPipelineError(context, response.Errors);
  33. return;
  34. }
  35. else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK)
  36. {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息
  37. if (response.Data.StatusCode == System.Net.HttpStatusCode.BadRequest)
  38. {//提取Ids4相关的异常(400)
  39. var result = await response.Data.Content.ReadAsStringAsync();
  40. JObject jobj = JObject.Parse(result);
  41. var errorMsg = jobj["error"]?.ToString();
  42. var error = new IdentityServer4Error(errorMsg??"未知异常");
  43. SetPipelineError(context, error);
  44. return;
  45. }
  46. else
  47. {
  48. var error = new InternalServerError($"请求服务异常");
  49. Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}");
  50. SetPipelineError(context, error);
  51. return;
  52. }
  53. }
  54. Logger.LogDebug("setting http response message");
  55. context.DownstreamResponse = new DownstreamResponse(response.Data);
  56. }
  57. }
  58. }

改造完成后,我们随时请求认证记录,最终显示效果如下。

奈斯,输出风格统一啦,这样就完美的解决了两个组件的输出问题,终于可以开心的使用啦。

四、内容总结

本篇我们详细的介绍了客户端授权的原理和支持的两个授权的方式,并各自演示了调用方式,然后知道了如何在数据库端新开通一个客户端的信息,然后介绍了配合网关实现客户端的授权和认证,并再次介绍了网关端的路由配置情况,最后介绍了如何把网关和认证统一输出格式,便于我们在正式环境的使用,涉及内容比较多,如果中间实现的有不对的地方,也欢迎大家批评指正。

【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权的更多相关文章

  1. 【.NET Core项目实战-统一认证平台】第十四章 授权篇-自定义授权方式

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何强制令牌过期的实现,相信大家对IdentityServer4的验证流程有了更深的了解,本篇我将介绍如何使用自定义的授权方 ...

  2. 【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4密码授权模式,从使用场景.原理分析.自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本 ...

  3. 【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.什么是RPC RPC是"远程调用(Remote Procedure Call)"的一个名称的缩写,并不是任何规范化的 ...

  4. 【.NET Core项目实战-统一认证平台】第十五章 网关篇-使用二级缓存提升性能

    [.NET Core项目实战-统一认证平台]开篇及目录索引 一.背景 首先说声抱歉,可能是因为假期综合症(其实就是因为懒哈)的原因,已经很长时间没更新博客了,现在也调整的差不多了,准备还是以每周1-2 ...

  5. 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. ...

  6. 【.NET Core项目实战-统一认证平台】第十一章 授权篇-密码授权模式

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4客户端授权的原理及如何实现自定义的客户端授权,并配合网关实现了统一的授权异常返回值和权限配置等相关功能,本篇将介绍 ...

  7. 【.NET Core项目实战-统一认证平台】第九章 授权篇-使用Dapper持久化IdentityServer4

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了IdentityServer4的源码分析的内容,让我们知道了IdentityServer4的一些运行原理,这篇将介绍如何使用d ...

  8. 【.NET Core项目实战-统一认证平台】第八章 授权篇-IdentityServer4源码分析

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上实现客户端自定义限流功能,基本完成了关于网关的一些自定义扩展需求,后面几篇将介绍基于IdentityServer ...

  9. 【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流

    [.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们 ...

随机推荐

  1. mysql学习3

    1.索引 索引是表的目录,在查找内容之前可以先在目录中查找索引位置,以此快速定位查询数据.对于索引, 会保存在额外的文件中. 作用: 约束 加速查找 1.1.建立索引 a.额外的文件保存特殊的数据结构 ...

  2. 转 Singleton clr via c#3

    /// <summary> /// 注意 只有在构造器没有副作用的时候才能使用这个技术 /// </summary> class Singleton { private sta ...

  3. js生成自定义随机数方法

    function getRandom() { var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', ...

  4. JavaScript递归函数解“汉诺塔”

    “汉诺塔”是一个著名的益智游戏.塔上有3根柱子和一套直径各不相同的空心圆盘.开始时柱子上的所有圆盘都按照从小到大的顺序堆叠.目标是通过每次移动一个圆盘到另一根柱子,最终把一堆圆盘移动到目标柱子上,过程 ...

  5. 实战深度学习(上)OpenCV库

    在如今人工智能的浪潮下,无数模拟机器学习和深度学习的开发者工具倍出,其中在计算机图形学和计算机视觉里面最流行的一个库就是OpenCV库了.计算机图形学和计算机视觉学在我们的自动驾驶和仿生机器人当中有着 ...

  6. Nginx限流配置

    电商平台营销时候,经常会碰到的大流量问题,除了做流量分流处理,可能还要做用户黑白名单.信誉分析,进而根据用户ip信誉权重做相应的流量拦截.限制流量.Nginx自身有的请求限制模块ngx_http_li ...

  7. TCP与UDP区别总结

    TCP与UDP区别总结:1.TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接2.TCP提供可靠的服务.也就是说,通过TCP连接传送的数据,无差错,不丢失,不重 ...

  8. 分门别类总结Java中的各种锁,让你彻底记住

    概念 公平锁/非公平锁 公平锁是指多个线程按照申请锁的顺序来获取锁. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁.有可能,会造成优先级反转或者饥 ...

  9. Android单元测试之四:仪器化测试

    Android单元测试之四:仪器化测试 仪器化测试 在某些情况下,虽然可以通过模拟的手段来隔离 Android 依赖,但代价很大,这种情况下可以考虑仪器化的单元测试,有助于减少编写和维护模拟代码所需的 ...

  10. [Swift]LeetCode82. 删除排序链表中的重复元素 II | Remove Duplicates from Sorted List II

    Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numb ...