• 如何使用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}
    ", 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
    ", 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 是不支持的类型 
    ", 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

  • 相关阅读:
    【NOI2005T4】聪聪和可可-期望DP+记忆化搜索
    总结:最大权闭合子图
    【NOI2009T4】植物大战僵尸-最大权闭合子图+拓补排序
    codevs 1090 加分二叉树
    codevs 1503 愚蠢的宠物
    codevs 1992 聚会
    welcome to new life
    codevs 1066 引水入城
    codevs 2021 中庸之道
    POJ 2104 K-th Number
  • 原文地址:https://www.cnblogs.com/anxin1225/p/12842365.html
Copyright © 2020-2023  润新知