第三节:必备中间件集成2(缓存、认证授权、自定义黑名单、日志等)

一. 缓存

 参考文章:

 (1). Asp.Net Core内存缓存:https://www.cnblogs.com/yaopengfei/p/11043337.html

 (2). Asp.Net Core分布式缓存(SQLServer和Redis):https://www.cnblogs.com/yaopengfei/p/11121984.html

 (3). Redis系列:https://www.cnblogs.com/yaopengfei/p/13870561.html

 (4).CSRedisCore的用法:https://www.cnblogs.com/yaopengfei/p/14211883.html

1. 说明

 Asp.Net Core默认的缓存Api相对单调,我们通常直接调用Redis进行处理,这里我们在YpfCore.Utils统一封装,封装了CSRedisCore和StackExchange.Redis两个程序集来调用Redis,推荐使用CSRedisCore程序集,基于该程序集这里既初始化Core Mvc框架的缓存,也实例化了CSRedisCore的全局调用对象。

 需要安装的程序集有:【CSRedisCore】【Caching.CSRedis】和【StackExchange.Redis】

策略类CacheStrategyExtensions代码

    /// <summary>
    /// 缓存策略扩展
    /// </summary>
    public static class CacheStrategyExtensions
    {
        /// <summary>
        /// 添加缓存类型
        /// (最后无论哪种模式,都把AddMemoryCache注入,方便单独使用IMemoryCache)(视情况而定)
        /// </summary>
        /// <param name="services"></param>
        /// <param name="CacheType">有4种取值 (Redis:代表基于CSRedisCore使用redis缓存, 并实例化redis相关对象. Memory:代表使用内存缓存; 
        /// StackRedis: 代表基于StackExchange.Redis初始化; "null":表示什也不注入)</param>
        /// <returns></returns>
        public static IServiceCollection AddCacheStrategy(this IServiceCollection services, string CacheType)
        {
            switch (CacheType)
            {
                case "Memory": services.AddDistributedMemoryCache(); break;
                case "Redis":
                    {
                        //基于CSRedisCore初始化
                        //初始化redis的两种使用方式
                        var csredis = new CSRedisClient(ConfigHelp.GetString("RedisStr"));
                        services.AddSingleton(csredis);
                        RedisHelper.Initialization(csredis);

                        //初始化缓存基于redis
                        services.AddSingleton<IDistributedCache>(new CSRedisCache(csredis));
                    }; break;
                case "StackRedis":
                    {
                        //基于StackExchange.Redis初始化(该程序集这里不初始化缓存)
                        var connectionString = ConfigHelp.GetString("RedisStr");
                        //int defaultDB = Convert.ToInt32(ConfigHelp.GetString("RedisStr:defaultDB"));
                        services.AddSingleton(new SERedisHelp(connectionString));
                    }; break;
                case "null":
                    {
                        //什么也不注入
                    }; break;
                default: throw new Exception("缓存类型无效");
            }
            //最后都把AddMemoryCache注入,方便单独使用IMemoryCache进行内存缓存(视情况而定)
            //services.AddMemoryCache();

            return services;
        }
    }

StackExchange.Redis相关代码封装

    /// <summary>
    /// redis链接帮助类 
    /// 基于程序集:StackExchange.Redis
    /// </summary>
    public class SERedisHelp
    {

        private string _connectionString; //连接字符串
        private int _defaultDB; //默认数据库
        private readonly ConnectionMultiplexer connectionMultiplexer;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="connectionString"></param>
        /// <param name="defaultDB">默认使用Redis的0库</param>
        public SERedisHelp(string connectionString, int defaultDB = 0)
        {
            _connectionString = connectionString;
            _defaultDB = defaultDB;
            connectionMultiplexer = ConnectionMultiplexer.Connect(_connectionString);
        }

        /// <summary>
        /// 获取数据库
        /// </summary>
        /// <returns></returns>
        public IDatabase GetDatabase()
        {
            return connectionMultiplexer.GetDatabase(_defaultDB);
        }
    }
View Code

ConfigureService中注入

//添加redis实例化配置和缓存策略
services.AddCacheStrategy(_Configuration["CacheType"]);

配置文件

  //缓存类型
  //有4种取值 (Redis:代表基于CSRedisCore使用redis缓存, 并实例化redis相关对象. Memory:代表使用内存缓存; StackRedis: 代表基于StackExchange.Redis初始化; "null":表示什也不注入)
  "CacheType": "null",
  "RedisStr": "xxx.45.xxx.249:6379,password=123456,defaultDatabase=0"

