开篇
排查接口需要看参数,但密码、令牌不该跟着整份请求写进日志。把日志截短也不保险,敏感值可能就在最前面。下面拆解字段过滤和长度控制各自负责什么。
环境:jdk 21、spring boot 3、spring framework 6、fastjson 2。baseaspectlogger、printcontrol 和 fastjson 是应用自有封装,接在接口切面处理链上;并非添加 spring aop 依赖就会自动获得这些行为。
坑点
坑点一:只限制长度,密码仍可能被打印
一份请求里有 name、password、token。我们需要保留 name,而不是从整个 json 中截出前几个字符。字段顺序一变,截断前的内容也会变;长度限制不能决定哪些字段有权进入日志。
正确写法
避坑一:序列化时过滤字段,再限制输出长度
字段过滤器按属性名和运行时类型判断是否输出:
@override
public boolean apply(object object, string name, object value) {
// 字段值是null默认会走全局配置给过滤掉
if (null == value) {
return false;
}
// 内置的字段类型规则
class valueclass = value.getclass();
if (bindingresult.class.isassignablefrom(valueclass) ||
multipartfile.class.isassignablefrom(valueclass) ||
servletrequest.class.isassignablefrom(valueclass) ||
servletresponse.class.isassignablefrom(valueclass)) {
return false;
}
// 按字段名称匹配的规则处理
if (ignorefieldnames.contains(name)) {
return false;
}
// 按字段类型简称精确匹配的规则处理
return !ignorefieldclasses.contains(valueclass.getsimplename());
}
}这里的 false 表示不序列化该属性。字段名精确匹配,因此 password 与 password 不是同一个规则;类型规则也按简称精确匹配,不是判断继承关系。
内置规则还会排除请求、响应、上传文件等对象,避免把这些对象直接展开。
先看过滤后的结果
以下是调用示例,使用虚构值,不打印真实密码:
map<string, object> input = new linkedhashmap<>();
input.put("name", "小林");
input.put("password", "demo-secret");
input.put("token", "demo-token");
string text = fastjson.obj2json(
input, new ignorerule("password,token", null), null);
system.out.println(text);
// {"name":"小林"}这是字段删除,不是把值改成星号。需要部分保留手机号等信息时属于值脱敏,本文不展开算法,也不把所有日志路径都视为已经脱敏。
配置多个字段时不要混入空格或重复项。当前规则用逗号拆分后构造 set,不自动 trim;“password, token” 中的第二项不能匹配 token,重复名称还可能导致规则构造失败。
切面日志怎样使用这份配置
printcontrol 中与本文有关的三个字段:
/**
* 忽略(不打印)的字段名称, 多个用英文逗号分隔
*/
private string ignorefieldname;
/**
* 忽略(不打印)的字段类型简称, 多个用英文逗号分隔
*/
private string ignorefieldclass;
/**
* 参数或返回值打印长度,0表示不输出,-1表示全输出
*/
private int printlength;baseaspectlogger 在获取参数文本时,把规则交给 fastjson,然后才截断:
/**
* 参数数组转换为日志输出的字符串
*/
private string fetchparam(printcontrol printcontrol, map<string, object> methodparammap) {
if (null == methodparammap) {
return param_null;
}
if (methodparammap.isempty()) {
return param_empty_map;
}
try {
string paramstr = fastjson.obj2json(
methodparammap,
parseignorerule(printcontrol),
aspectlogproperties.getglobaldesensitizecontrols());
return "{\"param\":" + printspecifiedlength(paramstr, printcontrol.getprintlength()) + "}";
} catch (exception e) {
string errormsg = string.format("fetchparam error: %s", throwableutil.getmessage(e));
log.error(errormsg, e);
return "{\"param\":\"" + errormsg + "\"}";
}
}配置属于 aspectlogproperties 的 aspecttypecontrols,接口接收对应 api_receive。下面仅演示装配值,实际应用需把配置对象注入切面日志处理器,并注册相应切面链:
printcontrol control = new printcontrol();
control.setignorefieldname("password,token");
control.setprintlength(500);
aspectlogproperties properties = new aspectlogproperties();
properties.setaspecttypecontrols(
map.of(aspecttypeenum.api_receive, control));实际项目可以通过 aspect.log 属性绑定提供这些值。本篇验证的是日志处理方法和过滤结果,没有运行完整 web 请求链,也不讨论 nacos 动态刷新。
500并不代表整条日志最多500字符
长度函数只截入参或返回值序列化得到的字符串:
/**
* 最后输出日志之前根据配置决定是否截取指定长度后输出
*/
private string printspecifiedlength(string str, int printlength) {
if (printlength > 0 && stringutils.length(str) > printlength) {
str = stringutils.substring(str, 0, printlength);
}
return str;
}应用名、接口名、耗时和额外包装不计入这个长度。截断后的文本也可能不再是合法 json,不能要求下游直接按完整 json 解析。
序列化在截断之前已经发生,所以这个上限不能避免大对象的序列化开销。java 字符串按 utf-16 单元截取,还可能切开一个补充字符。
printlength 为0时,正常请求前后日志分支直接跳过;未配置的切面也默认得到0。errorhandle 是独立分支,不能据此认为异常日志也被关闭或过滤。异常消息可能含敏感数据,仍需单独审查。
序列化失败时,代码会记录错误并返回错误描述,既没有保证错误文本安全,也没有保证包装后的字符串总是合法 json。敏感字段遗漏、被拼进普通字符串或异常消息,都不在字段名过滤的保证范围内。
总结
字段过滤决定“不写什么”,长度限制决定“最多保留多少序列化文本”。先验证敏感字段确实被移除,再设置输出上限;异常日志和已经拼好的字符串需要另外检查。
如果这篇文章对您有用,欢迎关注。后续会继续拆解 java 后端的实用代码,讲清写法、原理和使用时要注意的细节。
到此这篇关于spring aop 接口日志脱敏:控制打印内容,避免敏感字段外泄的文章就介绍到这了,更多相关spring aop 接口日志内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论