針對(duì)大規(guī)模服務(wù)日志敏感信息的長(zhǎng)效治理實(shí)踐
1 背景
近年來(lái),國(guó)家采取了多項(xiàng)重要舉措來(lái)加強(qiáng)個(gè)人數(shù)據(jù)保護(hù),包括實(shí)施《中華人民共和國(guó)網(wǎng)絡(luò)安全法》和《個(gè)人信息保護(hù)法》等法律法規(guī)。這些舉措旨在確保用戶隱私的安全,同時(shí)確保企業(yè)合規(guī)運(yùn)營(yíng)。在處理敏感數(shù)據(jù)時(shí),企業(yè)有責(zé)任采取適當(dāng)?shù)拇胧﹣?lái)保護(hù)用戶信息。
在數(shù)據(jù)保護(hù)方面,日志記錄成為一個(gè)需要特別關(guān)注的敏感信息領(lǐng)域。因此,本文將重點(diǎn)介紹轉(zhuǎn)轉(zhuǎn)在日志脫敏方面的應(yīng)用與實(shí)踐。
2 目標(biāo)與措施
目標(biāo):對(duì)日志內(nèi)的手機(jī)號(hào)、身份證號(hào)、銀行卡號(hào)等敏感信息脫敏,建立一個(gè)可持續(xù)的日志敏感信息管控機(jī)制。
措施:
- 檢測(cè)和定位存在敏感日志的服務(wù)與CASE;
 - 開(kāi)發(fā)低接入成本的日志脫敏工具;
 - 推動(dòng)相關(guān)業(yè)務(wù)進(jìn)行迭代修改;
 - 長(zhǎng)期監(jiān)控和持續(xù)治理,確保日志安全。
 
圖片
我們的第一步是利用大數(shù)據(jù)離線掃描服務(wù)日志,并使用正則表達(dá)式匹配敏感信息。
然而,第二和第三步是挑戰(zhàn)的關(guān)鍵,即如何在不干擾業(yè)務(wù)正常迭代排期的情況下,推動(dòng)大量服務(wù)的日志做脫敏。我們希望使用技術(shù)手段盡量降低業(yè)務(wù)日志脫敏的人力成本。
3 實(shí)施
參考《轉(zhuǎn)轉(zhuǎn)日志規(guī)范》查看標(biāo)準(zhǔn)日志輸出要求,在此基礎(chǔ)之上,提供一些工具輔助業(yè)務(wù)對(duì)日志脫敏。
【推薦】JavaBean類需實(shí)現(xiàn)toString()方法,日志直接打印對(duì)象,慎用JSON工具將對(duì)象轉(zhuǎn)換成String。
3.1 脫敏工具類
我們開(kāi)發(fā)了脫敏工具類,期望業(yè)務(wù)同學(xué)在實(shí)現(xiàn)JavaBean toString()方法的同時(shí),使用脫敏工具對(duì)敏感字段使用脫敏。
圖片
- desensitize(String input):通用脫敏函數(shù),支持對(duì)任意字符脫敏,將提取字符串中4位以上數(shù)字(如手機(jī)號(hào)、銀行卡號(hào)、身份證號(hào)、數(shù)字驗(yàn)證碼等)做脫敏;
 - desensitizeByInputLength(String input):據(jù)字符串長(zhǎng)度匹配不同的脫敏規(guī)則,如:11位則使用手機(jī)號(hào)脫敏規(guī)則,18位則使用身份證號(hào)脫敏規(guī)則;
 - desensitizePhoneNumber(String phoneNumber):脫敏手機(jī)號(hào),前3位和后4位,中間的數(shù)字用*代替;
 - desensitizeIDCard(String idCard):脫敏身份證號(hào), 保留前6位和后4位,脫敏7~15位生日信息, 用*代替;
 - desensitizeBankCardNumber(String bankCardNumber):脫敏銀行卡號(hào), 前6位和后4位,中間的數(shù)字用*代替。
 
public final class DesensitizeUtil {    
    
    /**
     * 根據(jù)字符串長(zhǎng)度匹配不同的脫敏函數(shù), 強(qiáng)制脫敏
     */
    public static String desensitizeByInputLength(String input) {
        int length = input.length();
        // 手機(jī)號(hào)
        if (length == 11) {
            return desensitizePhoneNumber(input);
        }
        // ,,,
    }
    /**
     * 脫敏手機(jī)號(hào), 前3位和后4位,中間的數(shù)字用*代替
     */
    public static String desensitizePhoneNumber(String phoneNumber) {
        // 11位手機(jī)號(hào)
        if (phoneNumber.length() == 11) {
            return phoneNumber.substring(0, phoneNumber.length() - 8) + "****" + phoneNumber.substring(phoneNumber.length() - 4);
        }
        return phoneNumber;
    }
    