2. 测试

 将配置文件中的CacheType类型改为“Redis”,然后在控制器中注入IDistributedCache调用缓存Api  或  直接使用RedisHelper类操控Redis各种数据结构即可。

代码分享:

            {
                //1.缓存的用法(redis or 内存主要看配置)
                _Cache.SetString("name1", "ypf1");
                var data1 = _Cache.GetString("name1");

                //2. redis的操控
                RedisHelper.HSet("myhash", "name2", "ypf2");
                var data2 = RedisHelper.HGet("myhash", "name2");
            }

二. 认证授权

参考文章:

 (1). 关于jwt的认证授权:https://www.cnblogs.com/yaopengfei/p/12162507.html

 (2). 关于grpc的认证授权:https://www.cnblogs.com/yaopengfei/p/13403001.html

 (3). 补充集中其它校验方式:https://www.cnblogs.com/yaopengfei/p/10468728.html

 (4). 基于IDS4相关:https://www.cnblogs.com/yaopengfei/p/12885217.html

1.说明

 目前该系统主要做两层校验,是否登录(前后端不分离的时候使用,借助Session),是否合法(jwt校验,可以用于前后端分离或者不分离)。

(1). 是否登录校验思路

 A. 登录成功后,将部分用户信息存入Session。

 B. 编写SkipLogin特性用于标记方法跳过登录校验。

 C. 编写过滤器,判断Session中是否有值,从而决定继续 or 驳回。(区分是否是Ajax请求)

 D. 过滤器可以配置全局或者作用于Controller 、Action.

(2). 是否合法校验思路

 A. 登录成功后,将所需信息存放到PayLoad中,然后进行Jwt加密,将加密字符串返回给客户端,客户端后续请求需要携带该Jwt字符串。

 B. 编写SkipJwt特性用于标记方法跳过登录校验。

 C. 编写过滤器,判断Jwt是否合法、是否过期等,从而决定继续 or 驳回。(区分是否是Ajax请求)

 D. 过滤器可以配置全局或者作用于Controller 、Action.

2.代码实操

校验登录过滤器:

 /// <summary>
    /// 校验是否登录的过滤器
    /// </summary>
    public class CheckLogin : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            //也可以这样获取Session,就不需要注入了。
            var _session = context.HttpContext.Session;
            //判断action是否有skip特性
            #region 老写法
            {
                //bool isHasAttr = false;
                ////目标对象上所有特性
                //var data = context.ActionDescriptor.EndpointMetadata.ToList();
                //string attrName = typeof(SkipAttribute).ToString();
                ////循环比对是否含有skip特性
                //for (int i = 0; i < data.Count; i++)
                //{
                //    if (data[i].ToString().Equals(attrName))
                //    {
                //        isHasAttr = true;
                //        break;
                //    }
                //}
            }
            #endregion

            var isHasAttr = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAllAttribute)|| x.GetType() == typeof(SkipLoginAttribute));
            if (isHasAttr == false)   //表示需要校验,反之不需要校验,正常走业务
            {
                //判断是否登录
                var userId = _session.GetString("userId");
                if (string.IsNullOrEmpty(userId))
                {
                    //表示没有值,校验没有通过
                    //判断请求类型
                    if (IsAjaxRequest(context.HttpContext.Request))
                    {
                        //表示是ajax请求
                        //context.Result = new JsonResult(new { status = "error", msg = "您没有登录" });
                        context.Result = new ContentResult() { StatusCode = 401, Content = "您没有登录" };
                        return;
                    }
                    else
                    {
                        context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noLogin");
                        return;
                    }
                }
            }
        }

        /// <summary>
        /// 判断该请求是否是ajax请求
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private bool IsAjaxRequest(HttpRequest request)
        {
            string header = request.Headers["X-Requested-With"];
            return "XMLHttpRequest".Equals(header);
        }
    }
View Code

校验jwt的过滤器

