Files
KJD/KJDService/KJD_WebService/Utils/ApiRequestLogger.cs
T
2026-08-07 18:53:43 +08:00

303 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using Newtonsoft.Json;
namespace KJD_Web.Utils
{
/// <summary>
/// API 请求录制工具:挂接在 Global.asax 的 BeginRequest / EndRequest,
/// 捕获请求链接(action/service)、请求内容(body/query)与响应,按天落盘为 JSON Lines,
/// 用于 .NET Core 迁移后的回放校验。
/// 配置(Web.config appSettings):
/// ApiRequestLog = 1 开启 / 0 关闭(默认开启)
/// ApiRequestLogDir = 日志目录(默认 ~/App_Data/ApiLogs)
/// </summary>
public static class ApiRequestLogger
{
private const string ItemKey = "__ApiRequestLog__";
private const string FilterItemKey = "__ApiResponseCapture__";
private const int MaxBodyLogLength = 100000;
private static readonly object SyncRoot = new object();
private static readonly Regex SensitiveKeyValueRegex = new Regex(
@"(?i)(\b(?:password|passwd|pwd|token|access_token|secret|app_secret|client_secret|sign|signature|api_key|authorization|key)=)([^&\s""]*)",
RegexOptions.Compiled);
private static readonly Regex SensitiveRegex = new Regex(
"\"((?i)password|passwd|pwd|token|access_token|secret|app_secret|client_secret|sign|signature|api_key|authorization|key)\"\\s*:\\s*\"([^\"]*)\"",
RegexOptions.Compiled);
private static readonly string[] FriendlyPrefixes =
{
"/user/", "/shop/", "/update/", "/order/", "/product/", "/product_third/", "/price/",
"/bxx_user/", "/bxx_shop/", "/bxx_product/", "/bxx_product_third/", "/bxx_price/",
"/dyxd_user/", "/dyxd_shop/", "/dyxd_product/", "/dyxd_product_third/", "/dyxd_price/",
"/wxxd_user/", "/wxxd_shop/", "/wxxd_product/", "/wxxd_product_third/", "/wxxd_price/", "/wxxd_order/"
};
private static bool _configLoaded;
private static bool _enabled = true;
private static string _logDir;
private sealed class RequestRecord
{
public string id;
public DateTime time;
public string service;
public string action;
public string url;
public string path;
public string method;
public string query;
public string body;
public string ip;
public string ua;
}
/// <summary>请求入口:解析 action/service、读取并缓存请求体、安装响应捕获。</summary>
public static void BeginRequest(HttpContext context)
{
try
{
EnsureConfig(context);
if (!_enabled) return;
string path = context.Request.Path ?? "";
if (!IsApiPath(path)) return;
string service, action;
ParsePath(path, out service, out action);
string body = ReadBody(context);
var record = new RequestRecord
{
id = Guid.NewGuid().ToString("N"),
time = DateTime.Now,
service = service,
action = action,
path = path,
method = context.Request.HttpMethod,
url = SafeGet(() => context.Request.Url == null ? path : context.Request.Url.ToString(), path),
query = Desensitize(SafeGet(() => context.Request.Url == null ? "" : context.Request.Url.Query, "")),
body = Truncate(Desensitize(body)),
ip = SafeGet(() => context.Request.UserHostAddress, ""),
ua = SafeGet(() => context.Request.UserAgent, "")
};
context.Items[ItemKey] = record;
try { context.Response.Headers["X-Request-Id"] = record.id; } catch { }
try
{
var filter = new ApiResponseCaptureStream(context.Response.Filter);
context.Response.Filter = filter;
context.Items[FilterItemKey] = filter;
}
catch { }
}
catch { }
}
/// <summary>请求出口:合并响应,写一条 JSON Lines 记录。</summary>
public static void EndRequest(HttpContext context)
{
try
{
if (!_enabled) return;
var record = context.Items[ItemKey] as RequestRecord;
if (record == null) return;
string responseText = null;
try
{
var filter = context.Items[FilterItemKey] as ApiResponseCaptureStream;
if (filter != null) responseText = Truncate(Desensitize(filter.GetCapturedText()));
}
catch { }
var line = JsonConvert.SerializeObject(new
{
id = record.id,
time = record.time.ToString("yyyy-MM-dd HH:mm:ss.fff"),
service = record.service,
action = record.action,
method = record.method,
url = record.url,
path = record.path,
query = record.query,
body = record.body,
response = responseText,
ip = record.ip,
ua = record.ua,
status = SafeGet(() => context.Response.StatusCode, 0)
});
string fileName = Path.Combine(_logDir, "api_" + DateTime.Now.ToString("yyyyMMdd") + ".jsonl");
lock (SyncRoot)
{
File.AppendAllText(fileName, line + Environment.NewLine, Encoding.UTF8);
}
}
catch { }
}
private static void EnsureConfig(HttpContext context)
{
if (_configLoaded) return;
lock (SyncRoot)
{
if (_configLoaded) return;
try
{
string sw = ConfigurationManager.AppSettings["ApiRequestLog"];
if (!string.IsNullOrEmpty(sw))
_enabled = (sw == "1" || sw.Equals("true", StringComparison.OrdinalIgnoreCase) || sw.Equals("on", StringComparison.OrdinalIgnoreCase));
string dir = ConfigurationManager.AppSettings["ApiRequestLogDir"];
if (string.IsNullOrEmpty(dir)) dir = "~/App_Data/ApiLogs";
_logDir = dir.StartsWith("~/") ? context.Server.MapPath(dir) : dir;
if (!Directory.Exists(_logDir)) Directory.CreateDirectory(_logDir);
}
catch { }
_configLoaded = true;
}
}
private static string ReadBody(HttpContext context)
{
try
{
var input = context.Request.InputStream;
if (input == null || !input.CanRead || input.Length <= 0) return "";
string text;
using (var reader = new StreamReader(input, Encoding.UTF8, true, 1024, true))
{
text = reader.ReadToEnd();
}
try { if (input.CanSeek) input.Position = 0; } catch { }
return text;
}
catch { return ""; }
}
private static bool IsApiPath(string path)
{
if (string.IsNullOrEmpty(path)) return false;
if (path.IndexOf(".asmx", StringComparison.OrdinalIgnoreCase) >= 0) return true;
foreach (string p in FriendlyPrefixes)
{
if (path.StartsWith(p, StringComparison.OrdinalIgnoreCase)) return true;
}
return false;
}
private static void ParsePath(string path, out string service, out string action)
{
service = "";
action = "";
if (string.IsNullOrEmpty(path)) return;
string p = path.TrimStart('/');
if (p.EndsWith("/")) p = p.TrimEnd('/');
string[] segs = p.Split('/');
if (segs.Length == 0) return;
action = segs[segs.Length - 1];
if (segs.Length >= 2)
{
int asmxIdx = -1;
for (int i = 0; i < segs.Length; i++)
{
if (segs[i].EndsWith(".asmx", StringComparison.OrdinalIgnoreCase)) { asmxIdx = i; break; }
}
if (asmxIdx >= 0) service = segs[asmxIdx].Replace(".asmx", "");
else service = segs[0];
}
else if (p.EndsWith(".asmx", StringComparison.OrdinalIgnoreCase))
{
service = p.Replace(".asmx", "");
action = service;
}
}
private static string Desensitize(string text)
{
if (string.IsNullOrEmpty(text)) return text;
string r = SensitiveRegex.Replace(text, "\"$1\":\"***\"");
r = SensitiveKeyValueRegex.Replace(r, "$1***");
return r;
}
private static string Truncate(string text)
{
if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLogLength) return text;
return text.Substring(0, MaxBodyLogLength) + " [truncated...]";
}
private static T SafeGet<T>(Func<T> fn, T def)
{
try { return fn(); }
catch { return def; }
}
/// <summary>响应捕获流:包装 Response.Filter,缓存写出内容。</summary>
private sealed class ApiResponseCaptureStream : Stream
{
private readonly Stream _inner;
private readonly MemoryStream _buffer = new MemoryStream();
private string _capturedText;
public ApiResponseCaptureStream(Stream inner)
{
_inner = inner ?? new MemoryStream();
}
public string GetCapturedText()
{
if (_capturedText == null) _capturedText = BytesToText(_buffer.ToArray());
return _capturedText;
}
private static string BytesToText(byte[] bytes)
{
if (bytes == null || bytes.Length == 0) return "";
bool binary = false;
for (int i = 0; i < bytes.Length; i++)
{
byte b = bytes[i];
if (b == 0 || b < 8) { binary = true; break; }
}
if (binary) return "<binary length=" + bytes.Length + ">";
return Encoding.UTF8.GetString(bytes);
}
public override bool CanRead { get { return _inner.CanRead; } }
public override bool CanSeek { get { return _inner.CanSeek; } }
public override bool CanWrite { get { return _inner.CanWrite; } }
public override long Length { get { return _inner.Length; } }
public override long Position { get { return _inner.Position; } set { _inner.Position = value; } }
public override void Flush() { _inner.Flush(); }
public override int Read(byte[] buffer, int offset, int count) { return _inner.Read(buffer, offset, count); }
public override long Seek(long offset, SeekOrigin origin) { return _inner.Seek(offset, origin); }
public override void SetLength(long value) { _inner.SetLength(value); }
public override void Write(byte[] buffer, int offset, int count)
{
_buffer.Write(buffer, offset, count);
_inner.Write(buffer, offset, count);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
try { GetCapturedText(); } catch { }
try { _inner.Dispose(); } catch { }
}
base.Dispose(disposing);
}
}
}
}