Тело ответа пусто после обновления с Core 2.0 до 3.1.1 - PullRequest
2 голосов
/ 27 января 2020

ОБНОВЛЕНИЕ: если я изменяю return new OkObjectResult (token); вернуть Ok («тест»); тогда он не пустой.

Чтобы использовать API, необходимо вызвать AuthenticateAsyn c, который создает и возвращает токен.

    [AllowAnonymous]
    [HttpPost]
    public async Task<IActionResult> AuthenticateAsync([FromBody]string apiKey)
    {
        return await StopwatchLogger.LogExecutionTime($"Authenticate user: {apiKey}", async () =>
        {
            EnsureValidLoginRequest(apiKey);

            //Validate user credentials
            AuthenticationInfo authenticationInfo = await _authenticationService.AuthenticateUser(apiKey);
            if (!authenticationInfo.IsAuthenticated)
            {
                throw new RestApiExceptionNoLogging(ErrorCode.AuthenticationInvalidApiKey, "");
            }

            //Login current user and get the created token with claims identifying current user
            Token token = _authenticationService.LoginApiUser(HttpContext, authenticationInfo.SystemUser);

            return new OkObjectResult(token);
        });
    }

Токен создан правильно и из чего Я могу сказать, что все так же, как и раньше, за исключением того, что тело ответа становится пустым. Не выдается никаких ошибок.

РЕДАКТИРОВАТЬ: Добавление startup.cs

public class Startup
{
    private const string LogText = "An error occurred during startup.";

    private readonly List<Exception> _exceptions = new List<Exception>();

    private IWebHostEnvironment _HostingEnvironment;
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Configuration = configuration;
        _HostingEnvironment = env;
    }

    /// <summary>
    /// This method gets called by the runtime. Use this method to add services to the container.
    /// </summary>
    /// <param name="services"></param>
    /// <remarks>Is called before Configure</remarks>
    public void ConfigureServices(IServiceCollection services)
    {
        try
        {
            if (_exceptions.Any())
            {
                return;
            }
            // Add framework services. See configuration class (implementing IConfigureOptions<MvcOptions>) for specific implementation details.
            services.AddApiVersioning(o =>
            {
                o.ReportApiVersions = true;
                o.AssumeDefaultVersionWhenUnspecified = true;
                o.DefaultApiVersion = new ApiVersion(1, 0);
            });

            // Adds services required for using options.
            services.AddOptions();
            services.AddLogging(logging =>
            {
                logging.AddConsole();
                logging.AddDebug();
            });

            //Add scoped services (New service for every request)
            services.AddScoped<IJwtBearerService, JwtBearerService>();
            services.AddScoped<IApiAuthService, ApiAuthService>();
            services.AddScoped<ILoggingService, LoggingService>();


            services.AddScoped<IResponseDatabaseLogger, ResponseDatabaseLogger>();
            services.AddScoped<IRequestDatabaseLogger, RequestDatabaseLogger>();

            services.AddScoped<IAuthService, AuthService>();
            services.AddScoped<IOrderService, OrderService>();
            services.AddScoped<IApiOrderService, ApiOrderService>();
            services.AddScoped<IHaikomService, HaikomService>();

            services.AddDbContext<LoggingContext>(options => options.UseSqlServer(connection));
            services.AddDbContext<OrderContext>(options => options.UseSqlServer(connection));
            services.AddDbContext<AuthContext>(options => options.UseSqlServer(connection));

            ////Add application setting as options to enable dependency injection for settings
            services.Configure<Mediacreator.RestApi.Models.AuthenticationModel.Options.AuthenticationOptions>(options =>
            {
                // Get the setting from environment variable or user secret.
                // For more info: https://blogcarlosperez.com/2016/05/24/user-secrets/ eller http://asp.net-hacker.rocks/2016/07/11/user-secrets-in-aspnetcore.html
                options.JwtEncryptionSigningSecret = Configuration["Tokens:Key"];
                options.Audience = Configuration["Tokens:Issuer"];
                options.Issuer = Configuration["Tokens:Issuer"];
            });


            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton<IResponseFileLogger, ResponseFileLogger>();
            services.AddSingleton<IRequestFileLogger, RequestFileLogger>();

            var sp = services.BuildServiceProvider();

            // Resolve the services from the service provider
            var jwtBearerService = sp.GetService<IJwtBearerService>();

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
            {
                options.TokenValidationParameters = jwtBearerService.CreateTokenValidationParameters(Configuration["Tokens:Key"]);
            });

            services.Configure<IISOptions>(options =>
            {
                options.ForwardClientCertificate = false;
                options.AutomaticAuthentication = false;
            });

            services.AddMvc(options =>
            {
                //The services should only support JSON as output format for avoiding supporting for example XML
                //By specifying this property, the client will get a 406 error message if the accept header is for example "Accept: application/xml".
                options.ReturnHttpNotAcceptable = true;

                options.EnableEndpointRouting = false;

                //Enable events executed before, and after an action has been executed.
                options.Filters.Add(typeof(ActionFilter));

                //Make all controllers be protected by authorization
                var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();

                options.Filters.Add(new AuthorizeFilter(policy));
            });
         }
        catch (Exception ex)
        {
            _exceptions.Add(ex);
        }
    }

    /// <summary>
    /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    /// </summary>
    /// <param name="app"></param>
    /// <param name="env"></param>
    /// <param name="loggerFactory"></param>
    /// <remarks>Is called after ConfigureServices</remarks>
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
    {
        ILogger logger = null;

        try
        {
            NLog.LogManager.LoadConfiguration("NLog.config");

            loggerFactory.EnsureApiLoggersExists();
            logger = loggerFactory.CreateLogger(this.GetType());
            logger.LogInformation($"Current environment: {env.EnvironmentName}");

            //Check if any exceptions occurred during startup
            if (_exceptions.Any())
            {
                //Terminate the pipeline and return a response to the client
                app.Run(async httpContext =>
                {
                    await ExceptionHandler.HandleExceptionWriteErrorResponse(logger, httpContext, _exceptions.First(), LogText);
                });
            }

            //Important to add this middleware first as exceptions in other middleware are also to be caught
            app.UseCustomExceptionHandler();

            //Important to add this middleware before UseMvc is added as the middleware component otherwise may not be called for all requests.
            app.UseRequestLogger();
            app.UseResponseLogger();
            app.UseAuthentication();

            app.UseMvcWithDefaultRoute();

        }
        catch (Exception ex)
        {
            //Terminate the pipeline and return a response to the client
            app.Run(async httpContext =>
            {
                await ExceptionHandler.HandleExceptionWriteErrorResponse(logger, httpContext, ex, LogText);
            });
        }
    }


}

Мне пришлось изменить IHostingEnvironment на IWebHostEnvironment. В журналирование были внесены некоторые изменения, мне пришлось добавить options.EnableEndpointRouting = false для параметров MVC. Было еще несколько вещей, которые я не могу вспомнить из верхней части головы.

РЕДАКТИРОВАТЬ 2, добавив запись ответа:

public class ResponseLogger : MiddlewareBase
{
    private readonly IResponseFileLogger _IResponseLogger;
    public ResponseLogger(RequestDelegate nextRequestDelegate, ILoggerFactory loggerFactory, IResponseFileLogger respLogger) : base(nextRequestDelegate, loggerFactory)
    {
        _IResponseLogger = respLogger;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        //Adds an identifier to the response headers. The identifier is used for connecting a response to log posts for corresponding requests and responses.
        context.AddTraceIdentifierToResponseHeaders();

        //The original response body needs to be stored locally as it can not be rewind after reading and logging it.
        //http://stackoverflow.com/questions/37855384/log-httpresponse-body-for-asp-net-core-1-0-rest-api/38275942#38275942

        using (var responseBodyBufferStream = new MemoryStream())
        {
            //Store the original body stream in a local variable
            var responseBodyStream = context.Response.Body;

            try
            {
                //Replace the context response with the newly created buffer as the original stream is not readable.
                context.Response.Body = responseBodyBufferStream;

                //Invoke the rest of the pipeline
                await InvokeNextDelegate(context);

                //Reset the buffer so the content can be read
                responseBodyBufferStream.Seek(0, SeekOrigin.Begin);

                //Create a stream reader to be able to read the response
                using (var bufferStreamReader = new StreamReader(responseBodyBufferStream))
                {
                    //Read the body from the stream
                    string responseBody = await bufferStreamReader.ReadToEndAsync();

                    //Reset the buffer
                    responseBodyBufferStream.Seek(0, SeekOrigin.Begin);

                    //Copy the content to the original stream and put it back
                    await responseBodyBufferStream.CopyToAsync(responseBodyStream);

                    //var responseFileLogger = new ResponseFileLogger(LoggerFactory, context, responseBody);
                    _IResponseLogger.Log(context, responseBody);
                }
            }
            finally
            {
                //Ensure original body stream is is written back to the response body even if an exception occurs in another middleware.
                context.Response.Body = responseBodyStream;
            }
        }
    }
}

Я попытался закомментировать зашифрованный код ответа чтобы увидеть, не является ли это частью проблемы, но, кажется, не имеет значения из того, что я могу сказать.

