第三节:必备中间件集成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); } }
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); } }
校验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); } }
3个跨过校验的特性标签

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

/// <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 = "" }); } }
其它关于如何传值的问题,详见开篇的参考文章。
三. 自定义黑名单
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); } }
(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 }
(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文件 注意"的位置,去空格 --> <param name="DatePattern" value="yyyy-MM-dd".log"" /> <!--1.7.2 在根目录下按日期产生文件夹,文件名固定 test.log --> <!--<param name="DatePattern" value="yyyy-MM-dd/"test.log"" />--> <!--1.7.3 在根目录下按日期产生文件夹,这是按日期产生文件夹,并在文件名前也加上日期 --> <!--<param name="DatePattern" value="yyyyMMdd/yyyyMMdd"-test.log"" />--> <!--1.7.4 在根目录下按日期产生文件夹,这再形成下一级固定的文件夹 --> <!--<param name="DatePattern" value="yyyyMMdd/"OrderInfor/test.log"" />--> <!--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:\CoreLog\OneLog\" />--> <!--改成存放到项目目录下了--> <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".log"" /> <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:\CoreLog\TwoLog\" />--> <!--改成存放到项目目录下了--> <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".log"" /> <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>
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); } } }
代码调用:
{ 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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
2020-01-07 第二十八节:Asp.Net Core中JWT的几种写法和认证方式
2018-01-07 第七节:利用CancellationTokenSource实现任务取消和利用CancellationToken类检测取消异常。