/// <summary>
    /// JWT校验过滤器
    /// </summary>
    public class CheckJWT : ActionFilterAttribute
    {
        private IConfiguration _configuration;
        public CheckJWT(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        public override void OnActionExecuting(ActionExecutingContext context)
        {        
            //判断action是否有skip特性
            #region 老写法
            {
                //bool isHasAttr = false;
              ////目标对象上所有特性
              //var data = context.ActionDescriptor.EndpointMetadata.ToList();
              //string attrName = typeof(SkipAttribute).ToString();
              ////循环比对是否含有skip特性
              //for (int i = 0; i < data.Count; i++)
              //{
              //    if (data[i].ToString().Equals(attrName))
              //    {
              //        isHasAttr = true;
              //        break;
              //    }
              //}
            }
            #endregion

            
            var isHasAttr = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAllAttribute) || x.GetType() == typeof(SkipJwtAttribute));
            if (isHasAttr==false)   //表示需要校验,反之不需要校验,正常走业务   
            {
                var actionContext = context.HttpContext;
                if (IsAjaxRequest(actionContext.Request))
                {
                    //表示是ajax请求,则auth从Header中传过来
                    var token = actionContext.Request.Headers["auth"].ToString();
                    if (token == "null" || string.IsNullOrEmpty(token))
                    {
                        //context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数为空" });
                        context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,参数为空" };
                        return;
                    }
                    //校验auth的正确性
                    var result = JWTHelp.JWTJieM(token, _configuration["JWTSecret"]);
                    if (result == "expired")
                    {
                        //context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });
                        context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,参数已经过期" };
                        return;
                    }
                    else if (result == "invalid")
                    {
                        //context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                        context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,未通过校验" };
                        return;
                    }
                    else if (result == "error")
                    {
                        //context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
                        context.Result = new ContentResult() { StatusCode = 401, Content = "非法请求,未通过校验" };
                        return;

                    }
                    else
                    {
                        //表示校验通过,用于向控制器中传值
                        context.RouteData.Values.Add("auth", result);
                    }

                }
                else
                {
                    //表示是非ajax请求,则auth拼接在参数中传过来
                    var token = actionContext.Request.Query["auth"].ToString();
                    if (string.IsNullOrEmpty(token))
                    {
                        context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noPer");
                        return;
                    }
                    //校验auth的正确性
                    var result = JWTHelp.JWTJieM(token, _configuration["JWTSecret"]);
                    if (result == "expired")
                    {
                        context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noPer");
                        return;
                    }
                    else if (result == "invalid")
                    {
                        context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noPer");
                        return;
                    }
                    else if (result == "error")
                    {
                        context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noPer");
                        return;
                    }
                    else
                    {
                        //表示校验通过,用于向控制器中传值
                        context.RouteData.Values.Add("auth", result);
                    }
                }
            }
        }
        /// <summary>
        /// 判断该请求是否是ajax请求
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        private bool IsAjaxRequest(HttpRequest request)
        {
            string header = request.Headers["X-Requested-With"];
            return "XMLHttpRequest".Equals(header);
        }
    }
View Code

3个跨过校验的特性标签

    /// <summary>
    /// 跨过系统所有校验
    /// </summary>
    public class SkipAllAttribute : Attribute
    {
    }
    /// <summary>
    /// 跨过JWT校验
    /// </summary>
    public class SkipJwtAttribute : Attribute
    {
    }
     /// <summary>
    /// 跨过登录校验
    /// </summary>
    public class SkipLoginAttribute : Attribute
    {
    }
View Code

登录业务 

        /// <summary>
        /// 校验登录
        /// </summary>
        /// <param name="userAccount">账号</param>
        /// <param name="passWord">密码</param>
        /// <returns></returns>
        [SkipAll]
        public IActionResult CheckLogin(string userAccount, string passWord)
        {
            try
            {
                //1.校验账号是否存在
                var userInfor = _baseService.Entities<T_SysUser>().Where(u => u.userAccount == userAccount).FirstOrDefault();
                if (userInfor != null)
                {
                    //2. 账号和密码是否匹配
                    var passWord1 = SecurityHelp.SHA(passWord);
                    if (passWord1.Equals(userInfor.userPwd, StringComparison.InvariantCultureIgnoreCase))
                    {
                        //3. 存入缓存
                        HttpContext.Session.SetString("userId", userInfor.id);
                        //4. 产生token进行返回
                        //过期时间(可以不设置,下面表示签名后 12个小时过期)
                        double exp = (DateTime.UtcNow.AddHours(12) - new DateTime(1970, 1, 1)).TotalSeconds;
                        //进行组装
                        var payload = new Dictionary<string, object>
                        {
                             {"userId", userInfor.id },
                             {"userAccount", userInfor.userAccount },
                             {"exp",exp }
                         };
                        var token = JWTHelp.JWTJiaM(payload, _configuration["JWTSecret"]);
                        //5.记录登录日志
                        T_SysLoginLog sysLoginLog = new T_SysLoginLog()
                        {
                            id = Guid.NewGuid().ToString("N"),
                            userId = userInfor.id,
                            userAccount = userInfor.userAccount,
                            loginTime = DateTime.Now,
                            delFlag = 0,
                            loginIp = HttpContext.Connection.RemoteIpAddress.ToString()
                        };
                        _baseService.Add(sysLoginLog);

                        return Json(new { status = "ok", msg = "登录成功", data = token });
                    }
                    else
                    {
                        //密码不正确
                        return Json(new { status = "error", msg = "密码不正确", data = "" });
                    }
                }
                else
                {
                    return Json(new { status = "error", msg = "账号不存在", data = "" });
                };
            }
            catch (Exception ex)
            {
                LogUtils.Error(ex); ;
                return Json(new { status = "error", msg = "登录失败", data = "" });
            }

        }
