apisix 自定义插件
作者: ryan 发布于: 1970/1/1 更新于: 1970/1/1 字数: 0 字 阅读: 0 分钟
auth
身份认证在日常生活当中是非常常见的一项功能,大家平时基本都会接触到。
比如用支付宝消费时的人脸识别确认、公司上班下班时的指纹/面部打卡以及网站上进行账号密码登录操作等,其实都是身份认证的场景体现。

如上图,Jack 通过账号密码请求服务端应用,服务端应用中需要有一个专门用做身份认证的模块来处理这部分的逻辑。请求处理完毕子后,如果使用 JWT Token 认证方式,服务器会反馈一个 Token 去标识这个用户为 Jack。如果登录过程中账号密码输入错误,就会导致身份认证失败。
但是每个应用服务模块去开发一个单独的身份认证模块,用来支持身份认证的一套流程处理,当服务量多了之后,就会发现这些模块的开发工作量都是非常巨大且重复的。这个时候,我们可以通过把这部分的开发逻辑放置到 Apache APISIX 的网关层来实现统一,减少开发量。

如上图所示,用户或应用方直接去请求 Apache APISIX,然后 Apache APISIX 通过识别并认证通过后,会将鉴别的身份信息传递到上游应用服务,之后上游应用服务就可以从请求头中读到这部分信息,然后进行后续的逻辑处理。
Apache APISIX 作为一个 API 网关,目前已开启与各种插件功能的适配合作,插件库也比较丰富。目前已经可与大量身份认证相关的插件进行搭配处理,如下图所示。

基础认证插件比如 Key-Auth、Basic-Auth,他们是通过账号密码的方式进行认证。复杂一些的认证插件如 Hmac-Auth、JWT-Auth,如 Hmac-Auth 通过对请求信息做一些加密,生成一个签名,当 API 调用方将这个签名携带到 Apache APISIX,Apache APISIX 会以相同的算法计算签名,只有当签名方和应用调用方认证相同时才予以通过。其他则是一些通用认证协议和联合第三方组件进行合作的认证协议,例如 OpenID-Connect 身份认证机制,以及 LDAP 认证等。
Apache APISIX 还可以针对每一个 Consumer (即调用方应用)去做不同级别的插件配置。如下图所示,我们创建了两个消费者 Consumer A、Consumer B,我们将 Consumer A 应用到应用 1,则后续应用 1 的访问将会开启 Consumer A 的这部分插件,例如 IP 黑白名单,限制并发数量等。将 Consumer B 应用到应用 2 ,由于开启了 http-log 插件,则应用 2 的访问日志将会通过 HTTP 的方式发送到日志系统进行收集。

总体说来 APISIX 的认证系统功能非常强大,我们非常有必要掌握。
basic-auth
首先我们来了解下最简单的基本认证在 APISIX 中是如何使用的。basic-auth 是一个认证插件,它需要与 Consumer 一起配合才能工作。添加 Basic Auth 到一个 Service 或 Route,然后 Consumer 将其用户名和密码添加到请求头中以验证其请求。
首先我们需要在 APISIX Consumer 消费者中增加 basic auth 认证配置,为其指定用户名和密码,我们这里在 APISIX Ingress 中,可以通过 ApisixConsumer 资源对象进行配置,比如这里我们为前面的 nexus 实例应用添加一个基本认证,如下所示:
# nexus-basic-auth.yaml
apiVersion: apisix.apache.org/v2alpha1
kind: ApisixConsumer
metadata:
name: nexusBauth
spec:
authParameter:
basicAuth:
value:
username: admin
password: admin321
ApisixConsumer 资源对象中只需要配置 authParameter 认证参数即可,目前只支持 BasicAuth 与 KeyAuth 两种认证类型,在 basicAuth 下面可以通过 value 可直接去配置相关的 username 和 password,也可以直接使用 Secret 资源对象进行配置,比起明文配置会更安全一些。
然后在 ApisixRoute 中添加 authentication,将其开启并指定认证类型即可,就可以实现使用 Consumer 去完成相关配置认证,如下所示:
apiVersion: apisix.apache.org/v2beta2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- '/nexus*'
- '/static/*'
- '/service/*'
plugins:
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/nexus(/|$)(.*)', '/$2']
- name: redirect
enable: true
config:
regex_uri: ['^(/nexus)$', '$1/']
- name: redirect
enable: true
config:
http_to_https: true
backends:
- serviceName: nexus
servicePort: 8081
authentication: # 开启 basic auth 认证
enable: true
type: basicAuth
直接更新上面的资源即可开启 basic auth 认证了,在 Dashboard 上也可以看到创建了一个 Consumer:

