forked from erp-dev/erp
feat: log formatted
This commit is contained in:
@@ -18,10 +18,10 @@ class ApiAuditLogMiddleware:
|
||||
"""
|
||||
API审计日志中间件
|
||||
|
||||
用于记录特定URL前缀的POST请求,保存"创建"操作的历史现场。
|
||||
|
||||
用于记录特定URL前缀的POST/PUT/PATCH请求,保存"创建/更新"操作的历史现场。
|
||||
|
||||
特性:
|
||||
- 只记录POST请求
|
||||
- 只记录POST、PUT、PATCH请求
|
||||
- 通过URL前缀白名单过滤
|
||||
- 支持multipart/form-data请求,文件字段只记录元信息
|
||||
- 通过Celery异步写入,不阻塞API响应
|
||||
@@ -42,8 +42,8 @@ class ApiAuditLogMiddleware:
|
||||
if not self.enabled:
|
||||
return self.get_response(request)
|
||||
|
||||
# 只处理POST请求
|
||||
if request.method != 'POST':
|
||||
# 只处理POST、PUT、PATCH请求
|
||||
if request.method not in ('POST', 'PUT', 'PATCH'):
|
||||
return self.get_response(request)
|
||||
|
||||
# 检查URL是否在白名单中
|
||||
@@ -51,7 +51,7 @@ class ApiAuditLogMiddleware:
|
||||
logger.debug('ApiAuditLog: URL不匹配白名单, path=%s, prefixes=%s', request.path, self.url_prefixes)
|
||||
return self.get_response(request)
|
||||
|
||||
logger.info('ApiAuditLog: 捕获到POST请求, path=%s', request.path)
|
||||
logger.info('ApiAuditLog: 捕获到%s请求, path=%s', request.method, request.path)
|
||||
|
||||
# 在请求进入视图前,缓存请求体数据
|
||||
# 注意:request.body只能读取一次,需要在这里缓存
|
||||
@@ -86,30 +86,54 @@ class ApiAuditLogMiddleware:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_post_and_files(self, request):
|
||||
"""
|
||||
获取表单字段和文件,兼容PUT/PATCH请求
|
||||
|
||||
Django的request.POST/request.FILES只在method为POST时才会被解析,
|
||||
PUT/PATCH请求即使是multipart/form-data或urlencoded也不会自动填充,
|
||||
因此这里针对非POST方法手动解析。
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
return request.POST, request.FILES
|
||||
|
||||
content_type = request.content_type or ''
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
try:
|
||||
return request.parse_file_upload(request.META, io.BytesIO(request.body))
|
||||
except MultiPartParserError:
|
||||
return QueryDict(), MultiValueDict()
|
||||
elif 'application/x-www-form-urlencoded' in content_type:
|
||||
return QueryDict(request.body, encoding=request.encoding or settings.DEFAULT_CHARSET), MultiValueDict()
|
||||
|
||||
return QueryDict(), MultiValueDict()
|
||||
|
||||
def _extract_request_data(self, request) -> dict:
|
||||
"""
|
||||
提取请求数据
|
||||
|
||||
|
||||
对于multipart/form-data请求,分别处理表单字段和文件字段。
|
||||
文件字段只记录元信息(文件名、大小、类型),不记录二进制内容。
|
||||
"""
|
||||
content_type = request.content_type or ''
|
||||
|
||||
|
||||
if 'multipart/form-data' in content_type:
|
||||
# multipart请求:分别处理表单字段和文件
|
||||
data = {}
|
||||
|
||||
post, files = self._get_post_and_files(request)
|
||||
|
||||
# 处理普通表单字段
|
||||
for key, values in request.POST.lists():
|
||||
for key, values in post.lists():
|
||||
if len(values) == 1:
|
||||
data[key] = values[0]
|
||||
else:
|
||||
data[key] = values
|
||||
|
||||
|
||||
# 处理文件字段:只记录元信息
|
||||
for key, files in request.FILES.lists():
|
||||
for key, file_list in files.lists():
|
||||
file_infos = []
|
||||
for f in files:
|
||||
for f in file_list:
|
||||
file_infos.append({
|
||||
'_type': 'file',
|
||||
'name': f.name,
|
||||
@@ -120,26 +144,27 @@ class ApiAuditLogMiddleware:
|
||||
data[key] = file_infos[0]
|
||||
else:
|
||||
data[key] = file_infos
|
||||
|
||||
|
||||
return data
|
||||
|
||||
|
||||
elif 'application/json' in content_type:
|
||||
# JSON请求:解析body
|
||||
try:
|
||||
return json.loads(request.body.decode('utf-8'))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return {'_raw': request.body.decode('utf-8', errors='replace')}
|
||||
|
||||
|
||||
elif 'application/x-www-form-urlencoded' in content_type:
|
||||
# 表单请求
|
||||
data = {}
|
||||
for key, values in request.POST.lists():
|
||||
post, _ = self._get_post_and_files(request)
|
||||
for key, values in post.lists():
|
||||
if len(values) == 1:
|
||||
data[key] = values[0]
|
||||
else:
|
||||
data[key] = values
|
||||
return data
|
||||
|
||||
|
||||
else:
|
||||
# 其他类型:尝试作为文本记录
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user