草庐IT

日志里的敏感信息还在打明文?3 种日志脱敏方案任你选

1024个为什么 2023-04-09 原文

背景

我们打的日志中经常包含姓名、手机号、银行卡号等敏感信息,如果不做任何处理,就会以明文的形式展示在日志中,存在安全风险。

像下面这样:

我们需要一种能自动帮我们脱敏的工具,效果如下:

方案1 - 基于 logback

我们得先搞清楚消息内容是在哪里处理的,也就是配置文件中这个占位符的内容:

对应到源码是这里 ch.qos.logback.classic.PatternLayout :

这里可以看出来都是 通过这个类处理的  ch.qos.logback.classic.pattern.MessageConverter。

继续看一下这个类的逻辑:

public class MessageConverter extends ClassicConverter {
    public MessageConverter() {
    }


    public String convert(ILoggingEvent event) {
        return event.getFormattedMessage();
    }
}

只有一个 convert 方法,想要修改日志的内容,重载此方法即可。

public class TuoMinConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event){
        try {
            String msg = event.getFormattedMessage();
            // 这里自定义脱敏的逻辑
            return doTuoMin(msg);
        }catch (Exception e){
            return super.convert(event);
        }
    }
}

可以参考这位朋友的写法:

https://blog.csdn.net/u011277745/article/details/108793590

再从 logback.xml 中加上这行配置

<conversionRule conversionWord="msg" converterClass="com.TuoMinConverter "/>

两个属性说明一下:

conversionWord:不能乱配,要和图里红框内的内容一致。

converterClass:就配我们刚写的转换类 TuoMinConverter 。

缺点:

这种方案处理的是整条日志内容,一般是通过正则匹配再替换内容,不能精准替换指定属性的内容,还存在误杀情况。

这个例子中的 [country : 中国] 就被误杀。

只要满足我们定义的正则,就会被误杀,就会出现很多不该被脱敏的也脱敏了的情况。

方案2 - 基于 fastjson

思路是把要打印的内容脱敏后,再交给日志框架。

这里又回到上篇文章里《日志里打出来的都是时间戳?教你一行代码搞定它》说的 JSON.toJSONString()。

fastjson 提供了很多 Filter,我们这里的场景是在序列化成字符串的时候替换掉 json 键值对里 value 的值,所以要用到这个 Filter。

com.alibaba.fastjson.serializer.ValueFilter
public interface ValueFilter extends SerializeFilter {
    Object process(Object object, String name, Object value);
}

介绍一下 process 方法的 3 个参数:

object:本次用不到,不用关注

name:可以理解为当前处理 java 对象的属性名,比如只处理手机号(phone)的话就可以根据 name 过滤

if(name.equals("phone")){
    value = 脱敏逻辑(value);
}

value:属性对应的值,也就是我们真正要脱敏的内容

实现这个接口,在 process 方法中编写脱敏逻辑。

public class DataMaskFilter implements ValueFilter {


    public static DataMaskFilter instance(){
        return new DataMaskFilter();
    }


    @Override
    public Object process(Object object, String name, Object value) {
        for (DataMaskRuleEnum rule : DataMaskRuleEnum.values()) {
            List<String> fields = Arrays.asList(rule.fieldName.split("\\|"));
            if(fields.contains(name.toUpperCase())){
                value = value.toString().replaceAll(rule.regular, rule.result);
            }
        }
        return value;
    }
}

这里我把脱敏规则单独放到了一个枚举类中,因为在实际工作中,可能 phone, phoneNo, phoneNum  都表示手机号,抽到枚举中单独管理方便添加新的属性名。只要属性名在枚举配置里能找到,就对它脱敏。

public enum DataMaskRuleEnum {
    ID_CARD("身份证号脱敏", "IDCARD", "^(\\d{4})\\d+(\\d{4})$", "$1****$2"),
    PHONE("手机号脱敏", "PHONE|BANKPHONE", "^(\\d{3})\\d+(\\d{4})$", "$1****$2"),
    BANK_CARD("银行卡号脱敏", "CARDNUM", "^(\\d{4})\\d+(\\d{4})$", "$1****$2"),
    NAME("姓名脱敏", "NAME|REALNAME|WORKERNAME", "^([\\u4E00-\\u9FA5]{1,3})([\\u4E00-\\u9FA5])$", "**$2")
    ;


    DataMaskRuleEnum(String description, String fieldName, String regular, String result){
        this.description = description;
        this.fieldName = fieldName;
        this.regular = regular;
        this.result = result;
    }


    /**
     * 脱敏规则描述
     */
    public String description;
    /**
     * 要脱敏的属性名
     */
    public String fieldName;
    /**
     * 脱敏规则
     */
    public String regular;
    /**
     * 脱敏结果
     */
    public String result;
}

使用:

logger.info("保存银行卡操作日志信息, cardLogDto={}", 
    JSON.toJSONString(cardLogDto, DataMaskFilter.instance()));

优点:

可以精准脱敏,具体到指定的属性名,基本不存在误杀情况(除非属性名起的很怪)。

缺点:

每次打日志的时候都要考虑是否传 SerializeFilter 这个参数。

方案3 - 基于 logback + fastjson

这个方案是集前两个方案的优点于一身。

思路是把 fastjson 的脱敏,嵌套在 logback 的 MessageConverter 中。

public class TuoMinConverter extends MessageConverter {
    @Override
    public String convert(ILoggingEvent event){
        try {
            return doTuoMin(event);
        }catch (Exception e){
            return super.convert(event);
        }
    }


