你是否也存在过这样的需求, 想要公开一个接口到网络上. 但是还得加点权限, 否则被人乱调用就不好了. 这个权限验证的过程, 最好越简单越好, 可能只是对比两个字符串相等就够了. 一般情况下我们遇到这种需要, 就是在函数实现或者添加一个全局的拦截器就够了. 但是还是需要自己来写那部分虽然简单但是很啰嗦的代码. 那么存不存在一种方式, 让我只管写我的代码就完了, 鉴权的事情交给其他人来做呢?
OpenAPI 一般情况下, 就是允许企业内部提供对外接口的项目. 你只管写你的接口, 然后, 在我这里注册一下, 我来负责你的调用权限判定, 如果他没有权限, 我就告诉他没有权限, 如果他存在权限, 我就转调一下你的接口, 然后把结果返回给他. 其实情景是相似的, 我们可以把这段需求抽象, 然后做一个配置文件版的开放接口.
想做这件事情, 其实 Golang 是一个非常不错的选择, 首先, Golang 对于这种转调的操作非常友好, 甚至于, Golang 语言本身就提供了一个反向代理的实现, 我们可以直接使用 Golang 的原始框架就完全够用.
在简单分析一下我们的需求, 其实很简单, 监听的某一段 Path 之后, 先判断有没有权限, 没有权限, 直接回写结果, 有权限交给反向代理来实现, 轻松方便. 既然是这样, 我们需要定义一下, 路径转发的规则.
比如说我们尝试给这个接口添加一个, 当然这只是其中一个接口, 我们应该要支持好多个接口
http://api.qingyunke.com/api.php?key=free&appid=0&msg=hello world.
在他进入到我们的系统中的时候看上去可能是这样的.
http://localhost:5000/jiqiren/API.PHP?key=free&appid=0&msg=hello world.
所以, 在我们的配置里边也应该是支持多个节点配置的.
- {
- "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
来源: https://www.cnblogs.com/anxin1225/p/12842365.html