Apisix(四)通过jwt-token + 自定义插件实现身份认证

一、背景

Apisix中提供了很多开箱即用的插件以供用户来完成自己业务需要的诉求。例如:limit-count,limit-conn,limit-req限流相关的插件,proxy-rewrite,response-rewrite等重写url以及response的插件,以及认证相关的插件basic-auth,jwt-auth等。

除了自带的插件之外apisix也为用户提供使用其他语言开发自定义插件,从而来实现符合自己业务特定的逻辑,apisix中有两种方式可以来实现自定义插件:内部插件和外部插件,内部插件的执行效率要更高一些。

  • 内部插件:通过自定义lua脚本来实现
  • 外部插件:通过apisix开发的插件接口,通过go,java,js等语言实现

二、Jwt-auth插件 + 自定义插件模拟Ops身份认证

下面着重看使用Jwt-auth插件 + 自定义插件来完成用户认证功能的模拟。

模拟功能中很多实现细节先忽略,后期开发中在进行补全。

jwt-auth插件介绍

未开启jwt-auth插件访问请求,会直接路由到对应的后端服务

yaml 复制代码
➜  curl http://192.168.101.23:30290/ams/amcenter
{"timestamp":"2024-04-17T09:53:12.084+08:00","status":401,"path":"/ams/amcenter","method":"GET","ms":13,"error":"login.required","message":"用户未登录"}%

通过apisix dashboard启用jwt-auth插件,默认启用会开通全局作用域,也可以根据自己需要将作用域调整到特定接口或者特定的consumer。

启用jwt-auth插件后,再次访问amcenter接口发现提示缺少jwt-token

yaml 复制代码
➜  curl http://192.168.101.23:30290/ams/amcenter
{"message":"Missing JWT token in request"}
➜ 

这里随便使用一个token来进行验证尝试,可以看到错误信息变了不再是缺少token,而是token不合理。

yaml 复制代码
➜  curl -i http://192.168.101.23:30290/ams/amcenter -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTU2NDA1MDgxMX0.Us8zh_4VjJXF-TmR5f8cif8mBU7SuefPlpxhH0jbPVI'
HTTP/1.1 401 Unauthorized
Date: Wed, 17 Apr 2024 01:58:40 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.8.0

{"message":"failed to verify jwt"}

通过apisix提供的sign接口生成token,使用sign接口前需要先通过public-api插件暴露接口。

yaml 复制代码
➜  ~ curl http://127.0.0.1:9180/apisix/admin/routes/jas \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "uri": "/apisix/plugin/jwt/sign",
    "plugins": {
        "public-api": {}
    }
}'
{"key":"/apisix/routes/jas","value":{"plugins":{"public-api":{}},"uri":"/apisix/plugin/jwt/sign","update_time":1713320808,"priority":0,"status":1,"id":"jas","create_time":1713320808}}

创建一个普通的token没有任何payload信息,然后使用新创建的token来访问amcenter接口可以看到通过了jwt-token校验(jwt-auth插件默认使用Authorization作为token对应的http request header,也可以通过配置来调整)。

yaml 复制代码
~ curl http://192.168.101.23:30290/apisix/plugin/jwt/sign?key=user-key -i
HTTP/1.1 200 OK
Date: Wed, 17 Apr 2024 02:44:55 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.8.0

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MTM0MDgyOTUsImtleSI6InVzZXIta2V5In0.qxXC4-zCGsOz7UHub8unbh6viHiSW3i2l24XJ1V2Cck%

 ~ curl -i http://192.168.101.23:30290/ams/amcenter -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MTM0MDgyOTUsImtleSI6InVzZXIta2V5In0.qxXC4-zCGsOz7UHub8unbh6viHiSW3i2l24XJ1V2Cck'
HTTP/1.1 401
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: client-uuid=95d5d788-a5d0-438f-9dcb-55d9306ad7bc; Max-Age=315360000; Expires=Sat, 15-Apr-2034 02:45:41 GMT; Path=/; HttpOnly
Date: Wed, 17 Apr 2024 02:45:41 GMT
Server: APISIX/3.8.0

{"timestamp":"2024-04-17T10:45:41.758+08:00","status":401,"path":"/ams/amcenter","method":"GET","ms":3,"error":"login.required","message":"用户未登录"}%

外部插件介绍


这里通过go-runner来了解apisix中通过go语言开发外部插件。

