我其實滿驚訝目前好像沒有任何library有實作這個,或者有但是我找不到,所以我就實作了一個。別看這功能看起來很小巧簡單,這個需要對reflect有相當的認識,所以把製作心得記錄下來。
成品網址
需求
通常我們server boot up的時候需要印出很多log,有時候無可避免的要把整個config給印到log上,這樣會產生一個問題:要是config內含敏感資訊,如帳號跟密碼,這就會產生一些資安問題:
log.Infof("bootup with config : %+v", config)
印出來就會像這樣(標準的%+v
會印出來的格式)
{Host:192.168.1.1 User:username Password:mypassword123}
顯然,這是不太恰當的,所以我們總歸來講有幾種作法
- 把他轉json再印,把敏感的地方用
json:"-"
讓他不被印出來 - 實作
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:
- 檢查是否tag在string上
- 準備印出跟
fmt.Printf("%+v")
相容的格式
- 若沒有tag
- 檢查型別是否struct,是的話對他執行recursive
- 其他型別直接用
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
}