Add user to the log context when using Serilog and Asp.Net Core

一世执手 提交于 2019-12-01 15:14:31

I was able to get the authenticated Active Directory user with just a few lines of code. I'm not very experienced with Core authentication, claims in particular, but perhaps this will get you on your way or at a minimum help others that come along with a similar problem to yours but with AD.

The key lines are Enrich.FromLogContext() and app.Use(async...

public class Startup
{
    public IConfigurationRoot Configuration { get; }

    public Startup(IHostingEnvironment env)
    {
        Log.Logger = new LoggerConfiguration()
                   .Enrich.FromLogContext() // Populates a 'User' property on every log entry
                   .WriteTo.MSSqlServer(Configuration.GetConnectionString("MyDatabase"), "Logs")
                   .CreateLogger();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.WithFilter(new FilterLoggerSettings
                                 {
                                     { "Default", LogLevel.Information },
                                     { "Microsoft", LogLevel.Warning },
                                     { "System", LogLevel.Warning }
                                 })
                     .AddSerilog();

        app.Use(async (httpContext, next) =>
                {
                    var userName = httpContext.User.Identity.IsAuthenticated ? httpContext.User.Identity.Name : "unknown";
                    LogContext.PushProperty("User", !String.IsNullOrWhiteSpace(userName) ? userName : "unknown");
                    await next.Invoke();
                });
    }
}

For AD Authentication via IIS/Kestrel the web.config requires a forwardWindowsAuthToken setting as follows:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <aspNetCore ... forwardWindowsAuthToken="true" />
  </system.webServer>
</configuration>
Nicholas Blumhardt

You need to invoke _next in a block like so:

public async Task Invoke(HttpContext httpContext)
{
    if (httpContext.User.Identity.IsAuthenticated)
    {
        var userFullName = (((ClaimsIdentity)httpContext.User.Identity).FindFirst(Member.FullnameClaimName).Value);
        var userName = "anyone@gmail.com";

        using (LogContext.PushProperty("Username", userName))
        using (LogContext.PushProperty("UserFullName", userFullName))
        {
            await _next(httpContext);
        }
    }
    else
    {
        await _next(httpContext);
    }
}

Your middleware is probably fine. But the order in which you configure the middleware is important. Your EnrichLogger middleware is the very first one. That means it runs before the authentication middleware. Move the app.EnrichLogger call to just below where you add the authentication middleware (probably app.UseAuthentication). This way, the HttpContext.User property will be properly set when your EnrichLogger middleware runs.

Update

Actually, even moving this middleware below the authentication middleware might not be enough. It seems that the identity may be set (at least in some configurations) within the MVC middleware. This means that you can't access the user identity from middleware until after your controller actions have executed (by moving it down after the MVC middleware). But this will be too late to be any use in your logs.

Instead, you may have to use an MVC filter to add the user information to the log context. For example, you might create a filter like this:

public class LogEnrichmentFilter : IActionFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LogEnrichmentFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void OnActionExecuting(ActionExecutingContext context)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (httpContext.User.Identity.IsAuthenticated)
        {
            LogContext.PushProperty("Username", httpContext.User.Identity.Name);
        }
        else
        {
            LogContext.PushProperty("Username", "Anonymous");
        }
    }

    public void OnActionExecuted(ActionExecutedContext context)
    {
    }
}

You could then apply your filter globally using DI. In your Services.cs file:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<LogEnrichmentFilter>();
        services.AddMvc(o =>
        {
            o.Filters.Add<LogEnrichmentFilter>();
        });
        ...
    }

Use the .Enrich.WithEnvironmentUserName() method on the LoggerConfiguration object, eg:

public static IWebHost BuildWebHost(string[] args) =>
  WebHost.CreateDefaultBuilder(args)
    .ConfigureLogging((hostingContext, config) =>
    {
      config.ClearProviders();
    })
    .UseStartup<Startup>()             
    .UseSerilog((ctx, cfg) =>
    {
      cfg.ReadFrom.Configuration(ctx.Configuration)
        .Enrich.WithEnvironmentUserName()
    })
    .Build();

I also have the following setting in web.config, although I haven't proved if it is required on all IIS servers or not:

<system.webServer><aspNetCore ... forwardWindowsAuthToken="true"> ...

Taken from here: Logging in ASP NET Core with Serilog

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!