主页

Nginx如何拦截访问的请求体和响应体信息并写入日志

梦冥光   IP属地: 青海
0 点赞 0阅读 770
现在使用的是fastadmin作为后端开发框架,准备开发一个三类医疗系统,fastadmin只有在调试的时候才有请求体的日志信息记录,但是没有响应体,有时候在测试过程中测试人员发现问题总说是后端问题,不说是前端问题,但是赖于拿不出响应体信息,

场景举例:调用一个接口,接口会返回当前调用了几次,但是测试人员不承认,只说调用了一次,但是我的接口是累加调用次数,写入数据库的,超过5次就限制调用,测试人员就只承认调用了一次就被限制了,现在我只想把调用时间和调用返回的信息给测试看

场景举例:为什么不直接用fastadmin自带的写入日志记录呢,或在public/index.php写一个钩子来获取请求体和响应体呢,这个问题想到过,因为使用的是宝塔,安装了防火墙免费的,有时候会莫名其妙的拦截客户端的请求,返回405,403状态等等,这些请求信息是不会写入fastadmin的runtime/log的,因为还没进入fastadmin就被拦截了

直接开始吧
第一步:

如果你使用的是宝塔,编辑/www/server/nginx/conf/nginx.conf文件,在http最后的access_log off;位置后面添加下面代码,也可以根据需求添加

# === 请添加以下配置 开始 ===
          # 提前声明可由 Lua 修改的日志变量
        map $request_id $trace_request_body {
            default "";
        }

        map $request_id $trace_request_body_truncated {
            default "0";
        }

        map $request_id $trace_response_body {
            default "";
        }

        map $request_id $trace_response_body_truncated {
            default "0";
        }

        log_format full_trace escape=json
            '{'
                '"time":"$time_iso8601",'
                '"request_id":"$request_id",'
                '"host":"$host",'
                '"server_addr":"$server_addr",'
                '"client_ip":"$remote_addr",'
                '"forwarded_for":"$http_x_forwarded_for",'

                '"request_method":"$request_method",'
                '"request_uri":"$request_uri",'
                '"server_protocol":"$server_protocol",'
                '"request_length":"$request_length",'
                '"request_content_type":"$content_type",'
                '"request_content_length":"$content_length",'
                '"request_body":"$trace_request_body",'
                '"request_body_truncated":"$trace_request_body_truncated",'

                '"status":"$status",'
                '"request_time":"$request_time",'
                '"bytes_sent":"$bytes_sent",'
                '"body_bytes_sent":"$body_bytes_sent",'

                '"upstream_addr":"$upstream_addr",'
                '"upstream_status":"$upstream_status",'
                '"upstream_connect_time":"$upstream_connect_time",'
                '"upstream_header_time":"$upstream_header_time",'
                '"upstream_response_time":"$upstream_response_time",'

                '"response_content_type":"$sent_http_content_type",'
                '"response_content_length":"$sent_http_content_length",'
                '"response_content_encoding":"$sent_http_content_encoding",'
                '"response_body":"$trace_response_body",'
                '"response_body_truncated":"$trace_response_body_truncated",'

                '"request_headers":{'
                    '"token":"$http_token",'
                    '"deviceid":"$http_deviceid",'
                    '"referer":"$http_referer",'
                    '"user_agent":"$http_user_agent"'
                '}'
            '}';

        # === 请添加以上配置 结束 ===
