如何使用Golang實現一個API閘道器
你是否也存在過這樣的需求,想要公開一個介面到網路上。但是還得加點許可權,否則被人亂呼叫就不好了。這個許可權驗證的過程,最好越簡單越好,可能只是對比兩個字串相等就夠了。一般情況下我們遇到這種需要,就是在函式實現或者新增一個全域性的攔截器就夠了。但是還是需要自己來寫那部分雖然簡單但是很囉嗦的程式碼。那麼存不存在一種方式,讓我只管寫我的程式碼就完了,鑑權的事情交給其他人來做呢?
OpenAPI 一般情況下,就是允許企業內部提供對外介面的專案。你只管寫你的介面,然後,在我這裡註冊一下,我來負責你的呼叫許可權判定,如果他沒有許可權,我就告訴他沒有許可權,如果他存在許可權,我就轉調一下你的介面,然後把結果返回給他。其實情景是相似的,我們可以把這段需求抽象,然後做一個配置檔案版的開放介面。
想做這件事情,其實Golang是一個非常不錯的選擇,首先,Golang對於這種轉調的操作非常友好,甚至於,Golang語言本身就提供了一個反向代理的實現,我們可以直接使用Golang的原始框架就完全夠用。
在簡單分析一下我們的需求,其實很簡單,監聽的某一段Path之後,先判斷有沒有許可權,沒有許可權,直接回寫結果,有許可權交給反向代理來實現,輕鬆方便。既然是這樣,我們需要定義一下,路徑轉發的規則。
比如說我們嘗試給這個介面新增一個,當然這只是其中一個介面,我們應該要支援好多個介面
http://api.qingyunke.com/api.php?key=free&appid=0&msg=hello%20world.
在他進入到我們的系統中的時候看上去可能是這樣的。
http://localhost:5000/jiqiren/api.php?key=free&appid=0&msg=hello%20world.
所以,在我們的配置裡邊也應該是支援多個節點配置的。
{ "upstreams": [ { "upstream": "http://api.qingyunke.com", "path": "/jiqieren/", "trim_path": true, "is_auth": true } ], ... }
upstreams:上游伺服器
upstream:上游伺服器地址
path:路徑,如果以斜線結尾的話代表攔截所有以 /jiqiren/開頭的連結
trim_path:剔除路徑,因為上游伺服器中其實並不包含 /jiqiren/ 這段的,所以要踢掉這塊
is_auth:是否是授權連結
其實至此的上游的連結已經配置好了,下面我們來配置一下授權相關的配置。現在我實現的這個版本里邊允許同時存在多個授權型別。滿足任何一個即可進行介面的呼叫。我們先簡單配置一個bearer的版本。
{ ... "auth_items": { "Bearer": { "oauth_type": "BearerConfig", "configs": { "file": "bearer.json" } } } }
Bearer 對應的Model的意思是說,要引用配置檔案的型別,對應的檔案是 bearer.json
對應的檔案內容如下
{ "GnPIymAqtPEodx2di0cS9o1GP9QEM2N2-Ur_5ggvANwSKRewH2DLmw": { "interfaces": [ "/jiqieren/api.php" ], "headers": { "TenantId": "100860" } } }
其實就是一個Key對應了他能呼叫那些介面,還有他給上游伺服器傳遞那些資訊。因為Token的其實一般不光是能不能呼叫,同時他還代表了某一個服務,或者說某一個使用者,對應的,我們可以將這些資訊,放到請求頭中傳遞給上游伺服器。就可以做到雖然上游伺服器,並不知道Token但是上游伺服器知道誰能夠呼叫它。
下面我們來說一下這個專案是如何實現的。其實,整個功能簡單的描述起來就是一個帶了Token解析、鑑權的反向代理。但是本質上他還是一個反向代理,我們可以直接使用Golang自帶的反向代理。
核心程式碼如下。
package main import ( "./Configs" "./Server" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "net/http/httputil" "net/url" "os" "strings" ) func main() { var port int var config string flag.IntVar(&port, "port", 80, "server port") flag.StringVar(&config, "config", "", "mapping config") flag.Parse() if config == "" { log.Fatal("not found config") } if fileExist(config) == false { log.Fatal("not found config file") } data, err := ioutil.ReadFile(config) if err != nil { log.Fatal(err) } var configInstance Configs.Config err = json.Unmarshal(data, &configInstance) if err != nil { log.Fatal(err) } auths := make(map[string]Server.IAuthInterface) if configInstance.AuthItems != nil { for name, configItem := range configInstance.AuthItems { auth_item := Server.GetAuthFactoryInstance().CreateAuthInstance(configItem.OAuthType) if auth_item == nil { continue } auth_item.InitWithConfig(configItem.Configs) auths[strings.ToLower(name)] = auth_item log.Println(name, configItem) } } for i := 0; i < len(configInstance.Upstreams); i++ { up := configInstance.Upstreams[i] u, err := url.Parse(up.Upstream) log.Printf("{%s} => {%s}\r\n", up.Application, up.Upstream) if err != nil { log.Fatal(err) } rp := httputil.NewSingleHostReverseProxy(u) http.HandleFunc(up.Application, func(writer http.ResponseWriter, request *http.Request) { o_path := request.URL.Path if up.UpHost != "" { request.Host = up.UpHost } else { request.Host = u.Host } if up.TrimApplication { request.URL.Path = strings.TrimPrefix(request.URL.Path, up.Application) } if up.IsAuth { auth_value := request.Header.Get("Authorization") if auth_value == "" { writeUnAuthorized(writer) return } sp_index := strings.Index(auth_value, " ") auth_type := auth_value[:sp_index] auth_token := auth_value[sp_index+1:] if auth_instance, ok := auths[strings.ToLower(auth_type)]; ok { err, headers := auth_instance.GetAuthInfo(auth_token, o_path) if err != nil { writeUnAuthorized(writer) } else { if headers != nil { for k, v := range headers { request.Header.Add(k, v) } } rp.ServeHTTP(writer, request) } } else { writeUnsupportedAuthType(writer) } } else { rp.ServeHTTP(writer, request) } }) } log.Printf("http server start on :%d\r\n", port) http.ListenAndServe(fmt.Sprintf(":%d", port), nil) log.Println("finsh") } func writeUnsupportedAuthType(writer http.ResponseWriter) () { writer.Header().Add("Content-Type", "Application/json") writer.WriteHeader(http.StatusBadRequest) writer.Write([]byte("{\"status\":\"unsupported authorization\"}")) } func writeUnAuthorized(writer http.ResponseWriter) { writer.Header().Add("Content-Type", "Application/json") writer.WriteHeader(http.StatusUnauthorized) writer.Write([]byte("{\"status\":\"un-authorized\"}")) } func fileExist(filename string) bool { _, err := os.Stat(filename) return err == nil || os.IsExist(err) }
最核心的程式碼不足150行,簡單點說就是,在反向代理中間加上了鑑權的邏輯。當然鑑權的邏輯,我做了一層抽象,現在是通過配置檔案來進行動態修改的。
package Server import ( "log" "strings" ) type IAuthInterface interface { GetAuthInfo(token string, url string) (err error, headers map[string]string) InitWithConfig(config map[string]string) } type AuthFactory struct { } var auth_factory_instance AuthFactory func init() { auth_factory_instance = AuthFactory{} } func GetAuthFactoryInstance() *AuthFactory { return &auth_factory_instance } func (this *AuthFactory) CreateAuthInstance(t string) IAuthInterface { if strings.ToLower(t) == "bearer" { return &BeareAuth{} } if strings.ToLower(t) == "bearerconfig" { return &BearerConfigAuth{} } log.Fatalf("%s 是不支援的型別 \r\n", t) return nil }
package Server import ( "encoding/json" "errors" "io/ioutil" "log" ) type BearerConfigItem struct { Headers map[string]string `json:"headers"` Interfaces []string `json:"interfaces"` } type BearerConfigAuth struct { Configs map[string]*BearerConfigItem // token =》 config item } func (this *BearerConfigAuth) GetAuthInfo(token string, url string) (err error, headers map[string]string) { configItem := this.Configs[token] if configItem == nil { err = errors.New("not found token") return } if IndexOf(configItem.Interfaces, url) == -1 { err = errors.New("un-authorized") return } headers = make(map[string]string) for k, v := range configItem.Headers { headers[k] = v } return } func (this *BearerConfigAuth) InitWithConfig(config map[string]string) { cFile := config["file"] if cFile == "" { return } data, err := ioutil.ReadFile(cFile) if err != nil { log.Panic(err) } var m map[string]*BearerConfigItem //this.Configs = make(map[string]*BearerConfigItem) err = json.Unmarshal(data, &m) if err != nil { log.Panic(err) } this.Configs = m } func IndexOf(array []string, item string) int { for i := 0; i < len(array); i++ { if array[i] == item { return i } } return -1 }
當然了,其實這個只適合內部簡單使用,並不適合對外的真實的OpenAPI,因為Token現在太死了,Token應該是另外一個系統(鑑權中心)裡邊的處理的。包括企業自建應用的資訊建立、Token的兌換、重新整理等等。並且,不光是業務邏輯,還有非常強烈的效能要求,畢竟OpenAPI可以說是一個企業公開介面的門戶了,跟這種軟體打交道,效能也不能差了(我們公司這邊我們團隊也做了這麼一個系統,鑑權介面可以單機1W QPS,響應時間4ms),當然也是要花費不少心思的。
最後,這個專案已經開源了,給大家做個簡單的參考。
https://gitee.com/anxin1225/OpenAPI.GO
&n