如何使用Golang實現一個API網關

你是否也存在過這樣的需求,想要公開一個接口到網絡上。可是還得加點權限,不然被人亂調用就很差了。這個權限驗證的過程,最好越簡單越好,可能只是對比兩個字符串相等就夠了。通常狀況下咱們遇到這種須要,就是在函數實現或者添加一個全局的攔截器就夠了。可是仍是須要本身來寫那部分雖然簡單可是很囉嗦的代碼。那麼存不存在一種方式,讓我只管寫個人代碼就完了,鑑權的事情交給其餘人來作呢?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),固然也是要花費很多心思的。

 

最後,這個項目已經開源了,給你們作個簡單的參考。

https://gitee.com/anxin1225/OpenAPI.GO

相關文章
相關標籤/搜索