View Code

其它关于如何传值的问题,详见开篇的参考文章。

 

三. 自定义黑名单

1. 目的

 这里我们的主要目的是学习自定义中间件的写法,借助IP黑名单这个场景进行落实。

2. 实操

(1). 中间件代码

 /// <summary>
    /// 非法ip拦截中间件
    /// </summary>
    public class SafeIpMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly string _illegalIpList;
        public SafeIpMiddleware(RequestDelegate next, string IllegalIpList)
        {
            _illegalIpList = IllegalIpList;
            _next = next;
        }
        public async Task Invoke(HttpContext context)
        {
            if (context.Request.Method == "GET"|| context.Request.Method == "POST")
            {
                var remoteIp = context.Connection.RemoteIpAddress;    //获取远程访问IP
                string[] ip = _illegalIpList.Split(';');
                var bytes = remoteIp.GetAddressBytes();
                var badIp = false;
                foreach (var address in ip)
                {
                    var testIp = IPAddress.Parse(address);
                    if (testIp.GetAddressBytes().SequenceEqual(bytes))
                    {
                        badIp = true;
                        break;    //直接跳出ForEach循环
                    }
                }
                if (badIp)
                {
                    context.Response.StatusCode = 401;
                    return;
                }
            }
            await _next.Invoke(context);
        }
    }
View Code

(2). Configure中注入 

 //6. 自定义中间件,拦截非法ip
  app.UseMiddleware<SafeIpMiddleware>(_Configuration["IllegalIp"]);

(3). 配置文件 

  //非法ip集合
  "IllegalIp": "192.168.1.100;22.535.22.85",

四. 日志

 参考:

  Log4Net:https://www.cnblogs.com/yaopengfei/p/9428206.html   https://www.cnblogs.com/yaopengfei/p/10864412.html

  SeriLog:https://www.cnblogs.com/yaopengfei/p/14261414.html

PS:该框架后期日志将以SeriLog为主,逐步淘汰Log4Net。

1. Log4Net