Apisix内部通过以子进程的方式运行plugin runner,当重启或者重新加载apisix的时候plugin runner也会被重启。我们使用go,java等语言开发的插件不会直接跑在apisix的lua环境中, 而是通过plugin runner来运行我们自定义开发的插件,然后apisix通过和plugin runner通信的方式,间接实现对于自定义插件的调用。

Plugin runner和apisix通过rpc的方式进行通信,对于plugin runner来说主要处理两种类型的rpc请求

  • PrepareConf: 当用户在apisix调整了对于自定义插件的配置,通过apisix通过prepareconf rpc请求将改动同步到plugin runner,以保证plugin runner可以使用到最新的插件配置。
  • HTTPReqCall: 自定义插件中可能会对reponse进行处理,当对response进行处理之后,plugin runner会通过httpreqcall告知apisix具体的改动。

简单来说就是我们开发的插件运行在plugin runner中,plugin runner作为一个子进程运行在apisix中,然后plugin runner可以看作是一个中间介质来完成apisix和自定义插件之间的功能和逻辑交互。

go runner自定义插件介绍

使用官方提供的apisix-go-plugin-runner项目进行发自定义插件开发,项目克隆下来之后我们主要关注cmd/runner/plugins目录下的say.go文件,这个是一个示例插件项目,里面主要包含了两个方法:

  • ParseConf:用来解析传递给插件的参数
  • RequestFilter:处理http请求

plugin接口展示

go plugin runner中提供了plugin接口来实现用户自己逻辑,go runner对于plugin有一个默认实现,在实际开发中根据自身需要来对DefaultPlugin进行重写。

golang 复制代码
package plugin

import (
   "net/http"
   "github.com/apache/apisix-go-plugin-runner/internal/plugin"
   pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
)

type  Plugin  interface {
 Name() string
 ParseConf(in []byte) (conf interface{}, err error)
 RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request)
 ResponseFilter(conf interface{}, w pkgHTTP.Response)
}

func  RegisterPlugin(p Plugin) error {
   return plugin.RegisterPlugin(p.Name(), p.ParseConf, p.RequestFilter, p.ResponseFilter)
}

type DefaultPlugin struct{}
func (*DefaultPlugin) RequestFilter(interface{}, http.ResponseWriter, pkgHTTP.Request) {}
func (*DefaultPlugin) ResponseFilter(interface{}, pkgHTTP.Response)                    {}

官方提供的示例是直接可以使用的,不需要进行任何代码改动直接编译,将编译之后的go-runner包配置到apisix中即可。

say.go插件代码展示

say.go代码展示主要实现将插件配置参数信息解析之后,再写回到requst的response中返回给用户。

golang 复制代码
package plugins

import (
   "encoding/json"
   "github.com/apache/apisix-go-plugin-runner/pkg/sts"
   "net/http"

   pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
   "github.com/apache/apisix-go-plugin-runner/pkg/log"
   "github.com/apache/apisix-go-plugin-runner/pkg/plugin"
)

func init() {
   err := plugin.RegisterPlugin(&Say{})
   if err != nil {
      log.Fatalf("failed to register plugin say: %s", err)
   }
}

type Say struct {
plugin.DefaultPlugin
}

type SayConf struct {
   Body string `json:"body"`
}

func (p *Say) Name() string {
   return "say"
}

func (p *Say) ParseConf(in []byte) (interface{}, error) {
   conf := SayConf{}
   err := json.Unmarshal(in, &conf)
   return conf, err
}

func (p *Say) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) {

   body := conf.(SayConf).Body
   if  len(body) == 0 {
      return
}
    _ , err  :=  w.Write( [] byte(body))
 if  err  !=  nil {
 log.Errorf("failed  to write:  %s" , err)
}
}

在项目目录下运行make build直接构建项目,将构建好的go-runner二进制文件配置到apisix的ext-plugin配置中。

shell 复制代码
make build

apisix配置自定义插件

具体配置如下在apisix的config.yaml中添加ext-plugin的配置,指向我们的插件二进制包。

这里为了简化代码省略了其他配置

yaml 复制代码
apisix:
  enable_control: true
  control:
    ip: 0.0.0.0
    port: 9092
deployment:
  role: traditional
  role_traditional:
    config_provider: etcd
  admin:
    admin_key_required: false
    allow_admin:
      - 0.0.0.0/0
ext-plugin:
  cmd: [/usr/local/apisix/conf/go-runner, run]

