你是否也存在過這樣的需求,想要公開一個接口到網絡上。可是還得加點權限,不然被人亂調用就很差了。這個權限驗證的過程,最好越簡單越好,可能只是對比兩個字符串相等就夠了。通常狀況下咱們遇到這種須要,就是在函數實現或者添加一個全局的攔截器就夠了。可是仍是須要本身來寫那部分雖然簡單可是很囉嗦的代碼。那麼存不存在一種方式,讓我只管寫個人代碼就完了,鑑權的事情交給其餘人來作呢?php
OpenAPI 通常狀況下,就是容許企業內部提供對外接口的項目。你只管寫你的接口,而後,在我這裏註冊一下,我來負責你的調用權限斷定,若是他沒有權限,我就告訴他沒有權限,若是他存在權限,我就轉調一下你的接口,而後把結果返回給他。其實情景是類似的,咱們能夠把這段需求抽象,而後作一個配置文件版的開放接口。git
想作這件事情,其實Golang是一個很是不錯的選擇,首先,Golang對於這種轉調的操做很是友好,甚至於,Golang語言自己就提供了一個反向代理的實現,咱們能夠直接使用Golang的原始框架就徹底夠用。
在簡單分析一下咱們的需求,其實很簡單,監聽的某一段Path以後,先判斷有沒有權限,沒有權限,直接回寫結果,有權限交給反向代理來實現,輕鬆方便。既然是這樣,咱們須要定義一下,路徑轉發的規則。json
好比說咱們嘗試給這個接口添加一個,固然這只是其中一個接口,咱們應該要支持好多個接口api
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.網絡
因此,在咱們的配置裏邊也應該是支持多個節點配置的。app
{ "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),固然也是要花費很多心思的。
最後,這個項目已經開源了,給你們作個簡單的參考。