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

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

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

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

1、如何添加客户端受权?

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

首先查看获取token的方式,核心代码以下。数据库

private async Task<IEndpointResult> ProcessTokenRequestAsync(HttpContext context)
{
    _logger.LogDebug("Start token request.");

    // 一、验证客户端及受权信息结果
    var clientResult = await _clientValidator.ValidateAsync(context);

    if (clientResult.Client == null)
    {
        return Error(OidcConstants.TokenErrors.InvalidClient);
    }

    // 二、验证请求结果
    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);
    }

    // 三、建立输出结果
    _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);

    // 四、返回结果
    _logger.LogDebug("Token request success.");
    return new TokenResult(response);
}

咱们须要详细分析下第一步客户端受权信息是如何验证的?核心代码以下。c#

/// <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的实现思路,详细代码以下。api

/// <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. BasicAuthenticationSecretParserapp

    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语句的方式来演示添加配置信息。详细的语句以下:

/*
    添加客户端脚本
*/
--一、添加客户端信息
INSERT INTO Clients(AccessTokenLifetime,ClientId,ClientName,Enabled) VALUES(3600,'clienta','测试客户端A',1);

--二、添加客户端密钥,密码为(secreta) sha256
INSERT INTO ClientSecrets VALUES(21,'',null,'SharedSecret','2tytAAysa0zaDuNthsfLdjeEtZSyWw8WzbzM8pfTGNI=');

--三、增长客户端受权权限
INSERT INTO ClientGrantTypes VALUES(21,'client_credentials');

--四、增长客户端可以访问scope
INSERT INTO ClientScopes VALUES(21,'mpc_gateway');

而后咱们来测试下新开通的客户端受权,以下图所示,能够正常获取受权信息了,另一种Basic受权方式可自行测试。

2、如何配合网关认证和受权?

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

咱们能够回忆下以前介绍网关篇时认证篇章,里面介绍的很是清楚。这里咱们参照刚才添加的客户端A为案例增长网关受权,由于咱们对外暴露的是网关地址,而不是内部具体认证项目地址。

一、添加网关受权路由

本项目的网关端口为7777,因此网关受权的地址为http://localhost:7777/connect/token,因为为添加网关路由,直接访问报401,咱们首先增长网关的路由信息。

-- 一、插入认证路由(使用默认分类)
insert into AhphReRoute values(1,'/connect/token','[ "POST" ]','','http','/connect/token','[{"Host": "localhost","Port": 6611 }]',
'','','','','','','',0,1);

--二、加入全局配置
INSERT INTO AhphConfigReRoutes VALUES(1,3)

--三、增长认证配置地址路由
insert into AhphReRoute values(1,'/.well-known/openid-configuration','[ "GET" ]','','http','/.well-known/openid-configuration','[{"Host": "localhost","Port": 6611 }]',
'','','','','','','',0,1);
--四、加入全局配置
INSERT INTO AhphConfigReRoutes VALUES(1,4);

--五、增长认证配置地址路由
insert into AhphReRoute values(1,'/.well-known/openid-configuration/jwks','[ "GET" ]','','http','/.well-known/openid-configuration/jwks','[{"Host": "localhost","Port": 6611 }]',
'','','','','','','',0,1);
--六、加入全局配置
INSERT INTO AhphConfigReRoutes VALUES(1,5);

经过PostMan测试,能够获得咱们预期的受权信息结果。

而后继续访问咱们以前配置的受权路由,提示401未受权,这块就涉及到前面网关篇的知识了,由于咱们的网关增长了受权,因此须要增长客户端受权才能访问。

二、添加客户端受权访问

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

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

使用咱们刚受权的信息,再次访问以前配置的须要认证的路由,能够获得咱们预期的结果,奈斯,和网关篇的内容彻底一致。

注意:在配置完信息后须要清理缓存,由于咱们以前作网关时,不少配置信息的读取使用了缓存。

3、如何统一输出结果?

做为一块准备应用到生产环境的产品,可能为各类第三方提供应用支持,那么统一的输出结果是必需要实现的,好比咱们使用微信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, "成功")
        {

        }
    }

一、网关默认输出改造

网关这段须要改造错误提示的代码和内容以及异常的输出结果,首先改造错误状况的输出结果,使用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":"请求服务异常"},获得了预期的全部目标,网关统一输出所有改造完毕。

二、认证的统一输出改造

这里为了统一风格,咱们先查看下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);
        }
    }
}

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

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

4、内容总结

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

相关文章
相关标签/搜索