    // 省略其他脫敏函數(shù)...
}3.2 JSON脫敏
在某些日志記錄的場(chǎng)景中,會(huì)打印包含敏感字段的JSON格式的數(shù)據(jù),需要對(duì)其中的敏感信息進(jìn)行脫敏處理。
在常見(jiàn)的JSON工具中,比如Jackson,可以使用自定義的序列化器/反序列化器來(lái)實(shí)現(xiàn)脫敏。下面以Jackson為例進(jìn)行說(shuō)明:
首先,我們可以定義一個(gè)注解來(lái)標(biāo)注哪些字段需要脫敏處理:
/**
 * 脫敏注解
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Desensitize {
 
}然后,我們可以創(chuàng)建一個(gè)自定義的Jackson模塊,通過(guò)繼承BeanSerializerModifier類來(lái)修改字段的序列化行為。在這個(gè)類中,我們可以根據(jù)字段上的Desensitize注解來(lái)判斷是否需要進(jìn)行脫敏處理:
/**
 * Jackson脫敏序列化修改器
 */
public class JacksonDesensitizeSerializerModifier extends BeanSerializerModifier {
    @Override
    public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
                                                     List<BeanPropertyWriter> beanProperties) {
        for (BeanPropertyWriter beanProperty : beanProperties) {
            // 只針對(duì)使用了@Desensitize的字段做脫敏
            Desensitize desensitize = beanProperty.getAnnotation(Desensitize.class);
            if(desensitize != null) {
                // 指定自定義的序列化器
                beanProperty.assignSerializer(new Desensitization());
            }
        }
        return beanProperties;
    }
    /**
     * Jackson序列化器
     */
    public class Desensitization extends StdSerializer<Object> {
        @Override
        public final void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
            // 根據(jù)長(zhǎng)度對(duì)字段做脫敏
            String desensitize = DesensitizeUtil.desensitizeByInputLength(String.valueOf(value));
            gen.writeString(desensitize);
        }
    }
}最后,我們需要注冊(cè)這個(gè)自定義的模塊到Jackson中
/**
 * JSON工具
 */
public class JsonUtil {
    private static final ObjectMapper DESENSITIZE_OBJECT_MAPPER = newObjectMapper();
    private static ObjectMapper newObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        //增加脫敏序列化器
        SimpleModule simpleModule = new SimpleModule("SimpleModuleDesensitize");
        simpleModule.setSerializerModifier(new JacksonDesensitizeSerializerModifier());
        mapper.registerModule(simpleModule);
        return mapper;
    }
    
   /**
    * 對(duì)象轉(zhuǎn)JSON的自動(dòng)脫敏工具
    */
    public static <T> String object2DesensitizeString(T object) throws JsonProcessingException {
        return DESENSITIZE_OBJECT_MAPPER.writeValueAsString(object);
    }
    
    //...
}對(duì)于業(yè)務(wù)同學(xué)而言,只需在需要脫敏的對(duì)象上添加脫敏注解,然后使用我們提供的JsonUtil進(jìn)行脫敏操作,實(shí)現(xiàn)簡(jiǎn)單高效。
/**
 * 需要脫敏的對(duì)象
 */
public class User {
      
    /**
     * 標(biāo)記此字段需要脫敏
     */
    @Desensitize
    private String mobile;
    private String username;
    
