我其實滿驚訝目前好像沒有任何library有實作這個,或者有但是我找不到,所以我就實作了一個。別看這功能看起來很小巧簡單,這個需要對reflect有相當的認識,所以把製作心得記錄下來。

成品網址

https://github.com/Rayer/hood

需求

通常我們server boot up的時候需要印出很多log,有時候無可避免的要把整個config給印到log上,這樣會產生一個問題:要是config內含敏感資訊,如帳號跟密碼,這就會產生一些資安問題:

log.Infof("bootup with config : %+v", config)

印出來就會像這樣(標準的%+v會印出來的格式)

{Host:192.168.1.1 User:username Password:mypassword123}


顯然,這是不太恰當的,所以我們總歸來講有幾種作法

  1. 把他轉json再印,把敏感的地方用 json:"-"讓他不被印出來
  2. 實作 func String() string,把敏感資料手動by key抹除掉

不過log有log的格式,尤其系統中要是預期用go struct原生格式parse一些warning的話,更改log格式並不恰當,所以1不是個好方法。至於2,手動實在是….不是很討喜,況且要是之後機敏資料多了,加都加不完也容易遺漏。

所以最好的方法應該是:tag在Config這個struct的field裡面,來宣告哪些是敏感資料。

type Config struct {
    Host     string
    User     string `confidential:"1,1"`
    Password string `confidential:"2,2"`
}

不過如果要更改為數眾多以%v %+v印出的地方,也不是很現實,所以override func String() string 讓他能在上面提到的地方自動印出consealed的資料是最好的。

所以可能的成品大概就長這樣

type ConfigWithTag struct {
    Host     string
    User     string `confidential:"1,1"`
    Password string `confidential:"2,2"`
}

func (c ConfigWithTag) String() string {
    ret, _ := hood.PrintConfidentialData(c)
    return ret
}

實作

要對tag操作,基本上都跑不掉reflect。取一個field上面的tag其實是一件滿麻煩的事情,他也不會幫你parse裡面的value。是有一些library幫你作這方面的事情,比方說https://github.com/fatih/structtag,不過這次就先用原生的方法來實作,畢竟雖然麻煩,但是其實這個場景還沒有複雜到需要用3rd party。

首先我們看一下原生的%+v是長什麼樣子

{Host:192.168.1.1 User:username Password:mypassword123}

所以我們目標是輸出的樣子跟他一致,大概會像

{Host:192.168.1.1 User:******** Password:*************}

Go的reflect有取型別跟取值兩個,分別是.TypeOf()以及.ValueOf()。不過,取型別才有tag資訊,所以要是以tag來改值的話,那我們兩者都要取出來,然後兩者可以用.Field(i)來溝通。以type取到的field會有tag資訊,以value取到的field才會有值。

所以又要取值又要取tag的話,大致上會看起來像下面這樣

ift := reflect.TypeOf(binding)
ifv := reflect.ValueOf(binding)

for i := 0; i < ift.NumField(); i++ {
	tag := ift.Field(i).Tag
	value := ifv.Field(i).Interface()
}

其中Interface()回傳interface{},如果確定型別的話,也可以直接以該型別取值,比方說String()。不過在我們這例子來講,照樣用interface即可。首先,我們可以先把有tag跟沒tag的field分類:

if tagContent, exists := field.Tag.Lookup("confidential"); exists {
	//有tag
	} else {
	//無tag,直接印出來
	...
	{

整個決策樹應該是類似這樣

  • 若有tag:
    1. 檢查是否tag在string上
    2. 準備印出跟fmt.Printf("%+v")相容的格式
  • 若沒有tag
    1. 檢查型別是否struct,是的話對他執行recursive
    2. 其他型別直接用fmt.Sprintf印出相容格式

剩下就是簡單的parse的問題了 可以參考 https://github.com/Rayer/hood/blob/c7a9fb4d33da569ac3e330672e9712d11c50e88b/hood.go

裡面的code差不多長這樣

func PrintConfidentialData(binding interface{}) (string, error) {
	ift := reflect.TypeOf(binding)
	ifv := reflect.ValueOf(binding)
	var kvString []string

	for i := 0; i < ifv.NumField(); i++ {
		field := ift.Field(i)
		if tagContent, exists := field.Tag.Lookup("confidential"); exists {
			valueField := ifv.Field(i)
			if valueField.Kind() != reflect.String {
				return "", fmt.Errorf("confidental tag can be only applied on string")
			}
			values := strings.Split(tagContent, ",")

			keepFirst := 0
			var err error
			if len(values) > 0 && strings.Trim(values[0], " ") != "" {
				keepFirst, err = strconv.Atoi(strings.Trim(values[0], " "))
				if err != nil {
					return "", fmt.Errorf("confidental value can only be integer")
				}
			}
			keepTail := 0
			if len(values) > 1 {
				keepTail, err = strconv.Atoi(strings.Trim(values[1], " "))
				if err != nil {
					return "", fmt.Errorf("confidental value can only be integer")
				}
			}

			ret := ""

			value := valueField.String()
			if len(value) < keepFirst && len(value) < keepTail {
				ret = value
			} else {
				for i := 0; i < len(value); i++ {
					if i < keepFirst || i >= len(value)-keepTail {
						ret += string(value[i])
					} else {
						ret += "*"
					}
				}
			}

			kvString = append(kvString, fmt.Sprintf("%v:%v", field.Name, ret))

		} else {
			t := field.Type
			if t.Kind() == reflect.Struct {
				inner, err := PrintConfidentialData(ifv.Field(i).Interface())
				if err != nil {
					return "", err
				}
				kvString = append(kvString, inner)
			} else {
				kvString = append(kvString, fmt.Sprintf("%v:%v", field.Name, ifv.Field(i).Interface()))
			}
		}
	}

	return "{" + strings.Join(kvString, " ") + "}", nil
}

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *