1. 程式人生 > >如何使用Golang實現一個API閘道器

如何使用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