欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

如何使用Golang实现一个API网关

程序员文章站 2022-05-03 09:46:26
你是否也存在过这样的需求,想要公开一个接口到网络上。但是还得加点权限,否则被人乱调用就不好了。这个权限验证的过程,最好越简单越好,可能只是对比两个字符串相等就够了。一般情况下我们遇到这种需要,就是在函数实现或者添加一个全局的拦截器就够了。但是还是需要自己来写那部分虽然简单但是很啰嗦的代码。那么存不存 ......

你是否也存在过这样的需求,想要公开一个接口到网络上。但是还得加点权限,否则被人乱调用就不好了。这个权限验证的过程,最好越简单越好,可能只是对比两个字符串相等就够了。一般情况下我们遇到这种需要,就是在函数实现或者添加一个全局的拦截器就够了。但是还是需要自己来写那部分虽然简单但是很啰嗦的代码。那么存不存在一种方式,让我只管写我的代码就完了,鉴权的事情交给其他人来做呢?

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

如何使用Golang实现一个API网关