一、背景
Apisix中提供了很多开箱即用的插件以供用户来完成自己业务需要的诉求。例如:limit-count,limit-conn,limit-req限流相关的插件,proxy-rewrite,response-rewrite等重写url以及response的插件,以及认证相关的插件basic-auth,jwt-auth等。
除了自带的插件之外apisix也为用户提供使用其他语言开发自定义插件,从而来实现符合自己业务特定的逻辑,apisix中有两种方式可以来实现自定义插件:内部插件和外部插件,内部插件的执行效率要更高一些。
二、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"
三、总结
到这里我们已经实现
- 通过go-plugin-runner来开发自定义插件和apisix进行交互。
- 在go runner中模拟对于用户信息的提取,并且将提取的用户信息写入到request header模拟认证。
- 我们也了解到自定义插件和apisix的交互原理和方式,以及对于自定义插件调用时间的配置,确保插件可以按照我们的希望在处理请求前,处理请求后或者处理response后执行插件逻辑。
相关链接
apisix.apache.org/zh/docs/api...
apisix.apache.org/docs/apisix...