(1). 帮助类

    /// <summary>
    /// 日志帮助类
    /// 依赖程序集:【log4net】
    /// </summary>
    public class LogUtils2
    {
        //日志仓储(单例模式,静态变量,程序在第一次使用的时候被调用,由clr保证)
        private static ILoggerRepository loggerRepository;
        //1. 适用于全部文件夹 (暂时不启用)
        public static ILog log;
        //2. OneLog文件夹
        public static ILog log1;
        //3. TwoLog文件夹
        public static ILog log2;

        //声明文件夹名称(这里分两个文件夹)
        static string log1Name = "WebLog";
        static string log2Name = "ApiLog";

        /// <summary>
        /// 初始化Log4net的配置
        /// xml文件一定要改为嵌入的资源
        /// </summary>
        public static void InitLog()
        {
            //1. 创建日志仓储(单例)
            loggerRepository = loggerRepository ?? LogManager.CreateRepository("myLog4net");
            //2. 加载xml文件
            Assembly assembly = Assembly.GetExecutingAssembly();
            //路径
            var xml = assembly.GetManifestResourceStream("YpfCore.Utils.Log.Log4net.log4net.xml");
            log4net.Config.XmlConfigurator.Configure(loggerRepository, xml);
            //3. 创建日志对象
            log = LogManager.GetLogger(loggerRepository.Name, "all");
            log1 = LogManager.GetLogger(loggerRepository.Name, log1Name);
            log2 = LogManager.GetLogger(loggerRepository.Name, log2Name);

        }


        /************************* 五种不同日志级别 *******************************/
        //FATAL(致命错误) > ERROR(一般错误) > WARN(警告) > INFO(一般信息) > DEBUG(调试信息)

        #region 00-将调试的信息输出,可以定位到具体的位置(解决高层封装带来的问题)
        /// <summary>
        /// 将调试的信息输出,可以定位到具体的位置(解决高层封装带来的问题)
        /// </summary>
        /// <returns></returns>
        private static string getDebugInfo()
        {
            StackTrace trace = new StackTrace(true);
            return trace.ToString();
        }
        #endregion

        #region 01-DEBUG(调试信息)
        /// <summary>
        /// DEBUG(调试信息)
        /// </summary>
        /// <param name="msg">日志信息</param>
        ///  <param name="logName">文件夹名称</param>
        public static void Debug(string msg, string logName = "")
        {
            if (logName == "")
            {
                log1.Debug(getDebugInfo() + msg);
            }
            else if (logName == log1Name)
            {
                log1.Debug(getDebugInfo() + msg);
            }
            else if (logName == log2Name)
            {
                log2.Debug(getDebugInfo() + msg);
            }
        }
        /// <summary>
        /// Debug
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="exception">错误信息</param>
        public static void Debug(string msg, Exception exception)
        {
            log.Debug(getDebugInfo() + msg, exception);
        }

        #endregion

        #region 02-INFO(一般信息)
        /// <summary>
        /// INFO(一般信息)
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="logName">文件夹名称</param>
        public static void Info(string msg, string logName = "")
        {
            if (logName == "")
            {
                log1.Info(msg);
            }
            else if (logName == log1Name)
            {
                log1.Info(msg);
            }
            else if (logName == log2Name)
            {
                log2.Info(msg);
            }
        }
        /// <summary>
        /// Info
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="exception">错误信息</param>
        public static void Info(string msg, Exception exception)
        {
            log.Info(getDebugInfo() + msg, exception);
        }
        #endregion

        #region 03-WARN(警告)
        /// <summary>
        ///WARN(警告)
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="logName">文件夹名称</param>
        public static void Warn(string msg, string logName = "")
        {
            if (logName == "")
            {
                log1.Warn(getDebugInfo() + msg);
            }
            else if (logName == log1Name)
            {
                log1.Warn(getDebugInfo() + msg);
            }
            else if (logName == log2Name)
            {
                log2.Warn(getDebugInfo() + msg);
            }
        }
        /// <summary>
        /// Warn
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="exception">错误信息</param>
        public static void Warn(string msg, Exception exception)
        {
            log.Warn(getDebugInfo() + msg, exception);
        }
        #endregion

        #region 04-ERROR(一般错误)
        /// <summary>
        /// ERROR(一般错误)
        /// </summary>
        /// <param name="ex">异常日志</param>
        /// <param name="logName">文件夹名称</param>
        public static void Error(Exception ex, string logName = "")
        {
            if (logName == "")
            {
                log1.Error(getDebugInfo() + ex.Message);
            }
            else if (logName == log1Name)
            {
                log1.Error(getDebugInfo() + ex.Message);
            }
            else if (logName == log2Name)
            {
                log2.Error(getDebugInfo() + ex.Message);
            }
        }
        /// <summary>
        /// Error
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="exception">错误信息</param>
        public static void Error(string msg, Exception exception)
        {
            log.Error(getDebugInfo() + msg, exception);
        }
        #endregion

        #region 05-FATAL(致命错误)
        /// <summary>
        /// FATAL(致命错误)
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="logName">文件夹名称</param>
        public static void Fatal(string msg, string logName = "")
        {
            if (logName == "")
            {
                log1.Fatal(getDebugInfo() + msg);
            }
            else if (logName == log1Name)
            {
                log1.Fatal(getDebugInfo() + msg);
            }
            else if (logName == log2Name)
            {
                log2.Fatal(getDebugInfo() + msg);
            }
        }
        /// <summary>
        /// Fatal
        /// </summary>
        /// <param name="msg">日志信息</param>
        /// <param name="exception">错误信息</param>
        public static void Fatal(string msg, Exception exception)
        {
            log.Fatal(getDebugInfo() + msg, exception);
        }

        #endregion


    }
View Code

