热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

开发笔记:如何使用Golang实现一个API网关

篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何使用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 {
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 {
if array[i] == item {
return i
}
}
return -1
}

当然了,其实这个只适合内部简单使用,并不适合对外的真实的OpenAPI,因为Token现在太死了,Token应该是另外一个系统(鉴权中心)里边的处理的。包括企业自建应用的信息创建、Token的兑换、刷新等等。并且,不光是业务逻辑,还有非常强烈的性能要求,毕竟OpenAPI可以说是一个企业公开接口的门户了,跟这种软件打交道,性能也不能差了(我们公司这边我们团队也做了这么一个系统,鉴权接口可以单机1W QPS,响应时间4ms),当然也是要花费不少心思的。

 

最后,这个项目已经开源了,给大家做个简单的参考。

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

技术图片

 


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 关键词:Golang, Cookie, 跟踪位置, net/http/cookiejar, package main, golang.org/x/net/publicsuffix, io/ioutil, log, net/http, net/http/cookiejar ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • 怎么在PHP项目中实现一个HTTP断点续传功能发布时间:2021-01-1916:26:06来源:亿速云阅读:96作者:Le ... [详细]
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • Android系统源码分析Zygote和SystemServer启动过程详解
    本文详细解析了Android系统源码中Zygote和SystemServer的启动过程。首先介绍了系统framework层启动的内容,帮助理解四大组件的启动和管理过程。接着介绍了AMS、PMS等系统服务的作用和调用方式。然后详细分析了Zygote的启动过程,解释了Zygote在Android启动过程中的决定作用。最后通过时序图展示了整个过程。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 本文介绍了如何使用jQuery和AJAX来实现动态更新两个div的方法。通过调用PHP文件并返回JSON字符串,可以将不同的文本分别插入到两个div中,从而实现页面的动态更新。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 如何在php文件中添加图片?
    本文详细解答了如何在php文件中添加图片的问题,包括插入图片的代码、使用PHPword在载入模板中插入图片的方法,以及使用gd库生成不同类型的图像文件的示例。同时还介绍了如何生成一个正方形文件的步骤。希望对大家有所帮助。 ... [详细]
author-avatar
白小蛮
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有