配置好后启动apisix,在apisix启动日志可以看到apisix中加载的插件。

yaml 复制代码
2024/04/17 06:51:33 [warn] 59#59: *114 [lua] init.lua:961: 2024-04-17T06:51:33.490Z        INFO        plugin/plugin.go:73        register plugin fault-injection
2024-04-17T06:51:33.491Z        INFO        plugin/plugin.go:73        register plugin limit-req
2024-04-17T06:51:33.491Z        INFO        plugin/plugin.go:73        register plugin request-body-rewrite
2024-04-17T06:51:33.491Z        INFO        plugin/plugin.go:73        register plugin response-rewrite
2024-04-17T06:51:33.491Z        INFO        plugin/plugin.go:73        register plugin say
2024-04-17T06:51:33.491Z        WARN        server/server.go:192        conf cache ttl is 1h12m0s
2024-04-17T06:51:33.491Z        WARN        server/server.go:200        listening to /usr/local/apisix/conf/apisix-1.sock

验证say.go插件

插件正常加载之后使用插件和使用自带插件没有太大差别,根据插件的配置添加对应的参数,唯一需要注意的是对于通过runner开发的插件,需要配置插件的执行阶段,也就是下面代码中配置的'ext-plugin-pre-req'属性。

yaml 复制代码
curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/get",
  "plugins": {
    "ext-plugin-pre-req": {
      "conf": [
        { "name": "say", "value":"{"body":"hello"}"}
      ]
    }
  },
  "upstream": {
        "type": "roundrobin",
        "nodes": {
            "127.0.0.1:1980": 1
        }
    }
}
'

ext-plugin-*属性主要包含3个值:

  • ext-plugin-pre-req:在处理http request期间执行,在大多数APISIX内置插件(Lua语言插件)之前
  • ext-plugin-post-req:在处理http request期间执行,在大多数APISIX内置插件(Lua语言插件)之后
  • ext-plugin-post-resp:在处理http response期间执行,在大多数APISIX内置插件(Lua语言插件)之后

接下来访问/get url来验证自定义插件say,可以看到会返回我们在代码中对于response重写的hello字符串被返回。

yaml 复制代码
curl http://127.0.0.1:9080/ams/amcenter
hello
[root@sps-drone-jenkins apisix]#

go runner插件模拟认证

say.go添加模拟认证代码

通过say.go插件了解到自定义插件的工作情况,接下来我们在say.go的基础上做一些简单改动来模拟ops的身份认证。调整say.go实现Request Filter方法对于验证通过的请求添加ops认证的header头信息, 并且为了让请求可以继续正常处理,我们注释掉代码中对于reponse的writer操作

goland 复制代码
func (p *Say) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) {

   body := conf.(SayConf).Body
   if  len(body) == 0 {
      return
}

   amsUser := sps.GetUserInfo()  //模拟通过jwt-token获取accountid和userid,放到response header中
 r.Header().Set("User-Id", amsUser.UserId)
   r.Header().Set("Account-Id", amsUser.AccountId)

   log.Infof("request header", r.Header())

   w.Header().Add("X-Resp-A6-Runner", "Go")
   w.Header().Add("X-User-Id-Resp", amsUser.UserId)
   w.Header().Add("X-Account-Id-Resp", amsUser.AccountId)
}

这里直接返回用户信息,模拟真实代码中查询用户信息

golang 复制代码
package sps

type AmsUser struct {
   UserId    string `json:"user_id"`
   AccountId string `json:"account_id"`
}

func  GetUserInfo() AmsUser {
   return AmsUser{"ba7ee581842a4a378f276ca605cd6a68", "100890"}
}

添加amcenter路由执行cmp服务

配置自定义插件say然后将ams/amcenter接口路由到后端服务,在say插件中,通过模拟解析jwttoken添加userId,accountid到header中,完成认证

yaml 复制代码
curl http://127.0.0.1:9180/apisix/admin/routes/2 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/ams/amcenter",
  "plugins": {
    "ext-plugin-pre-req": {
      "conf": [
        { "name": "say", "value":"{"body":"hello"}"}
      ]
    }
  },  "upstream": {
        "type": "roundrobin",
        "nodes": {
            "192.168.101.23:31102": 1
        }
    }
}'

访问接口curl -v http://127.0.0.1:9080/ams/amcenter查看效果

