无法使用 Serilog 从 HttpContext 访问用户声明以进行日志记录

Posted

技术标签:

【中文标题】无法使用 Serilog 从 HttpContext 访问用户声明以进行日志记录【英文标题】:Can't access User Claims for logging from HttpContext using Serilog 【发布时间】:2019-12-23 19:13:34 【问题描述】:

我在一个 ASP Net Core API 项目中使用 Serilog 作为我的记录器,我想记录接收到的访问令牌的内容(声明)。声明应该在 HttpContext 对象的 User 属性中。

问题是当我尝试记录它们不可用的声明时。但是,当我稍后在应用程序中访问声明时,我可以看到它们。在另一个项目中,我遇到了类似的问题,并注意到当我将 Microsoft 日志记录事件从信息转为警告时,我可以看到声明,但当我切换回信息时,它们消失了。

如何记录用户声明?这部分日志记录是否应该在 Startup.cs 中间件而不是 Program.cs 中完成?微软是否可能试图阻止 PII 的记录?我在这里不知所措。任何帮助将不胜感激。

来自 Program.cs:

       public static void Main(string[] args)
        
            Log.Logger = new LoggerConfiguration()
                .MinimumLevel.Debug()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .WriteTo.File(new JsonFormatter(), @".\logs\log-bsw-startup-.txt",
                    rollingInterval: RollingInterval.Day,
                    retainedFileCountLimit: null,
                    rollOnFileSizeLimit: true)
                 .CreateLogger();

            try
            
                Log.Information("Building and running web host.");

                CreateWebHostBuilder(args).Build().Run();
            
            catch(Exception ex)
            
                Log.Information("Could not start web host. @ex", ex);
            
            finally
            
                Log.Information("Closing web host.");
                Log.CloseAndFlush();
            
        

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseSerilog((provider, context, config) =>
                
                    SerilogConfiguration.Configure(provider, context, config);
                )
                .UseStartup<Startup>();
    

来自 SerilogConfiguration.cs 文件:

        public static void Configure(
            IServiceProvider provider, 
            WebHostBuilderContext context, 
            LoggerConfiguration config)
        
            var name = Assembly.GetExecutingAssembly().GetName();

            var aspNetCoreEnvironment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            if (aspNetCoreEnvironment == null)
                throw new Exception($"No ASPNETCORE_ENVIRONMENT found.");

            var connectionString = context.Configuration.GetConnectionString("Default");
            if (connectionString == null)
                throw new Exception($"No connection string in appsettings.json for ASPNETCORE_ENVIRONMENT 'aspNetCoreEnvironment'.");

            var batchPostingLimit = context.Configuration
                .GetSection("Logging")
                .GetValue("BatchPostingLimit", 1);

            var sqlLogTable = context.Configuration
                .GetSection("Logging")
                .GetValue<string>("SqlLogTable", null);
            if (sqlLogTable == null)
                throw new Exception("No SQL log table in appsettings.json");

            var options = new ColumnOptions();
            options.Store.Remove(StandardColumn.Properties); // Removes XML config
            options.Store.Add(StandardColumn.LogEvent); // Adds JSON config
            options.Id.ColumnName = "id";
            options.Message.ColumnName = "message";
            options.MessageTemplate.ColumnName = "message_template";
            options.TimeStamp.ColumnName = "timestamp";
            options.Exception.ColumnName = "exception";
            options.Level.ColumnName = "level";
            options.LogEvent.ColumnName = "log_event";
            options.LogEvent.ExcludeStandardColumns = true;

            config.MinimumLevel.Debug()
                .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                .MinimumLevel.Override("System", LogEventLevel.Warning)
                .MinimumLevel.Override("IdentityServer4", LogEventLevel.Warning)
                .Enrich.FromLogContext()
                .Enrich.WithMachineName()
                .Enrich.WithThreadId()
                .Enrich.WithThreadName()
                .Enrich.WithProcessId()
                .Enrich.WithProcessName()
                .Enrich.WithEnvironmentUserName()
                .Enrich.WithExceptionDetails()
                .Enrich.WithProperty("LogId", $"Guid.NewGuid()")
                .Enrich.WithProperty("Assembly", $"name.Name")
                .Enrich.WithProperty("Version", $"name.Version")
                .Enrich.WithProperty("AspNetCoreEnvironment", $"aspNetCoreEnvironment")
                .Enrich.WithAspnetcoreHttpcontext(provider, CustomHttpContextEnricher);

            config.WriteTo.MSSqlServer(connectionString, sqlLogTable, columnOptions: options,
                batchPostingLimit: batchPostingLimit,
                restrictedToMinimumLevel: LogEventLevel.Information);

            config.WriteTo.File(new JsonFormatter(), @".\logs\log-.txt",
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: null,
                rollOnFileSizeLimit: true);
        

        private static HttpContextProperties CustomHttpContextEnricher(
            IHttpContextAccessor httpContextAccessor)
        
            var context = httpContextAccessor.HttpContext;
            if (context == null)
                return null;

            var contextIndex = "http-properties";

            // Short circuit if there's already a context properties object available
            var contextProperties = context.Items[contextIndex] as HttpContextProperties;
            if (contextProperties != null)
                return contextProperties;

            var user = context.User;
            var isAuthenticated = false;
            string internalUserId = null;
            string email = null;
            var userClaims = new Dictionary<string, string>();
            var i = 1;