    //getter setter...
}
User user = new User();
user.setAge(18);
user.username = "zhangsan";
user.password = "123456";
JsonUtil.object2DesensitizeString(user);
//輸出結(jié)果: {"mobile":"135****5555","username":"張三"}注意:以上代碼只是一個(gè)示例,并不完整。在實(shí)際使用中,還需要根據(jù)具體的需求來(lái)靈活實(shí)現(xiàn)脫敏處理。
3.3 APT自動(dòng)脫敏
在實(shí)際實(shí)施過(guò)程中,以上兩個(gè)方案遇到了很多阻礙。主要問(wèn)題在于業(yè)務(wù)同學(xué)手動(dòng)維護(hù)Bean的toString()方法過(guò)于繁瑣、重復(fù)工作多、容易遺漏對(duì)象并導(dǎo)致增加或刪除字段時(shí)需要不斷修改toString()函數(shù)。此外,業(yè)務(wù)服務(wù)所依賴的Bean來(lái)源復(fù)雜,有可能是其他業(yè)務(wù)提供的第二方Jar包或第三方Jar包。
因此,在實(shí)際應(yīng)用中,業(yè)務(wù)同學(xué)更傾向于將Bean序列化為JSON并輸出到日志中,如下所示:
log.info("data={}", JsonUtil.object2DesensitizeString(bean));然而,這種方法不符合《轉(zhuǎn)轉(zhuǎn)日志規(guī)范》要求,而且忽略了JSON序列化性能的問(wèn)題。此外,這種方案也需要耗費(fèi)大量的人力資源:需要評(píng)估每一行日志,以確定是否需要添加JSON脫敏功能。
因此,業(yè)務(wù)同學(xué)提出了以下需求:是否可以實(shí)現(xiàn)類似Lombok一樣的功能,只需在Bean的字段上添加脫敏注解,就能在編譯期自動(dòng)實(shí)現(xiàn)脫敏后的toString()函數(shù)?這樣的話,在打印日志時(shí)直接打印對(duì)象即可自動(dòng)脫敏。
經(jīng)過(guò)調(diào)研發(fā)現(xiàn),Lombok在編譯時(shí)利用APT(Annotation Processing Tools)生成代碼,實(shí)現(xiàn)了自動(dòng)化的代碼生成過(guò)程,從而簡(jiǎn)化了開(kāi)發(fā)工作。
APT(Annotation Processing Tool)是Java的編譯期注解處理器。它允許開(kāi)發(fā)人員在編譯期間處理注解,并根據(jù)注解和相關(guān)對(duì)象的信息生成Java代碼模板或配置文件等。
APT的使用可以提高程序性能,因?yàn)樗诖a編譯時(shí)完成注解處理,而不是在運(yùn)行時(shí)使用反射方式處理注解。
著名的開(kāi)源框架,如Lombok、MapStruct和AutoService等,也使用了類似的技術(shù)來(lái)優(yōu)化代碼的生成和處理過(guò)程。
圖片
我們利用APT技術(shù)實(shí)現(xiàn)了這樣的功能:如果一個(gè)類沒(méi)有重寫(xiě)Object.toString()方法,在編譯時(shí)會(huì)自動(dòng)為該類生成一個(gè)脫敏后的toString()方法。這個(gè)自動(dòng)生成的toString()方法能夠識(shí)別脫敏注解,并在生成的toString()方法內(nèi)對(duì)敏感信息進(jìn)行脫敏處理。
在Java編譯后的Class文件中,toString()方法可能來(lái)自三個(gè)來(lái)源:源代碼、轉(zhuǎn)轉(zhuǎn)APT處理、Lombok等。優(yōu)先級(jí)為:源代碼 > 轉(zhuǎn)轉(zhuǎn)APT處理 > Lombok等其他APT。簡(jiǎn)言之,我們的APT處理不會(huì)覆蓋源代碼中定義的toString()方法,但會(huì)覆蓋由Lombok生成的toString()方法。
比如,我們有以下源碼:
class User {
    private String username;
    /**
     * 密碼,增加了脫敏注解
     */
    @Desensitize
    private String password;
}在接入轉(zhuǎn)轉(zhuǎn)APT后,反編譯的Class文件如下:
class User {
    private String username;
    @Desensitize
    private String password;
    public String toString() {
        StringJoiner sj = new StringJoiner(", ", "User[", "]");
        
        if (this.username != null) {
            sj.add("username=" + this.username);
        }
 
        if (this.password != null) {
            sj.add("password=" + DesensitizeUtil.desensitizeByInputLength(value));
        }
 
        return sj.toString();
    }
}測(cè)試如下:
User user = new User();
user.username = "zhangsan";
user.password = "123456";
System.out.println(user);  
//輸出結(jié)果: User[username=張三, password=1****6]這個(gè)功能的上線大大降低了業(yè)務(wù)同學(xué)實(shí)現(xiàn)日志脫敏的工作量,只需為字段添加脫敏注解即可。同時(shí),也解決了線上對(duì)象未重寫(xiě)Object.toString()時(shí)打印日志的尷尬問(wèn)題。
不過(guò),在落地APT過(guò)程中,我們也遇到了一些問(wèn)題,希望能給讀者提供一些有收益的參考。
3.3.1 本地緩存問(wèn)題
在某個(gè)服務(wù)的Spring Bean上,有一個(gè)包含大量本地緩存的List字段,這個(gè)服務(wù)會(huì)打印Spring Bean對(duì)象到日志中。在引入轉(zhuǎn)轉(zhuǎn)APT之前,一切正常;但引入后,出現(xiàn)了頻繁的OOM問(wèn)題。通過(guò)內(nèi)存分析后發(fā)現(xiàn),問(wèn)題出在轉(zhuǎn)轉(zhuǎn)APT為Spring Bean自動(dòng)生成的toString()函數(shù)內(nèi)產(chǎn)生了大量的字符串上。
@Service
public class AppService {
    /**
     * 本地緩存
     */
    private List<Object> cache = new ArrayList<>();
    
}
@Autowired
private AppService service;
log.info("service={}", service);我們觀察到大部分帶有本地緩存(或者高內(nèi)存占用字段)的對(duì)象都是Spring的Bean,因此,我們對(duì)轉(zhuǎn)轉(zhuǎn)APT進(jìn)行了修改:即不再為Spring Bean生成toString()函數(shù)。
3.3.2 JDK序列化問(wèn)題
某個(gè)服務(wù)的JavaBean使用了原生JDK的序列化/反序列化工具,但是這個(gè)JavaBean卻沒(méi)有添加serialVersionUID。
class Person implements Serializable {
    // 沒(méi)有定義serialVersionUID
    // private static final long serialVersionUID = -55721300387280236L;
}Java序列化機(jī)制使用long型的serialVersionUID字段來(lái)標(biāo)志類的版本號(hào);序列化對(duì)象時(shí),JVM會(huì)將serialVersionUID的值寫(xiě)入序列化數(shù)據(jù)中;反序列化時(shí),JVM會(huì)將序列化數(shù)據(jù)中的serialVersionUID與對(duì)應(yīng)類中的serialVersionUID進(jìn)行比較,若不同,則拋出InvalidCastException;若版本號(hào)相同,則能夠進(jìn)行反序列化。
當(dāng)一個(gè)類沒(méi)有顯式定義serialVersionUID時(shí),JVM會(huì)自動(dòng)根據(jù)類的信息計(jì)算生成一個(gè)默認(rèn)的serialVersionUID。這樣,在類發(fā)生變化時(shí),自動(dòng)生成的serialVersionUID可能會(huì)改變,導(dǎo)致無(wú)法正確反序列化之前的數(shù)據(jù)。
引入轉(zhuǎn)轉(zhuǎn)APT后,由于自動(dòng)生成了toString函數(shù),類信息發(fā)生變化,導(dǎo)致serialVersionUID也發(fā)生了改變,進(jìn)而導(dǎo)致反序列化失敗。
解決方式是將之前默認(rèn)生成的serialVersionUID找到,并將其添加到類的源碼中。
3.4 棄用方案
還有一種快速落地的方法是,通過(guò)在應(yīng)用程序內(nèi)部統(tǒng)一攔截日志輸出,正則匹配敏感信息,并利用脫敏工具進(jìn)行脫敏處理。
我們沒(méi)有使用這種方式的原因是因?yàn)椋好撁魬?yīng)盡量避免正則匹配,容易誤傷且性能低下。
4 規(guī)劃
上文提過(guò),服務(wù)內(nèi)依賴的Java Bean來(lái)源十分復(fù)雜,我們目前只解決了對(duì)象本身的脫敏問(wèn)題。而對(duì)于服務(wù)依賴的Jar包版本控制,仍需要業(yè)務(wù)團(tuán)隊(duì)梳理依賴關(guān)系,并手動(dòng)修改脫敏后的Jar包版本,這一過(guò)程仍需要耗費(fèi)較多的時(shí)間和人力。
考慮到這個(gè)問(wèn)題,是否可以為每個(gè)服務(wù)提供一個(gè)依賴關(guān)系管控系統(tǒng)?該系統(tǒng)可以對(duì)Jar包的版本實(shí)現(xiàn)自動(dòng)更新、自動(dòng)化測(cè)試、灰度發(fā)布、自動(dòng)發(fā)布和回滾等一系列功能。對(duì)于轉(zhuǎn)轉(zhuǎn)目前的情況來(lái)說(shuō),我相信這不是一個(gè)技術(shù)問(wèn)題,而是一個(gè)需要更多時(shí)間來(lái)完善的TODO List。
圖片
5 總結(jié)
一個(gè)小小的功能日志脫敏,卻經(jīng)歷了多個(gè)階段與挑戰(zhàn),從敏感日志的發(fā)現(xiàn)到開(kāi)發(fā)脫敏工具類,再到Json脫敏,再到APT脫敏,最終推動(dòng)業(yè)務(wù)應(yīng)用。核心的挑戰(zhàn)在于如何做好推動(dòng)相關(guān)的工作?
我認(rèn)為,推動(dòng)相關(guān)工作的核心在于有效應(yīng)對(duì)內(nèi)在和外在的因素。然而,外部因素對(duì)推動(dòng)的阻力常常更大,要成功推動(dòng)工作,轉(zhuǎn)變外部阻力為內(nèi)部動(dòng)力至關(guān)重要。而對(duì)于推動(dòng)者而言,換位思考、勇于挑戰(zhàn)未知、深入追根究底的打磨產(chǎn)品會(huì)使產(chǎn)品更容易被接受和推廣。















 
 
 













 
 
 
 