yaml 复制代码
[root@sps-drone-jenkins ~]# curl -v http://127.0.0.1:9080/ams/amcenter
About to connect() to 127.0.0.1 port 9080 (#0)
Trying 127.0.0.1...
Connected to 127.0.0.1 (127.0.0.1) port 9080 (#0)
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Set-Cookie: client-uuid=cdf2baf0-e752-4d1b-a21e-a9b1f6426497; Max-Age=315360000; Expires=Mon, 10-Apr-2034 10:11:02 GMT; Path=/; HttpOnly
< Date: Fri, 12 Apr 2024 10:11:02 GMT
< Server: APISIX/3.8.0
<
{"timestamp":"2024-04-12T18:11:02.71+08:00","status":200,"data":{"id":"ba7d68","name":"troy","accountId":100980,"email":"troy@qq.com","passwordReset":false,"passwordUpdateAt":"2023-09-11T09:51:14.000+00:00","description":"用户","createdAt":"2018-07-10T20:19:18.000+00:00","createdBy":"ba7ee","accountName":"xx司","companyInfo":{}},"path":"/ams/amcenter","method":"GET","ms":50,"message":"个人中心成功"}[root@sps-drone-jenkins ~]#

apisix控制台say插件日志

yaml 复制代码
2024/04/12 10:11:02 [warn] 59#59: *181 [lua] init.lua:961: 2024-04-12T10:11:02.619Z INFO  server/server.go:115  Client connected (unix)
2024-04-12T10:11:02.619Z  INFO  server/server.go:115  Client connected (unix)
, context: ngx.timer
2024/04/12 10:11:02 [warn] 59#59: *181 [lua] init.lua:961: 2024-04-12T10:11:02.620Z INFO  server/server.go:131  receive rpc type: 2 data length: 228
2024-04-12T10:11:02.620Z  INFO  plugin/plugin.go:120  run plugin say
, context: ngx.timer
2024/04/12 10:11:02 [warn] 59#59: *181 [lua] init.lua:961: 2024-04-12T10:11:02.620Z INFO  plugins/say.go:70 request header%!(EXTRA ***http.Header=&{map[Account-Id:[100089] User-Id:[ba7a68]] map[Accept:[***/*] Host:[127.0.0.1:9080] User-Agent:[curl/7.29.0]] map[]})
, context: ngx.timer
192.168.80.1 - - [12/Apr/2024:10:11:02 +0000] 127.0.0.1:9080 "GET /ams/amcenter HTTP/1.1" 200 2406 0.116 "-" "curl/7.29.0" 192.168.101.23:31102 200 0.112 "http://127.0.0.1:9080"

三、总结

到这里我们已经实现

  1. 通过go-plugin-runner来开发自定义插件和apisix进行交互。
  2. 在go runner中模拟对于用户信息的提取,并且将提取的用户信息写入到request header模拟认证。
  3. 我们也了解到自定义插件和apisix的交互原理和方式,以及对于自定义插件调用时间的配置,确保插件可以按照我们的希望在处理请求前,处理请求后或者处理response后执行插件逻辑。

相关链接

github.com/apache/apis...

apisix.apache.org/zh/docs/api...

apisix.apache.org/docs/apisix...

apisix.apache.org/zh/docs/api...

apisix.apache.org/docs/apisix...

相关推荐
爱吃涮毛肚的肥肥(暂时吃不了版)33 分钟前
Linux高阶——1103—修改屏蔽字&&信号到达及处理流程&&时序竞态问题
linux·运维·服务器·开发语言·c++·后端
生活不只*眼前的苟且37 分钟前
实体(Entity)详解
后端·架构
尘浮生37 分钟前
Java项目实战II基于Java+Spring Boot+MySQL的高校办公室行政事务管理系统(源码+数据库+文档)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
重生成为码农‍1 小时前
Spring-Day4
java·后端·spring
睎zyl2 小时前
Scala的访问权限。
开发语言·后端·scala
飞升不如收破烂~2 小时前
Spring底层原理
java·后端·spring
公众号-架构师汤师爷3 小时前
一文搞懂4种用户权限模型
java·后端·saas·架构设计
monkey_meng3 小时前
【rust中的闭包】
开发语言·后端·rust·边缘计算
陈序缘4 小时前
Rust 力扣 - 1493. 删掉一个元素以后全为 1 的最长子数组
开发语言·后端·算法·leetcode·职场和发展·rust