然后我们可以进行如下的测试来进行验证:
# 缺少 Authorization header
$ curl -i http://ops.qikqiak.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 11 Jan 2022 07:44:49 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
WWW-Authenticate: Basic realm='.'
Server: APISIX/2.10.0
{"message":"Missing authorization in request"}
# 用户名不存在
$ curl -i -ubar:bar http://ops.qikqiak.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 11 Jan 2022 07:45:07 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.10.0
{"message":"Invalid user key in authorization"}
# 成功请求
$ curl -uadmin:admin321 http://ops.qikqiak.com/nexus/
<html>
<head><title>301 Moved Permanently</title></head>
<body>
<center><h1>301 Moved Permanently</h1></center>
<hr><center>openresty</center>
</body>
</html>
consumer-restriction
不过这里大家可能会有一个疑问,在 Route 上面我们并没有去指定具体的一个 Consumer,然后就可以进行 Basic Auth 认证了,那如果我们有多个 Consumer 都定义了 Basic Auth 岂不是都会生效的?确实是这样的,这就是 APISIX 的实现方式,所有的 Consumer 对启用对应插件的 Route 都会生效的,如果我们只想 Consumer A 应用在 Route A、Consumer B 应用在 Route B 上面的话呢?要实现这个功能就需要用到另外一个插件:consumer-restriction。
consumer-restriction 插件可以根据选择的不同对象做相应的访问限制,该插件可配置的属性如下表所示:

其中的 type 字段是个枚举类型,它可以是 consumer_name 或 service_id,分别代表以下含义:
- consumer_name:把 consumer 的 username 列入白名单或黑名单(支持单个或多个 consumer)来限制对服务或路由的访问。
- service_id:把 service 的 id 列入白名单或黑名单(支持一个或多个 service)来限制 service 的访问,需要结合授权插件一起使用。
比如现在我们有两个 Consumer:jack1 和 jack2,这两个 Consumer 都配置了 Basic Auth 认证,配置如下所示:
Conumer jack1 的认证配置:
$ curl http://192.168.31.46/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"username": "jack1",
"plugins": {
"basic-auth": {
"username":"jack2019",
"password": "123456"
}
}
}'
Conumer jack2 的认证配置:
$ curl http://192.168.31.46/apisix/admin/consumers -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '
{
"username": "jack2",
"plugins": {
"basic-auth": {
"username":"jack2020",
"password": "123456"
}
}
}'
现在我们只想给一个 Route 路由对象启用 jack1 这个 Consumer 的认证配置,则除了启用 basic-auth 插件之外,还需要在 consumer-restriction 插件中配置一个 whitelist 白名单(当然配置黑名单也是可以的),如下所示:
$ curl http://192.168.31.46/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
"uri": "/index.html",
"upstream": {
"type": "roundrobin",
"nodes": {
"127.0.0.1:1980": 1
}
},
"plugins": {
"basic-auth": {},
"consumer-restriction": {
"whitelist": [
"jack1"
]
}
}
}'
然后我们使用 jack1 去访问我们的路由进行验证:
$ curl -u jack2019:123456 http://127.0.0.1:9080/index.html -i
HTTP/1.1 200 OK
...
正常使用 jack2 访问就会认证失败了:
$ curl -u jack2020:123456 http://127.0.0.1:9080/index.html -i
HTTP/1.1 403 Forbidden
...
{"message":"The consumer_name is forbidden."}
所以当你只想让一个 Route 对象关联指定的 Consumer 的时候,记得使用 consumer-restriction 插件。
jwt-auth
在平时的应用中可能使用 jwt 认证的场景是最多的,同样在 APISIX 中也有提供 jwt-auth 的插件,它同样需要与 Consumer 一起配合才能工作,我们只需要添加 JWT Auth 到一个 Service 或 Route,然后 Consumer 将其密钥添加到查询字符串参数、请求头或 cookie 中以验证其请求即可。
由于目前 ApisixConsumer 还不支持 jwt-auth 配置,所以需要我们去 APISIX 手动创建一个 Consumer,可以通过 APISIX 的 API 进行创建,当然也可以直接通过 Dashboard 页面操作。在 Dashboard 消费者页面点击创建消费者:

点击下一步进入插件配置页面,这里我们需要启用 jwt-auth 这个插件:

在插件配置页面配置 jwt-auth 相关属性,可参考插件文档 https://apisix.apache.org/zh/docs/apisix/plugins/jwt-auth/:

可配置的属性如下表所示:

然后提交即可创建完成 Consumer,然后我们只需要在需要的 Service 或者 Route 上开启 jwt-auth 即可,比如同样还是针对上面的 nexus 应用,我们只需要在 ApisixRoute 对象中启用一个 jwt-auth 插件即可:
apiVersion: apisix.apache.org/v2beta2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: root
match:
hosts:
- ops.qikqiak.com
paths:
- '/nexus*'
- '/static/*'
- '/service/*'
plugins:
- name: jwt-auth
enable: true
- name: redirect
enable: true
config:
http_to_https: true
- name: redirect
enable: true
config:
regex_uri: ['^(/nexus)$', '$1/']
- name: proxy-rewrite
enable: true
config:
regex_uri: ['^/nexus(/|$)(.*)', '/$2']
backends:
- serviceName: nexus
servicePort: 8081
需要注意的是 authentication 属性也不支持 jwt-auth,所以这里我们通过 plugins 进行启用,重新更新上面的对象后我们同样来测试验证下:
$ curl -i http://ops.qikqiak.com/nexus/
HTTP/1.1 401 Unauthorized
Date: Tue, 11 Jan 2022 08:54:30 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.10.0
{"message":"Missing JWT token in request"}
要正常访问我们的服务就需要先进行登录获取 jwt-auth 的 token,通过 APISIX 的 apisix/plugin/jwt/sign 可以获取:
$ curl -i http://192.168.31.46/apisix/plugin/jwt/sign\?key\=user-key
HTTP/1.1 200 OK
Date: Tue, 11 Jan 2022 09:01:29 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/2.10.0
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY0MTk3ODA4OX0.rdzMxM4QAKI444c3SC3u3ZqfW9rKnsqrdorLHCGqrQg
要注意上面我们在获取 token 的时候需要传递创建消费者的标识 key,因为可能有多个不同的 Consumer 消费者,然后我们将上面获得的 token 放入到 Header 头中进行访问:
$ curl -i http://ops.qikqiak.com/nexus/ -H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY0MTk3ODA4OX0.rdzMxM4QAKI444c3SC3u3ZqfW9rKnsqrdorLHCGqrQg'
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 8802
Connection: keep-alive
......
Expires: 0
Server: APISIX/2.10.0
<!DOCTYPE html>
<html lang="en">
......
可以看到可以正常访问。同样也可以放到请求参数中验证:
$ curl -i http://ops.qikqiak.com/nexus/?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY0MTk3ODA4OX0.rdzMxM4QAKI444c3SC3u3ZqfW9rKnsqrdorLHCGqrQg
HTTP/1.1 200 OK
......
此外还可以放到 cookie 中进行验证:
$ curl -i http://ops.qikqiak.com/nexus/ --cookie jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1c2VyLWtleSIsImV4cCI6MTY0MTk3ODA4OX0.rdzMxM4QAKI444c3SC3u3ZqfW9rKnsqrdorLHCGqrQg
HTTP/1.1 200 OK
......
自定义插件
除了 APISIX 官方内置的插件之外,我们也可以根据自己的需求去自定义插件,要自定义插件需要使用到 APISIX 提供的 Runner,目前已经支持 Java、Go 和 Python 语言的 Runner,这个 Runner 相当于是 APISIX 和自定义插件之间的桥梁,比如 apache-apisix-python-runner 这个项目通过 Python Runner 可以把 Python 直接应用到 APISIX 的插件开发中,整体架构如下所示:

左边是 APISIX 的工作流程,右边的 Plugin Runner 是各语言的插件运行器,当在 APISIX 中配置一个 Plugin Runner 时,APISIX 会启动一个子进程运行 Plugin Runner,该子进程与 APISIX 进程属于同一个用户,当我们重启或重新加载 APISIX 时,Plugin Runner 也将被重启。
如果你为一个给定的路由配置了 ext-plugin-* 插件,请求命中该路由时将触发 APISIX 通过 Unix Socket 向 Plugin Runner 发起 RPC 调用。调用分为两个阶段:
ext-plugin-pre-req
:在执行 APISIX 内置插件之前ext-plugin-post-req
:在执行 APISIX 内置插件之后
接下来我们就以 Python 为例来说明如何自定义插件,首先获取 apache-apisix-python-runner 项目:
$ git clone https://github.com/apache/apisix-python-plugin-runner.git
$ cd apisix-python-plugin-runner
$ git checkout 0.1.0 # 切换刀0.1.0版本
如果是开发模式,则我们可以直接使用下面的命令启动 Python Runner:
$ APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock python3 apisix/main.py start
启动后需要在 APISIX 配置文件中新增外部插件配置,如下所示:
$ vim /path/to/apisix/conf/config.yaml
apisix:
admin_key:
- name: "admin"
key: edd1c9f034335f136f87ad84b625c8f1
role: admin
ext-plugin:
path_for_test: /tmp/runner.sock
通过 ext-plugin.path_for_test 指定 Python Runner 的 unix socket 文件路径即可,如果是生产环境则可以通过 ext-plugin.cmd 来指定 Runner 的启动命令即可:
ext-plugin:
cmd: [ "python3", "/path/to/apisix-python-plugin-runner/apisix/main.py", "start" ]
我们这里的 APISIX 是运行 Kubernetes 集群中的,所以要在 APISIX 的 Pod 中去执行 Python Runner 的代码,我们自然需要将我们的 Python 代码放到 APISIX 的容器中去,然后安装自定义插件的相关依赖,直接在 APISIX 配置文件中添加上面的配置即可,所以我们这里基于 APISIX 的镜像来重新定制包含插件的镜像,在 apisix-python-plugin-runner 项目根目录下新增如下所示的 Dockerfile 文件:
FROM apache/apisix:2.10.0-alpine
ADD . /apisix-python-plugin-runner
RUN apk add --update python3 py3-pip && \
cd /apisix-python-plugin-runner && \
python3 -m pip install --upgrade pip && \
python3 -m pip install -r requirements.txt --ignore-installed && \
python3 setup.py install --force
基于上面 Dockerfile 构建一个新的镜像,推送到 Docker Hub:
$ docker build -t cnych/apisix:py3-plugin-2.10.0-alpine .
# 推送到DockerHub
$ docker push cnych/apisix:py3-plugin-2.10.0-alpine
接下来我们需要使用上面构建的镜像来安装 APISIX,我们这里使用的是 Helm Chart 进行安装的,所以需要通过 Values 文件进行覆盖,如下所示:
# ci/prod.yaml
apisix:
enabled: true
image:
repository: cnych/apisix
tag: py3-plugin-2.10.0-alpine
......
由于官方的 Helm Chart 没有提供对 ext-plugin 配置的支持,所以需要我们手动修改模板文件 templates/configmap.yaml,在 apisix 属性同级目录下面新增 ext-plugin 相关配置,如下所示:
{{- if .Values.extPlugins.enabled }}
ext-plugin:
{{- if .Values.extPlugins.pathForTest }}
path_for_test: {{ .Values.extPlugins.pathForTest }}
{{- end }}
{{- if .Values.extPlugins.cmds }}
cmd:
{{- range $cmd := .Values.extPlugins.cmds }}
- {{ $cmd }}
{{- end }}
{{- end }}
{{- end }}
nginx_config:
user: root # fix 执行 python runner没权限的问题
然后在定制的 Values 文件中添加如下所示的配置:
# ci/prod.yaml
extPlugins:
enabled: true
cmds: ['python3', '/apisix-python-plugin-runner/apisix/main.py', 'start']
接着就可以重新部署 APISIX 了:
$ helm upgrade --install apisix ./apisix -f ./apisix/ci/prod.yaml -n apisix
部署完成后在 APISIX 的 Pod 中可以看到会启动一个 Python Runner 的子进程:

在插件目录 /apisix-python-plugin-runner/apisix/plugins 中的 .py 文件都会被自动加载,上面示例中有两个插件 stop.py 和 rewrite.py,我们以 stop.py 为例进行说明,该插件代码如下所示:
from apisix.runner.plugin.base import Base
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
class Stop(Base):
def __init__(self):
super(Stop, self).__init__(self.__class__.__name__)
def filter(self, request: Request, response: Response):
# 可以通过 `self.config` 获取配置信息,如果插件配置为JSON将自动转换为字典结构
# print(self.config)
# 设置响应 Header 头
response.headers["X-Resp-A6-Runner"] = "Python"
# 设置响应body
response.body = "Hello, Python Runner of APISIX"
# 设置响应状态码
response.status_code = 201
# 通过调用 `self.stop()` 中断请求流程,此时将立即响应请求给客户端
# 如果未显示调用 `self.stop()` 或 显示调用 `self.rewrite()`将继续将请求
# 默认为 `self.rewrite()`
self.stop()
实现插件首先必须要继承 Base 类,必须实现 filter 函数,插件执行核心业务逻辑就是在 filter 函数中,该函数只包含 Request 和 Response 类对象作为参数,Request 对象参数可以获取请求信息,Response 对象参数可以设置响应信息 ,self.config 可以获取插件配置信息,在 filter 函数中调用 self.stop() 时将马上中断请求,响应数据,调用 self.rewrite() 时,将会继续请求。
然后我们在前面的 Nexus 应用中新增一个路由来测试我们上面的 stop 插件,在 ApisixRoute 对象中新增一个路由规则,如下所示:
apiVersion: apisix.apache.org/v2beta2
kind: ApisixRoute
metadata:
name: nexus
namespace: default
spec:
http:
- name: ext
match:
hosts:
- ops.qikqiak.com
paths:
- '/extPlugin'
plugins:
- name: ext-plugin-pre-req # 启用ext-plugin-pre-req插件
enable: true
config:
conf:
- name: 'stop' # 使用 stop 这个自定义插件
value: '{"body":"hello"}'
backends:
- serviceName: nexus
servicePort: 8081
直接创建上面的路由即可,核心配置是启用 ext-plugin-pre-req 插件(前提是在配置文件中已经启用该插件,在 Helm Chart 的 Values 中添加上),然后在 config 下面使用 conf 属性进行配置,conf 为数组格式可以同时设置多个插件,插件配置对象中 name 为插件名称,该名称需要与插件代码文件和对象名称一致,value 为插件配置,可以为 JSON 字符串。
创建后同样在 Dashboard 中也可以看到 APISIX 中的路由配置格式:

接着我们可以来访问 http://ops.qikqiak.com/extPlugin 这个路径来验证我们的自定义插件:
$ curl -i http://ops.qikqiak.com/extPlugin
HTTP/1.1 201 Created
Date: Thu, 13 Jan 2022 07:04:50 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
accept: */*
user-agent: curl/7.64.1
host: ops.qikqiak.com
X-Resp-A6-Runner: Python
Server: APISIX/2.10.0
Hello, Python Runner of APISIX
访问请求结果中有一个 X-Resp-A6-Runner: Python 头信息,返回的 body 数据为 Hello, Python Runner of APISIX,和我们在插件中的定义是符合的。到这里就完成了使用 Python 进行 APISIX 自定义插件,我们有任何的业务逻辑需要处理直接去定义一个对应的插件即可。