目录
[1.1 核心需求拆解](#1.1 核心需求拆解)
[1.2 实现原理](#1.2 实现原理)
[3.1 代码目录结构](#3.1 代码目录结构)
[3.2 配置文件:desensitize_config.json](#3.2 配置文件:desensitize_config.json)
[3.3 核心脱敏模块:desensitizer.py](#3.3 核心脱敏模块:desensitizer.py)
[3.4 日志集成模块:desensitize_handler.py](#3.4 日志集成模块:desensitize_handler.py)
[3.5 演示示例:demo.py](#3.5 演示示例:demo.py)
[3.6 运行效果](#3.6 运行效果)
[4.1 接口测试日志脱敏](#4.1 接口测试日志脱敏)
[4.2 APP测试日志脱敏(Airtest集成)](#4.2 APP测试日志脱敏(Airtest集成))
在测试开发日常工作中,日志是排查问题、跟踪流程的核心依据------接口请求参数、用户操作轨迹、数据库交互记录,都需要通过日志留存。但随之而来的问题是,这些日志里很容易夹杂敏感信息:用户的手机号、身份证号、银行卡号,甚至是数据库密码、接口Token。一旦这些日志被不当泄露,不仅可能违反数据安全相关规定,还会给用户和项目带来潜在风险。
所以,实现一套能自动识别并屏蔽敏感信息的日志脱敏工具,成了测试开发工作中的刚需。今天就带大家从零构建这个工具,涵盖从原理理解到实际落地的完整流程,最终产出可直接复用的Python 3.8.6脚本。
一、先搞懂:日志脱敏的核心需求与实现原理
1.1 核心需求拆解
在动手写代码前,先明确工具要解决的核心问题:
-
能精准识别常见敏感信息:比如手机号(11位数字)、身份证号(18位,含X)、银行卡号(16-19位)、密码/Token(通常是长字符串或特定格式);
-
脱敏规则可配置:不同项目的敏感信息类型可能不同,支持自定义添加/修改脱敏规则,不用改核心代码;
-
不破坏原有日志结构:只替换敏感信息部分,保留日志的时间、级别、模块等原有字段;
-
低侵入性:能无缝集成到Python logging模块,不用大幅修改现有日志打印逻辑。
1.2 实现原理
日志脱敏的核心逻辑很简单:**"匹配-替换"**两步走:
-
匹配:通过正则表达式,精准匹配日志文本中的敏感信息;
-
替换:将匹配到的敏感信息,用指定字符(比如*)进行部分或全部屏蔽,同时保留部分非敏感字符用于定位(比如手机号保留前3后4位)。
整个流程可以理解为:日志文本 → 正则匹配敏感信息 → 脱敏替换 → 输出脱敏后日志。

二、工具设计思路:从易用性和扩展性出发
结合实际工作场景,工具采用"配置化+模块化"的设计思路,拆解为3个核心模块:
-
配置模块:用配置文件存储脱敏规则(比如正则表达式、替换格式),支持动态修改,不用重启服务;
-
核心脱敏模块:封装脱敏逻辑,接收原始日志文本,应用配置的脱敏规则,返回脱敏后的文本;
-
日志集成模块:自定义Python logging的Handler或Filter,将脱敏逻辑嵌入日志输出流程,实现"打印即脱敏"。
这样设计的好处是:后续要新增敏感信息类型,只需修改配置文件;要适配不同的日志框架,只需调整集成模块,核心脱敏逻辑无需改动,扩展性更强。
三、代码实现:可直接复用的完整脚本
本次实现采用多文件结构,先给出整体目录,再逐一讲解各文件的功能和代码:
3.1 代码目录结构
python
log_desensitizer/
├── config/
│ └── desensitize_config.json # 脱敏规则配置文件
├── core/
│ └── desensitizer.py # 核心脱敏逻辑
├── log_handler/
│ └── desensitize_handler.py # 日志集成Handler
└── demo.py # 演示示例
3.2 配置文件:desensitize_config.json
用JSON文件存储脱敏规则,每个规则包含3个核心字段:
-
rule_name:规则名称(用于标识);
-
regex:匹配敏感信息的正则表达式;
-
replace:替换格式(用group分组保留部分字符,其余用*替换)。
python
{
"rules": [
{
"rule_name": "phone",
"regex": "1[3-9]\\d{9}",
"replace": "$1****$2"
},
{
"rule_name": "id_card",
"regex": "(\\d{6})(\\d{8})(\\d{4})",
"replace": "$1********$3"
},
{
"rule_name": "bank_card",
"regex": "(\\d{4})(\\d{12,15})",
"replace": "$1***********$2"
},
{
"rule_name": "password",
"regex": "password[:=]\\s*['\"]?([^'\"]+)['\"]?",
"replace": "password=******"
},
{
"rule_name": "token",
"regex": "token[:=]\\s*['\"]?([a-zA-Z0-9]{32,})['\"]?",
"replace": "token=******"
}
]
}
说明:
-
手机号正则:匹配11位手机号,替换后保留前3位和后4位(比如13800138000 → 138****8000);
-
身份证号正则:匹配18位身份证号(含X),替换后保留前6位和后4位(比如110101199001011234 → 110101********1234);
-
密码/Token正则:匹配"password=xxx""token=xxx"格式的字符串,直接全部替换为******。
3.3 核心脱敏模块:desensitizer.py
该模块负责加载配置文件、实现脱敏逻辑,提供一个对外的desensitize方法:
python
import json
import re
import os
class LogDesensitizer:
def __init__(self):
# 加载配置文件
self.config_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"config",
"desensitize_config.json"
)
self.rules = self._load_config()
def _load_config(self):
"""加载脱敏规则配置"""
try:
with open(self.config_path, "r", encoding="utf-8") as f:
config = json.load(f)
return config.get("rules", [])
except Exception as e:
print("加载脱敏配置文件失败:", str(e))
return []
def desensitize(self, text):
"""
核心脱敏方法:对输入文本应用所有脱敏规则
:param text: 原始文本(日志内容)
:return: 脱敏后的文本
"""
if not text or not self.rules:
return text
desensitized_text = text
for rule in self.rules:
regex = rule.get("regex")
replace = rule.get("replace")
if not regex or not replace:
continue
# 应用正则替换
desensitized_text = re.sub(regex, replace, desensitized_text)
return desensitized_text
# 单例模式,避免重复加载配置
desensitizer = LogDesensitizer()
说明:
-
采用单例模式创建desensitizer实例,避免每次脱敏都重新加载配置文件,提升性能;
-
_load_config方法负责读取配置文件,若读取失败则返回空列表,不影响原有日志输出;
-
desensitize方法接收原始日志文本,遍历所有脱敏规则,依次应用正则替换,返回脱敏后的文本。
3.4 日志集成模块:desensitize_handler.py
自定义logging的Handler,将脱敏逻辑嵌入日志输出流程。这里继承StreamHandler(控制台输出Handler),重写emit方法:
python
import logging
from core.desensitizer import desensitizer
class DesensitizeStreamHandler(logging.StreamHandler):
def emit(self, record):
"""
重写emit方法:输出日志前先进行脱敏处理
:param record: 日志记录对象
"""
# 获取原始日志消息
msg = self.format(record)
# 脱敏处理
desensitized_msg = desensitizer.desensitize(msg)
# 替换record的msg为脱敏后的消息
record.msg = desensitized_msg
# 调用父类的emit方法输出日志
super().emit(record)
说明:emit方法是logging输出日志的核心方法,我们在输出前对日志消息进行脱敏处理,再调用父类方法完成输出,实现"打印即脱敏"的效果。如果需要输出到文件,可类似继承FileHandler重写emit方法。
3.5 演示示例:demo.py
编写测试代码,验证工具的脱敏效果:
python
import logging
from log_handler.desensitize_handler import DesensitizeStreamHandler
def setup_logger():
"""配置日志器,集成脱敏Handler"""
logger = logging.getLogger("test_logger")
logger.setLevel(logging.DEBUG)
# 移除默认Handler,避免重复输出
logger.handlers.clear()
# 添加脱敏Handler
handler = DesensitizeStreamHandler()
# 配置日志格式:时间-级别-模块-消息
formatter = logging.Formatter(
"%(asctime)s - %(levelname)s - %(module)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
return logger
if __name__ == "__main__":
logger = setup_logger()
# 模拟不同场景的日志输出,包含各类敏感信息
logger.debug("用户登录:手机号13800138000,密码password=12345678")
logger.info("用户实名认证:身份证号110101199001011234")
logger.warning("接口请求:token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...,银行卡号6222021234567890123")
logger.error("数据库连接失败:url=mysql://user:password=root123@localhost:3306/test")
3.6 运行效果
执行demo.py后,控制台输出的脱敏日志如下:
python
2024-05-20 15:30:00 - DEBUG - demo - 用户登录:手机号138****8000,密码password=******
2024-05-20 15:30:00 - INFO - demo - 用户实名认证:身份证号110101********1234
2024-05-20 15:30:00 - WARNING - demo - 接口请求:token=******,银行卡号6222***********90123
2024-05-20 15:30:00 - ERROR - demo - 数据库连接失败:url=mysql://user:password=******@localhost:3306/test
可以看到,所有敏感信息都被成功屏蔽,同时保留了部分特征用于问题定位,符合预期效果。
四、实际应用场景演示
除了上面的基础演示,工具还能适配测试开发工作中的多种实际场景,这里举2个常见例子:
4.1 接口测试日志脱敏
在接口测试脚本中,打印请求参数和响应数据时,自动脱敏敏感信息:
python
import requests
from log_handler.desensitize_handler import DesensitizeStreamHandler
import logging
# 配置日志(集成脱敏Handler)
logger = logging.getLogger("api_test")
logger.setLevel(logging.INFO)
handler = DesensitizeStreamHandler()
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
def test_user_info():
url = "https://api.example.com/user/info"
headers = {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
data = {
"phone": "13912345678",
"id_card": "310101198001011234",
"bank_card": "6217001234567890123"
}
logger.info("请求用户信息接口,请求参数:%s", data)
response = requests.post(url, headers=headers, json=data)
logger.info("接口响应数据:%s", response.json())
if __name__ == "__main__":
test_user_info()
输出日志中,手机号、身份证号、Token等敏感信息会被自动脱敏,避免泄露。
4.2 APP测试日志脱敏(Airtest集成)
在Airtest的APP自动化测试中,集成脱敏工具,屏蔽日志中的用户敏感信息:
python
from airtest.core.api import *
import logging
from log_handler.desensitize_handler import DesensitizeStreamHandler
# 配置Airtest日志,集成脱敏Handler
logger = logging.getLogger("airtest")
logger.setLevel(logging.INFO)
logger.handlers.clear()
handler = DesensitizeStreamHandler()
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
def test_app_login():
# 连接设备
connect_device("Android:///")
# 启动APP
start_app("com.example.app")
# 输入手机号和密码
touch(Template("phone_input.png"))
text("13700137000")
touch(Template("password_input.png"))
text("abc123456")
logger.info("用户输入:手机号13700137000,密码abc123456")
# 点击登录
touch(Template("login_btn.png"))
sleep(2)
logger.info("登录操作完成")
if __name__ == "__main__":
test_app_login()
Airtest运行日志中,输入的手机号和密码会被自动脱敏,保障数据安全。
五、使用注意事项
-
正则表达式精准性:配置文件中的正则表达式需要精准匹配敏感信息,避免误脱敏。比如手机号正则要排除11位非手机号的数字串,可根据实际场景优化正则;
-
配置文件更新:修改配置文件后,需要重启应用才能生效(若需要热更新,可在_desensitize方法中添加定时重新加载配置的逻辑);
-
性能影响:对于日志量极大的场景,正则替换可能会影响性能。可通过限制脱敏规则数量、优化正则表达式(避免贪婪匹配)来提升性能;
-
特殊格式敏感信息:若项目中有特殊格式的敏感信息(比如企业内部的员工编号),可在配置文件中新增对应的正则和替换规则;
-
日志存储安全:脱敏后的日志仍需妥善存储,避免被未授权人员访问,同时保留脱敏记录,便于后续审计。
最后:工具的扩展方向
这个日志脱敏工具是基础版本,后续可根据实际需求扩展:
-
支持更多配置格式:比如YAML配置文件,比JSON更易读;
-
动态加载配置:通过接口或定时任务,实现配置的热更新,不用重启应用;
-
脱敏等级配置:支持不同脱敏等级(比如部分屏蔽、全部屏蔽),根据日志用途动态切换;
-
敏感信息统计:记录脱敏次数、敏感信息类型分布,生成统计报表,便于掌握日志安全状况。