(2). 配置文件(要改成嵌入的资源)

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <!-- 一. 添加log4net的自定义配置节点-->
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
  </configSections>
  <!--二. log4net的核心配置代码-->
  <log4net>
    <!--1. 输出途径(一) 将日志以回滚文件的形式写到文件中-->
    
    <!--模式一:全部存放到一个文件夹里-->
    <appender name="log0" type="log4net.Appender.RollingFileAppender">
      <!--1.1 文件夹的位置(也可以写相对路径)-->
      <param name="File"  value="D:CoreLog" />
      <!--相对路径-->
      <!--<param name="File"  value="Logs/" />-->
      <!--1.2 是否追加到文件-->
      <param name="AppendToFile" value="true" />
      <!--1.3 使用最小锁定模型(minimal locking model),以允许多个进程可以写入同一个文件 -->
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <!--1.4 配置Unicode编码-->
      <Encoding value="UTF-8" />
      <!--1.5 是否只写到一个文件里-->
      <param name="StaticLogFileName" value="false" />
      <!--1.6 配置按照何种方式产生多个日志文件 (Date:日期、Size:文件大小、Composite:日期和文件大小的混合方式)-->
      <param name="RollingStyle" value="Composite" />
      <!--1.7 介绍多种日志的的命名和存放在磁盘的形式-->
      <!--1.7.1 在根目录下直接以日期命名txt文件 注意&quot;的位置,去空格 -->
      <param name="DatePattern" value="yyyy-MM-dd&quot;.log&quot;" />
      <!--1.7.2 在根目录下按日期产生文件夹,文件名固定 test.log  -->
      <!--<param name="DatePattern" value="yyyy-MM-dd/&quot;test.log&quot;"  />-->
      <!--1.7.3 在根目录下按日期产生文件夹,这是按日期产生文件夹,并在文件名前也加上日期  -->
      <!--<param name="DatePattern" value="yyyyMMdd/yyyyMMdd&quot;-test.log&quot;"  />-->
      <!--1.7.4 在根目录下按日期产生文件夹,这再形成下一级固定的文件夹  -->
      <!--<param name="DatePattern" value="yyyyMMdd/&quot;OrderInfor/test.log&quot;"  />-->
      <!--1.8 配置每个日志的大小。【只在1.6 RollingStyle 选择混合方式与文件大小方式下才起作用!!!】可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志,
      超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。-->
      <param name="maximumFileSize" value="10MB" />
      <!--1.9 最多产生的日志文件个数,超过则保留最新的n个 将value的值设置-1,则不限文件个数 【只在1.6 RollingStyle 选择混合方式与文件大小方式下才起作用!!!】
        与1.8中maximumFileSize文件大小是配合使用的-->
      <param name="MaxSizeRollBackups" value="5" />
      <!--1.10 配置文件文件的布局格式,使用PatternLayout,自定义布局-->
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="记录时间:%date %n线程ID:[%thread] %n日志级别:%-5level %n出错类:%logger property: [%property{NDC}] - %n错误描述:%message%newline %n%newline"/>
      </layout>
    </appender>

    <!--模式二:分文件夹存放-->
    <!--文件夹1-->
    <appender name="log1" type="log4net.Appender.RollingFileAppender">
      <!--<param name="File"  value="D:CoreLogOneLog" />-->
      <!--改成存放到项目目录下了-->
      <param name="File"  value="Log/WebLog/" />
      <param name="AppendToFile" value="true" />
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <Encoding value="UTF-8" />
      <param name="StaticLogFileName" value="false" />
      <param name="RollingStyle" value="Composite" />
      <param name="DatePattern" value="yyyy-MM-dd&quot;.log&quot;" />
      <param name="maximumFileSize" value="10MB" />
      <param name="MaxSizeRollBackups" value="5" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="记录时间:%date %n线程ID:[%thread] %n日志级别:%-5level %n日志内容:%message%newline %n%newline"/>
      </layout>
      <!--下面是利用过滤器进行分文件夹存放,两种过滤器进行配合-->
      <!--与Logger名称(OneLog)匹配,才记录,-->
      <filter type="log4net.Filter.LoggerMatchFilter">
        <loggerToMatch value="WebLog" />
      </filter>
      <!--阻止所有的日志事件被记录-->
      <filter type="log4net.Filter.DenyAllFilter" />
    </appender>
    <!--文件夹2-->
    <appender name="log2" type="log4net.Appender.RollingFileAppender">
      <!--<param name="File"  value="D:CoreLogTwoLog" />-->
      <!--改成存放到项目目录下了-->
      <param name="File"  value="Log/ApiLog/" />
      <param name="AppendToFile" value="true" />
      <lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
      <Encoding value="UTF-8" />
      <param name="StaticLogFileName" value="false" />
      <param name="RollingStyle" value="Composite" />
      <param name="DatePattern" value="yyyy-MM-dd&quot;.log&quot;" />
      <param name="maximumFileSize" value="10MB" />
      <param name="MaxSizeRollBackups" value="5" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="记录时间:%date %n线程ID:[%thread] %n日志级别:%-5level  %n日志内容:%message%newline %n%newline"/>
      </layout>
      <!--下面是利用过滤器进行分文件夹存放,两种过滤器进行配合-->
      <!--与Logger名称(TwoLog)匹配,才记录,-->
      <filter type="log4net.Filter.LoggerMatchFilter">
        <loggerToMatch value="ApiLog" />
      </filter>
      <!--阻止所有的日志事件被记录-->
      <filter type="log4net.Filter.DenyAllFilter" />
    </appender>


    <!--2. 输出途径(二) 记录日志到数据库-->
    <appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
      <!--2.1 设置缓冲区大小,只有日志记录超设定值才会一块写入到数据库-->
      <param name="BufferSize" value="1" />
      <!--2.2 引用-->
      <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
      <!--2.3 数据库连接字符串-->
      <connectionString value="data source=localhost;initial catalog=LogDB;integrated security=false;persist security info=True;User ID=sa;Password=123456" />
      <!--2.4 SQL语句插入到指定表-->
      <commandText value="INSERT INTO LogInfor ([threadId],[log_level],[log_name],[log_msg],[log_exception],[log_time]) VALUES (@threadId, @log_level, @log_name, @log_msg, @log_exception,@log_time)" />
      <!--2.5 数据库字段匹配-->
      <!-- 线程号-->
      <parameter>
        <parameterName value="@threadId" />
        <dbType value="String" />
        <size value="100" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%thread" />
        </layout>
      </parameter>
      <!--日志级别-->
      <parameter>
        <parameterName value="@log_level" />
        <dbType value="String" />
        <size value="100" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%level" />
        </layout>
      </parameter>
      <!--日志记录类名称-->
      <parameter>
        <parameterName value="@log_name" />
        <dbType value="String" />
        <size value="100" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%logger" />
        </layout>
      </parameter>
      <!--日志信息-->
      <parameter>
        <parameterName value="@log_msg" />
        <dbType value="String" />
        <size value="5000" />
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%message" />
        </layout>
      </parameter>
      <!--异常信息  指的是如Infor 方法的第二个参数的值-->
      <parameter>
        <parameterName value="@log_exception" />
        <dbType value="String" />
        <size value="2000" />
        <layout type="log4net.Layout.ExceptionLayout" />
      </parameter>
      <!-- 日志记录时间-->
      <parameter>
        <parameterName value="@log_time" />
        <dbType value="DateTime" />
        <layout type="log4net.Layout.RawTimeStampLayout" />
      </parameter>
    </appender>


    <!--(二). 配置日志的的输出级别和加载日志的输出途径-->
    <root>
      <!--1. level中的value值表示该值及其以上的日志级别才会输出-->
      <!--OFF > FATAL(致命错误) > ERROR(一般错误) > WARN(警告) > INFO(一般信息) > DEBUG(调试信息)  > ALL  -->
      <!--OFF表示所有信息都不写入,ALL表示所有信息都写入-->
      <level value="ALL"></level>
      <!--2. append-ref标签表示要加载前面的日志输出途径代码  通过ref和appender标签的中name属性相关联-->
      
      <!--<appender-ref ref="AdoNetAppender"></appender-ref>-->
      <!--<appender-ref ref="log0"></appender-ref>-->
      <appender-ref ref="log1"></appender-ref>
      <appender-ref ref="log2"></appender-ref>
    </root>
  </log4net>