第二步:
在需要写入日志的网站配置nginx参数,宝塔打开网站的配置文件,在server最后的error_log后面添加下面代码,并且把原来的access_log这行注释掉
   # 采集请求体前 4096 个原始字节
       access_by_lua_block {
    local limit = 4096

    -- 避免在 UTF-8 中文字符中间截断
    local function utf8_prefix(value, max_bytes)
        if not value then
            return ""
        end

        if #value <= max_bytes then
            return value
        end

        local cut = max_bytes

        -- 向前寻找当前 UTF-8 字符的起始字节
        while cut > 0 do
            local byte = string.byte(value, cut)

            if byte < 128 or byte >= 192 then
                break
            end

            cut = cut - 1
        end

        if cut <= 0 then
            return ""
        end

        local lead = string.byte(value, cut)
        local char_length = 1

        if lead >= 240 and lead <= 244 then
            char_length = 4
        elseif lead >= 224 and lead <= 239 then
            char_length = 3
        elseif lead >= 194 and lead <= 223 then
            char_length = 2
        end

        -- 最后一个字符不完整时将其删除
        if cut + char_length - 1 > max_bytes then
            return string.sub(value, 1, cut - 1)
        end

        return string.sub(value, 1, max_bytes)
    end

    ngx.req.read_body()

    local body = ngx.req.get_body_data()
    local excerpt = ""

    if body then
        excerpt = utf8_prefix(body, limit)

        if #body > limit then
            ngx.var.trace_request_body_truncated = "1"
        end
    else
        local body_file = ngx.req.get_body_file()

        if body_file then
            local file = io.open(body_file, "rb")

            if file then
                -- 多读取4字节,便于安全处理UTF-8边界
                local data = file:read(limit + 4) or ""
                file:close()

                excerpt = utf8_prefix(data, limit)

                if #data > limit then
                    ngx.var.trace_request_body_truncated = "1"
                end
            end
        end
    end

    ngx.var.trace_request_body = excerpt
}

        # 逐块采集响应体,只累计前 4096 个原始字节
       body_filter_by_lua_block {
    local limit = 4096

    local function utf8_prefix(value, max_bytes)
        if not value then
            return ""
        end

        if #value <= max_bytes then
            return value
        end

        local cut = max_bytes

        while cut > 0 do
            local byte = string.byte(value, cut)

            if byte < 128 or byte >= 192 then
                break
            end

            cut = cut - 1
        end

        if cut <= 0 then
            return ""
        end

        local lead = string.byte(value, cut)
        local char_length = 1

        if lead >= 240 and lead <= 244 then
            char_length = 4
        elseif lead >= 224 and lead <= 239 then
            char_length = 3
        elseif lead >= 194 and lead <= 223 then
            char_length = 2
        end

        if cut + char_length - 1 > max_bytes then
            return string.sub(value, 1, cut - 1)
        end

        return string.sub(value, 1, max_bytes)
    end

    local chunk = ngx.arg[1]
    local current = ngx.ctx.trace_response_body_raw or ""

    if chunk and chunk ~= "" then
        local remaining = limit - #current

        if remaining > 0 then
            current = current .. string.sub(chunk, 1, remaining)
            ngx.ctx.trace_response_body_raw = current

            ngx.var.trace_response_body =
                utf8_prefix(current, limit)
        end

        if #chunk > remaining or
           (remaining <= 0 and #chunk > 0) then
            ngx.var.trace_response_body_truncated = "1"
        end
    end
}
    
    access_log /www/wwwlogs/xxxxx.com/full_trace/current.log
               full_trace
               buffer=64k
               flush=1s;
    # 注意:为了确保 $request_body 有值,建议在 server 块级别开启缓冲
    #client_body_buffer_size 128k;
    #client_body_in_single_buffer on;

到这里其实已经大功告成了,只需要重启nginx就可以了,如果需要把写入的日志按日期分类需要进行下面操作

日志按日期分类
如果你使用的是宝塔linux系统在/etc下创建xxxx.conf文件复制下面这段代码
"/www/wwwlogs/xxxxxx/full_trace/current.log" {
    daily
    rotate 30

    dateext
    dateyesterday
    dateformat -%Y-%m-%d
    extension .log

    nocompress

    missingok
    notifempty

    su www www
    create 0640 www www

    sharedscripts

    postrotate
        if [ -s /www/server/nginx/logs/nginx.pid ]; then
            kill -USR1 "$(cat /www/server/nginx/logs/nginx.pid)"
        fi
    endscript
}
  • daily:每天轮转一次。
  • rotate 30:保留30份历史日志。
  • dateext:历史文件使用日期后缀。
  • dateyesterday:文件名使用昨天日期,与日志内容日期对应。
  • extension .log:让 .log 保持在日期后面。
  • compress:压缩历史日志。
  • delaycompress:刚轮转的文件暂不压缩,下一轮再压缩。
  • create:轮转后创建新的 current.log。
  • USR1:通知 Nginx关闭旧文件并重新打开新日志文件。这是 Nginx官方推荐的轮转方式。

  • 在宝塔添加计划任务
    在宝塔新建一个计划任务,可以每晚00:00执行
    LC_ALL=C LANG=C /usr/sbin/logrotate \
      -s /var/lib/logrotate-api-full-trace.status \
      /etc/xxxx.conf
    xxxx.conf是在etc文件夹下创建的文件
    测试定时任务使用下面代码
    LC_ALL=C LANG=C /usr/sbin/logrotate \
      -f -v \
      -s /var/lib/logrotate-api-full-trace.status \
      /etc/logrotate-api-full-trace.conf
    会创建一个新的日志文件
    如果想方便查日志,可以在目标网站添加的server添加
    add_header X-Request-ID $request_id always;
    在 PHP的 FastCGI配置中确保传递:
    fastcgi_param HTTP_X_REQUEST_ID $request_id;
    如果目标网站的server使用的是include enable-php-74.conf;,需要到宝塔的/www/server/nginx/conf目录修改enable-php-74.conf文件
        location ~ [^/]\.php(/|$)
        {
            try_files $uri =404;
            fastcgi_pass  unix:/tmp/php-cgi-74.sock;
            fastcgi_param HTTP_X_REQUEST_ID $request_id;
            fastcgi_index index.php;
            include fastcgi.conf;
            include pathinfo.conf;
        }
    
    如果使用的是thinkPHP5.0通过中间件全局返回,创建文件application/common/behavior/RequestId.php
    内容:
    <?php
    
    namespace app\common\behavior;
    
    use think\response\Json;
    
    class RequestId
    {
        /**
         * response_send 钩子
         *
         * @param mixed $response
         */
        public function responseSend(&$response)
        {
            // 获取 Nginx 传给 PHP 的 request_id
            $requestId = isset($_SERVER['HTTP_X_REQUEST_ID'])
                ? trim($_SERVER['HTTP_X_REQUEST_ID'])
                : '';
    
            // 防止直接访问 PHP 时没有 request_id
            if (
                $requestId === '' ||
                !preg_match('/^[A-Za-z0-9._-]{1,128}$/', $requestId)
            ) {
                $requestId = bin2hex(random_bytes(16));
            }
    
            // 所有由 ThinkPHP 返回的响应都增加响应头
            $response->header('X-Request-ID', $requestId);
    
            // 只修改 JSON 响应体
            if ($response instanceof Json) {
                $data = $response->getData();
    
                if (is_array($data)) {
                    // 判断是不是 [1, 2, 3] 这种顶层列表
                    $isList = count($data) > 0
                        && array_keys($data) === range(0, count($data) - 1);
    
                    if ($isList) {
                        // 包装顶层数组
                        $data = [
                            'data'       => $data,
                            'request_id' => $requestId,
                        ];
                    } else {
                        // 常见的接口对象直接增加字段
                        $data['request_id'] = $requestId;
                    }
                } elseif (is_object($data)) {
                    $data->request_id = $requestId;
                } else {
                    $data = [
                        'data'       => $data,
                        'request_id' => $requestId,
                    ];
                }
    
                $response->data($data);
            }
        }
    }
    注册行为,编辑application/tags.php,加入 response_send
    <?php
    
    return [
        'app_init'     => [],
        'app_begin'    => [],
        'module_init'  => [],
        'action_begin' => [],
        'view_filter'  => [],
        'log_write'    => [],
        'app_end'      => [],
    
        // 响应发送前统一处理
        'response_send' => [
            'app\\common\\behavior\\RequestId',
        ],
    ];

    效果:

    {
    	"code": 0,
    	"msg": "授权码错误,剩余9次或联系管理员解除",
    	"time": "1787036720",
    	"data": {
    		"error_num": 1
    	},
    	"request_id": "7a784f80125fa8d9e1e0c12b62cdd7a5"
    }


    注意:宝塔重启后有时候会把目标网站的*.conf配置文件删除,需要手动恢复,在/www/server/panel/vhost/nginx文件夹下,在操作前最好备份一下配置文件
    总结文件:总结文件
    总结图片:下载链接
    完整配置:完整配置

    Nginx完整配置:NGINX完整配置

    © 著作权归作者所有,转载或内容合作请联系作者