РЕДАКТИРОВАТЬ 3, добавив вывод из запроса:

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/1.1 POST http://localhost:5000/api/v1.0/authentication application/json; charset=utf-8 41
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[3]
      Route matched with {action = "Authenticate", controller = "Authentication"}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.IActionResult] AuthenticateAsync(System.String) on controller Mediacreator.RestApi.Controllers.AuthenticationController (CoreRestApi).
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.1 initialized 'LoggingContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (70ms) [Parameters=[@p0='?' (Size = 4000), @p1='?' (Size = 4000), @p2='?' (Size = 4000), @p3='?' (DbType = DateTime2), @p4='?' (DbType = DateTime2), @p5='?' (DbType = Int32), @p6='?' (Size = 4000), @p7='?' (DbType = Int64), @p8='?' (Size = 4000), @p9='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [ApiLogs] ([Action], [RemoteAddress], [RequestData], [RequestDateUtc], [ResponseDateUtc], [ResponseStatusCode], [Status], [SystemUserId], [TraceIdentifier], [Url])
      VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9);
      SELECT [Id]
      FROM [ApiLogs]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: Microsoft.EntityFrameworkCore.Infrastructure[10403]
      Entity Framework Core 3.1.1 initialized 'AuthContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer' with options: None
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (22ms) [Parameters=[@__apiKey_0='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SELECT [t].[Id], [t].[ApiKey], [t].[Deleted], [t].[Description], [t].[IsActive], [t].[Password], [t].[Permissions], [t].[Username], [t0].[SystemUserId], [t0].[UserRoleId], [t0].[Id], [t0].[Description], [t0].[Name]
      FROM (
          SELECT TOP(2) [s].[Id], [s].[ApiKey], [s].[Deleted], [s].[Description], [s].[IsActive], [s].[Password], [s].[Permissions], [s].[Username]
          FROM [SystemUsers] AS [s]
          WHERE [s].[ApiKey] = @__apiKey_0
      ) AS [t]
      LEFT JOIN (
          SELECT [s0].[SystemUserId], [s0].[UserRoleId], [u].[Id], [u].[Description], [u].[Name]
          FROM [SystemUserRole] AS [s0]
          INNER JOIN [UserRoles] AS [u] ON [s0].[UserRoleId] = [u].[Id]
      ) AS [t0] ON [t].[Id] = [t0].[SystemUserId]
      ORDER BY [t].[Id], [t0].[SystemUserId], [t0].[UserRoleId], [t0].[Id]
info: Mediacreator.RestApi.Services.Authentication.ApiAuthService[0]
      User with ApiKey <xxx> authenticated
info: Mediacreator.RestApi.Services.Authentication.ApiAuthService[0]
      Token created for user '<yyy>' (5). Token is valid '3600' seconds
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (20ms) [Parameters=[@__logId_0='?' (DbType = Int64)], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [a].[Id], [a].[Action], [a].[RemoteAddress], [a].[RequestData], [a].[RequestDateUtc], [a].[ResponseDateUtc], [a].[ResponseStatusCode], [a].[Status], [a].[SystemUserId], [a].[TraceIdentifier], [a].[Url]
      FROM [ApiLogs] AS [a]
      WHERE [a].[Id] = @__logId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (20ms) [Parameters=[@p3='?' (DbType = Int64), @p0='?' (DbType = DateTime2), @p1='?' (DbType = Int32), @p2='?' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      UPDATE [ApiLogs] SET [ResponseDateUtc] = @p0, [ResponseStatusCode] = @p1, [Status] = @p2
      WHERE [Id] = @p3;
      SELECT @@ROWCOUNT;
info: Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor[1]
      Executing ObjectResult, writing value of type 'Mediacreator.RestApi.Models.AuthenticationModel.Entities.Token'.
info: Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker[2]
      Executed action Mediacreator.RestApi.Controllers.AuthenticationController.AuthenticateAsync (CoreRestApi) in 8305.5034ms
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished in 11069.4992ms 200 application/json; charset=utf-8

РЕДАКТИРОВАТЬ 4: Добавление класса Token

public class Token
{
    public readonly string access_token;
    public readonly string token_type = Constants.TokenTypeBearer;
    public readonly int expires_in;

    public Token(string encodedJsonWebToken, int expiresInSeconds)
    {
        access_token = encodedJsonWebToken;
        expires_in = expiresInSeconds;
    }
}

1 Ответ

3 голосов
/ 28 января 2020

Причина, по которой он не работал, заключалась в том, что @Ian Kemp предположил, что сериализация моего класса Token не работает. Я читал, что они добавили новую сериализацию с System.Text. Json в Core 3.0, и по какой-то причине она не работает с

public class Token
{
   public readonly string access_token;
   public readonly string token_type = Constants.TokenTypeBearer;
   public readonly int expires_in;

   public Token(string encodedJsonWebToken, int expiresInSeconds)
   {
      access_token = encodedJsonWebToken;
      expires_in = expiresInSeconds;
   }
}

Поэтому я решил попробовать Newtonsoft, и теперь она работает как прежде.

Решением было добавить это в ConfigureServices в Startup.cs

services.AddControllers().AddNewtonsoftJson();
...