    private String doTuoMin(ILoggingEvent event){
        try {
            Object[] objects = Stream.of(event.getArgumentArray()).map(obj -> {
                String msg;
                if (obj instanceof String) {
                    // String 类型直接打印
                    msg = obj.toString();
                } else {
                    // 其他类型 通过 fastjson Filter 功能脱敏后转成 json 字符串
                    msg = JSON.toJSONString(obj);
                }
                return msg;
            }).toArray();
            return MessageFormatter.arrayFormat(event.getMessage(), objects).getMessage();
        } catch (Exception e) {
            return event.getMessage();
        }
    }
}

两个地方重点说明一下:

event.getArgumentArray() 是获取到 logger.info() 方法中的所有参数,获取到参数之后,遍历每个参数,String 类型的直接打印,其他类型的变成 json 字符串后返回。得到的 objects 数组就是经过脱敏处理的  json 字符串对象集合。

MessageFormatter.arrayFormat(event.getMessage(), objects) 是把脱敏处理过的内容填充到日志的占位符中。

额外说明一下:

细心的朋友可能已经发现脱敏处理就一行  msg = JSON.toJSONString(obj);  怎么就脱敏了???

玄机还是在上篇文章《日志里打出来的都是时间戳?教你一行代码搞定它》里提到的 SerializeConfig  全局配置,这个里面不仅可以添加 ObjectSerializer 还可以添加 SerializeFilter 。

static {
    SerializeConfig.getGlobalInstance().put(Date.class, new SimpleDateFormatSerializer("yyyy-MM-dd HH:mm:ss"));
    SerializeConfig.getGlobalInstance().addFilter(User.class, DataMaskFilter.instance());
    SerializeConfig.getGlobalInstance().addFilter(UserAccountDto.class, DataMaskFilter.instance());
}

可以为指定的 java 类型添加指定的 SerializeFilter ,需要为哪些 java 对象脱敏,加到这里就行,做到了更加精准的脱敏。

通过全局配置,toJSONString(obj)  就不用再传其他参数了,也支持集合。

看看效果

是不是很过瘾,以后就连日志参数都不用每次加一层 JSON.toJSONString()了,随意往里面扔各种对象。

缺点:

这么完美的方案哪还有缺点。

优点:

时间戳、脱敏 一站式解决,好不好用一看就知道。

扯两句

只要是重复的代码,就能往外抽

以后日志就这么打,让他们好奇去吧

原创不易,多多关注,一键三连,感谢支持!

参考文献:

https://blog.csdn.net/weixin_43897590/article/details/115729271

https://blog.csdn.net/u011277745/article/details/108793590

有关日志里的敏感信息还在打明文?3 种日志脱敏方案任你选的更多相关文章

  1. ruby-on-rails - Rails 常用字符串(用于通知和错误信息等) - 2

    大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. ruby - what is - gets is a directory - 错误信息 - 2

    我遇到了这个奇怪的错误.../Users/gideon/Documents/ca_ruby/rubytactoe/lib/player.rb:13:in`gets':Isadirectory-spec(Errno::EISDIR)player_spec.rb:require_relative'../spec_helper'#theuniverseisvastandinfinite...itcontainsagame....butnoplayersdescribe"tictactoegame"docontext"theplayerclass"doit"musthaveahumanplay

  6. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  7. ruby - Sinatra 中的全局救援和日志记录异常 - 2

    如何在出现异常时指定全局救援,如果您将Sinatra用于API或应用程序,您将如何处理日志记录? 最佳答案 404可以在not_found方法的帮助下处理,例如:not_founddo'Sitedoesnotexist.'end500s可以通过调用带有block的错误方法来处理,例如:errordo"Applicationerror.Plstrylater."end错误的详细信息可以通过request.env中的sinatra.error访问,如下所示:errordo'Anerroroccured:'+request.env['si

  8. ruby-on-rails - 使用 Ruby 标准 Logger 每天只创建一个日志 - 2

    我正在使用ruby​​标准记录器,我想要每天轮换一次,所以在我的代码中我有:Logger.new("#{$ROOT_PATH}/log/errors.log",'daily')它运行完美,但它创建了两个文件errors.log.20130217和errors.log.20130217.1。如何强制它每天只创建一个文件? 最佳答案 您的代码对于长时间运行的应用程序是正确的。发生的事情是您在给定的一天多次运行代码。第一次运行时,Ruby会创建一个日志文件“errors.log”。当日期改变时,Ruby将文件重命名为“errors.log

  9. ruby - Cucumber/Savon 省略或删除日志输出 - 2

    在运行Cucumber测试时,我得到(除了测试结果)大量调试/日志相关的输出形式:D,[2013-03-06T12:21:38.911829#49031]DEBUG--:SOAPrequest:D,[2013-03-06T12:21:38.911919#49031]DEBUG--:Pragma:no-cache,SOAPAction:"",Content-Type:text/xml;charset=UTF-8,Content-Length:1592W,[2013-03-06T12:21:38.912360#49031]WARN--:HTTPIexecutesHTTPPOSTusingt

  10. ruby-on-rails - faraday如何设置日志级别 - 2

    我最近将我的http客户端切换到faraday,一切都按预期工作。我有以下代码来创建连接:@connection=Faraday.new(:url=>base_url)do|faraday|faraday.useCustim::Middlewarefaraday.request:url_encoded#form-encodePOSTparamsfaraday.request:jsonfaraday.response:json,:content_type=>/\bjson$/faraday.response:loggerfaraday.adapterFaraday.default_ada

随机推荐