</configuration>
View Code

2. SeriLog

 给YpfCore.Utils层添加程序集【Serilog】【Serilog.Sinks.File】【Serilog.Sinks.Async】,然后封装LogUtils类,利用Filter过滤器实现分文件夹存储,在ConfigureService中进行初始化。这里仅封装一个Infor和Error方法,其它可自行封装。

代码分享:

    /// <summary>
    /// SeriLog帮助类
    /// </summary>
    public class LogUtils
    {
        static string log1Name = "WebLog";
        static string log2Name = "ApiLog";
        static string log3Name = "ErrorWebLog";
        static string log4Name = "ErrorApiLog";

        /// <summary>
        /// 初始化日志
        /// </summary>
        public static void InitLog()
        {
            //static string LogFilePath(string FileName) => $@"{AppContext.BaseDirectory}Log{FileName}log.log";   //bin目录下
            static string LogFilePath(string FileName) => $@"Log{FileName}log.log";
            string SerilogOutputTemplate = "{NewLine}Date:{Timestamp:yyyy-MM-dd HH:mm:ss.fff}{NewLine}LogLevel:{Level}{NewLine}Message:{Message}{NewLine}{Exception}" + new string('-', 100);
            Serilog.Log.Logger = new LoggerConfiguration()
                        .Enrich.FromLogContext()
                        .MinimumLevel.Debug() // 所有Sink的最小记录级别
                        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(Matching.WithProperty<string>("position", p => p == log1Name)).WriteTo.Async(a => a.File(LogFilePath(log1Name), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
                        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(Matching.WithProperty<string>("position", p => p == log2Name)).WriteTo.Async(a => a.File(LogFilePath(log2Name), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
                        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(Matching.WithProperty<string>("position", p => p == log3Name)).WriteTo.Async(a => a.File(LogFilePath(log3Name), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
                        .WriteTo.Logger(lg => lg.Filter.ByIncludingOnly(Matching.WithProperty<string>("position", p => p == log4Name)).WriteTo.Async(a => a.File(LogFilePath(log4Name), rollingInterval: RollingInterval.Day, outputTemplate: SerilogOutputTemplate)))
                        .CreateLogger();
        }


        /*****************************下面是不同日志级别*********************************************/
        // FATAL(致命错误) > ERROR(一般错误) > Warning(警告) > Information(一般信息) > DEBUG(调试信息)>Verbose(详细模式,即全部)


        /// <summary>
        /// 普通日志
        /// </summary>
        /// <param name="msg">日志内容</param>
        /// <param name="fileName">文件夹名称</param>
        public static void Info(string msg, string fileName = "")
        {
            if (fileName == "" || fileName == log1Name)
            {
                Serilog.Log.Information($"{{position}}:{msg}", log1Name);
            }
            else if (fileName == log2Name)
            {
                Serilog.Log.Information($"{{position}}:{msg}", log2Name);
            }
            else
            {
                //输入其他的话,还是存放到第一个文件夹
                Serilog.Log.Information($"{{position}}:{msg}", log1Name);
            }
        }

        /// <summary>
        /// 异常日志
        /// </summary>
        /// <param name="ex">Exception</param>
        /// <param name="fileName">文件夹名称</param>
        public static void Error(Exception ex, string fileName = "")
        {
            if (fileName == "" || fileName == log3Name)
            {

                Serilog.Log.Error(ex, "{position}:" + ex.Message, log3Name);
            }
            else if (fileName == log4Name)
            {

                Serilog.Log.Error(ex, "{position}:" + ex.Message, log4Name);
            }
            else
            {
                //输入其他的话,还是存放到第一个文件夹
                Serilog.Log.Error(ex, "{position}:" + ex.Message, log3Name);
            }
        }
    }
View Code

代码调用:

            {
                LogUtils.Info("我是二哈");
                LogUtils.Info("我是二哈1", "WebLog");//效果同上
                LogUtils.Info("我是二哈2", "ApiLog");
                try
                {
                    int.Parse("dsfsdf");
                }
                catch (Exception ex)
                {
                    LogUtils.Error(ex);
                    LogUtils.Error(ex, "ErrorWebLog");  //效果同上
                }
            } 

3. 最终整合 

 后续Log4net将彻底弃用,所以这里暂时不抽象接口来注入了,使用静态方法的模式简单粗暴,仅提供一个简单的策略用于选择使用哪种日志。

代码分享:

        /// <summary>
        /// 注册日志服务
        /// </summary>
        /// <param name="services"></param>
        /// <returns></returns>
        public static IServiceCollection AddLogStrategy(this IServiceCollection services, string logType = "SeriLog")
        { 
            if (logType == "Log4net")
            {
                LogUtils2.InitLog();
            }
            else
            {
                LogUtils.InitLog();
            }
            return services;
        }

ConfigureService注册:

//添加日志策略(SeriLog 或 Log4net)
services.AddLogStrategy(_Configuration["LogType"]);

配置文件: 

  //日志类型(SeriLog 或 Log4net)
  "LogType": "SeriLog"

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
原文地址:https://www.cnblogs.com/yaopengfei/p/14244416.html