// Here's where I'm trying to grab the user claims. When I do logger.LogInformation I would think the user claims would be available but they aren't. 
            if (user != null)
            
                isAuthenticated = user.Identity.IsAuthenticated;

                var userId = "user_id";
                if (user.HasClaim(c => c.Type == userId))
                    internalUserId = user.Claims.FirstOrDefault(
                        c => c.Type == userId).Value.ToString();

                if (user.HasClaim(c => c.Type == JwtRegisteredClaimNames.Email))
                    email = user.Claims.FirstOrDefault(
                        c => c.Type == JwtRegisteredClaimNames.Email).Value.ToString();

                userClaims = user.Claims.ToDictionary(x => $"i++.ToString("D2")-x.Type", y => y.Value.ToString());
            

            var properties = new HttpContextProperties
            
                IpAddress = context.Connection.RemoteIpAddress.ToString(),
                Host = context.Request.Host.ToString(),
                Path = context.Request.Path.ToString(),
                IsHttps = context.Request.IsHttps,
                Scheme = context.Request.Scheme,
                Method = context.Request.Method,
                ContentType = context.Request.ContentType,
                Protocol = context.Request.Protocol,
                QueryString = context.Request.QueryString.ToString(),
                Query = context.Request.Query.ToDictionary(x => x.Key, y => y.Value.ToString()),
                Headers = context.Request.Headers
                    .Where(x => !(x.Key.ToString() == "Authorization")) // Remove from logs for security purposes
                    .ToDictionary(x => x.Key, y => y.Value.ToString()),
                UserClaims = userClaims,
                InternalUserId = internalUserId,
                Email = email,
                IsAuthenticated = isAuthenticated
            ;

            context.Items[contextIndex] = properties;

            return properties;
        

        public class HttpContextProperties
        
            public string Path  get; set; 
            public string Host  get; set; 
            public string Method  get; set; 
            public string IpAddress  get; set; 
            public bool IsHttps  get; set; 
            public string Scheme  get; set; 
            public string ContentType  get; set; 
            public string Protocol  get; set; 
            public string QueryString  get; set; 
            public Dictionary<string, string> Query  get; set; 
            public Dictionary<string, string> Headers  get; set; 
            public string InternalUserId  get; set; 
            public string Email  get; set; 
            public bool IsAuthenticated  get; set; 
            public Dictionary<string, string> UserClaims  get; set; 
        

【问题讨论】:

【参考方案1】:

我不确定这是否是答案,但它可能会有所帮助 - .Net Core 的东西目前有点神秘,所以这可能会对你有所帮助(你可能也有这个无论如何你的startup.cs):

我试图在我的 startup.cs 中利用管道以在加载 SPA 服务之前触发到身份服务器登录的重定向(我不希望用户甚至尝试加载 SPA,除非他们经过身份验证。

我发现我的 Startup.cs 中的以下代码(在配置方法中)中的 IsAuthenticate 始终为 false:

app.Use(async (context, next) =>

    if (!context.User.Identity.IsAuthenticated)
    
        await context.ChallengeAsync("Identity.Application");
    
    else
    
        await next();
    
); 

直到我将这段代码(在 ConfigureService 方法中)添加到我的启动中:

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddRoles<IdentityRole>()
    //The following two may not be as relevant as the first two.
    .AddDefaultUI()
    .AddEntityFrameworkStores<ApplicationDbContext>();

【讨论】:

以上是关于无法使用 Serilog 从 HttpContext 访问用户声明以进行日志记录的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 Serilog 登录 MS SQL

Serilog:无法使用 MongoDb 接收器登录到 MongoDb

Serilog 的 AddSerilog 无法识别

解决错误 - 无法解析“Serilog.ILogger”类型的服务

SeriLog 使用经典 .net 框架从 sql server 读取属性值

无法将类型字符串隐式转换为 serilog.formatting.ITextformatter