• 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权


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

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

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

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

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

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

    private async Task<IEndpointResult> 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);
    }
    

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

    /// <summary>
            ///验证客户端授权结果
            /// </summary>
            /// <param name="context">请求上下文</param>
            /// <returns></returns>
            public async Task<ClientSecretValidationResult> 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的实现思路,详细代码如下。

    /// <summary>
    /// 检查上下文获取授权信息
    /// </summary>
    /// <param name="context">The HTTP context.</param>
    /// <returns></returns>
    public async Task<ParsedSecret> 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时支持哪几种客户端授权方式。

    /// <summary>
    /// 添加默认的授权分析
    /// </summary>
    /// <param name="builder">The builder.</param>
    /// <returns></returns>
    public static IIdentityServerBuilder AddDefaultSecretParsers(this IIdentityServerBuilder builder)
    {
    builder.Services.AddTransient<ISecretParser, BasicAuthenticationSecretParser>();
    builder.Services.AddTransient<ISecretParser, PostBodySecretParser>();
    
    return builder;
    }
    

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

    1. BasicAuthenticationSecretParser

      public Task<ParsedSecret> ParseAsync(HttpContext context)
      {
          _logger.LogDebug("Start parsing Basic Authentication secret");
      
          var notfound = Task.FromResult<ParsedSecret>(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加密。然后请求后显示如图所示结果,奈斯,得到我们授权的结果。

    2. PostBodySecretParser

      public async Task<ParsedSecret> 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_idclient_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<TokenRequestValidationResult> 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-configuration','[{"Host": "localhost","Port": 6611 }]',
    '','','','','','','',0,1);
    --4、加入全局配置
    INSERT INTO AhphConfigReRoutes VALUES(1,4);
    
    --5、增加认证配置地址路由
    insert into AhphReRoute values(1,'/.well-known/openid-configuration/jwks','[ "GET" ]','','http','/.well-known/openid-configuration/jwks','[{"Host": "localhost","Port": 6611 }]',
    '','','','','','','',0,1);
    --6、加入全局配置
    INSERT INTO AhphConfigReRoutes VALUES(1,5);
    

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

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

    2、添加客户端授权访问

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

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

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

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

    三、如何统一输出结果?

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

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

    /// <summary>
    /// 金焰的世界
    /// 2018-12-10
    /// 信息输出基类
    /// </summary>
    public class BaseResult
    {
        public BaseResult(int _errCode,string _errMsg)
        {
            errCode = _errCode;
            errMsg = _errMsg;
        }
        public BaseResult()
        {
    
        }
        /// <summary>
        /// 错误类型标识
        /// </summary>
        public int errCode { get; set; }
    
        /// <summary>
        /// 错误类型说明
        /// </summary>
        public string errMsg { get; set; }
    }
    
    /// <summary>
    /// 金焰的世界
    /// 2018-12-10
    /// 默认成功结果
    /// </summary>
    public class SuccessResult : BaseResult
        {
            public SuccessResult() : base(0, "成功")
            {
    
            }
        }
    

    1、网关默认输出改造

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

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

    using Czar.Gateway.Configuration;
    using Microsoft.AspNetCore.Http;
    using Ocelot.Errors;
    using Ocelot.Logging;
    using Ocelot.Middleware;
    using Ocelot.Responder;
    using System.Collections.Generic;
    using System.Net;
    using System.Threading.Tasks;
    
    namespace Czar.Gateway.Responder.Middleware
    {
        /// <summary>
        /// 金焰的世界
        /// 2018-12-10
        /// 统一输出中间件
        /// </summary>
        public class CzarResponderMiddleware: OcelotMiddleware
        {
            private readonly OcelotRequestDelegate _next;
            private readonly IHttpResponder _responder;
            private readonly IErrorsToHttpStatusCodeMapper _codeMapper;
    
            public CzarResponderMiddleware(OcelotRequestDelegate next,
                IHttpResponder responder,
                IOcelotLoggerFactory loggerFactory,
                IErrorsToHttpStatusCodeMapper codeMapper
               )
                : base(loggerFactory.CreateLogger<CzarResponderMiddleware>())
            {
                _next = next;
                _responder = responder;
                _codeMapper = codeMapper;
            }
    
            public async Task Invoke(DownstreamContext context)
            {
                await _next.Invoke(context);
    
                if (context.IsError)
                {//自定义输出结果
                    var errmsg = context.Errors[0].Message;
                    int httpstatus = _codeMapper.Map(context.Errors);
                    var errResult = new BaseResult() { errcode = httpstatus, errmsg = errmsg };
                    var message = errResult.ToJson();
                    context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
                    await context.HttpContext.Response.WriteAsync(message);
                    return;
                }
                else if (context.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK)
                {//标记失败,不做任何处理,自定义扩展时预留
                    
                }
                else if (context.DownstreamResponse == null)
                {//添加如果管道强制终止,不做任何处理,修复未将对象实例化错误
    
                }
                else
                {//继续请求下游地址返回
                    Logger.LogDebug("no pipeline errors, setting and returning completed response");
                    await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse);
                }
            }
    
            private void SetErrorResponse(HttpContext context, List<Error> errors)
            {
                var statusCode = _codeMapper.Map(errors);
                _responder.SetErrorResponseOnContext(context, statusCode);
            }
        }
    }
    

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

    using Ocelot.Middleware.Pipeline;
    
    namespace Czar.Gateway.Responder.Middleware
    {
        /// <summary>
        /// 金焰的世界
        /// 2018-12-10
        /// 应用输出中间件扩展
        /// </summary>
        public static class CzarResponderMiddlewareExtensions
        {
            public static IOcelotPipelineBuilder UseCzarResponderMiddleware(this IOcelotPipelineBuilder builder)
            {
                return builder.UseMiddleware<CzarResponderMiddleware>();
            }
        }
    }
    

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

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

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

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

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

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

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

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

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

    2、认证的统一输出改造

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

    using Czar.Gateway.Errors;
    using Newtonsoft.Json.Linq;
    using Ocelot.Logging;
    using Ocelot.Middleware;
    using Ocelot.Requester;
    using System.Threading.Tasks;
    
    namespace Czar.Gateway.Requester.Middleware
    {
        /// <summary>
        /// 金焰的世界
        /// 2018-12-10
        /// 自定义请求中间件
        /// </summary>
        public class CzarHttpRequesterMiddleware : OcelotMiddleware
        {
            private readonly OcelotRequestDelegate _next;
            private readonly IHttpRequester _requester;
    
            public CzarHttpRequesterMiddleware(OcelotRequestDelegate next,
                IOcelotLoggerFactory loggerFactory,
                IHttpRequester requester)
                    : base(loggerFactory.CreateLogger<CzarHttpRequesterMiddleware>())
            {
                _next = next;
                _requester = requester;
            }
    
            public async Task Invoke(DownstreamContext context)
            {
                var response = await _requester.GetResponse(context);
    
                if (response.IsError)
                {
                    Logger.LogDebug("IHttpRequester returned an error, setting pipeline error");
    
                    SetPipelineError(context, response.Errors);
                    return;
                }
                else if(response.Data.StatusCode != System.Net.HttpStatusCode.OK)
                {//如果后端未处理异常,设置异常信息,统一输出,防止暴露敏感信息
                    if (response.Data.StatusCode == System.Net.HttpStatusCode.BadRequest)
                    {//提取Ids4相关的异常(400)
                        var result = await response.Data.Content.ReadAsStringAsync();
                        JObject jobj = JObject.Parse(result);
                        var errorMsg = jobj["error"]?.ToString();
                       var error = new IdentityServer4Error(errorMsg??"未知异常");
                        SetPipelineError(context, error);
                        return;
                    }
                    else
                    {
                        var error = new InternalServerError($"请求服务异常");
                        Logger.LogWarning($"路由地址 {context.HttpContext.Request.Path} 请求服务异常. {error}");
                        SetPipelineError(context, error);
                        return;
                    }
                }
                Logger.LogDebug("setting http response message");
    
                context.DownstreamResponse = new DownstreamResponse(response.Data);
            }
        }
    }
    

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

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

    四、内容总结

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

  • 相关阅读:
    webpack 入门
    javascript 函数重载另一种实现办法
    5个python爬虫教材,让小白也有爬虫可写,含视频教程!
    简书模拟登陆缺陷!!!
    Python操作Mongodb
    【爬虫系列之一】爬虫开发环境的搭建
    CentOS7.4,anaconda3,python3.6,tensorflow环境下gdal的编译和问题解决
    返回Json格式结果
    json扩展
    EF中使用SQL语句或存储过程
  • 原文地址:https://www.cnblogs.com/jackcao/p/10100621.html
Copyright © 2020-2023  润新知