基于APISIX的ABAC模型鉴权插件开发
APISIX是一个非常优秀的开源全流量网关,内置了很多插件。但如果要扩展实现自定义的插件,网上可参考的文章非常少。本文将以简化版ABAC模型的鉴权需求为例介绍APISIX插件的开发。
前置知识
如果不了解APISIX请先移步官网: https://apisix.apache.org/
如果不会lua请先学习基础的语法:https://www.lua.org/start.html 或是 https://learnxinyminutes.com/docs/lua/
也许你还需要知道一点点perl语法:https://learnxinyminutes.com/docs/perl/
最后还需要先学习下APISIX官方的插件开发教程,核心的内容已经讲得明明白白了:https://apisix.apache.org/zh/docs/apisix/plugin-develop
插件说明
不同于主流的RBAC( https://en.wikipedia.org/wiki/Role-based_access_control )权限模型,ABAC( https://en.wikipedia.org/wiki/Attribute-based_access_control )具备各灵活的权限配置策略,该模型的介绍也可参见笔者过往的文章( http://www.idealworld.group/2021/01/18/iam-more-elegant-model-and-implementation/ )。
目前笔者在构建的 BIOS( https://github.com/ideal-world/bios ,欢迎star)项目需要使用到全流量、高性能网关,选型为APISIX,在此基础上需要扩展实现简化版ABAC模型的鉴权插件。
环境准备
Tip | 如果觉得麻烦可尝试使用笔者写的一键安装脚本:https://github.com/ideal-world/bios/blob/main/gateway/init-ubuntu.sh |
Tip | 下文基于ubuntu(含wsl2)介绍,如操作系统有差异请自行修改。 |
添加Openresty源
wget -qO - https://openresty.org/package/pubkey.gpg | sudo apt-key add -
sudo apt-get update
sudo apt-get -y install software-properties-common
sudo apt-get update安装Lua及各类开发类库/工具
# 注意lua必须为dev版本
sudo apt-get -y install git curl liblua5.1-0-dev openresty openresty-openssl111-dev cpanminus安装并启动ETCD
wget https://github.com/etcd-io/etcd/releases/download/v3.4.13/etcd-v3.4.13-linux-amd64.tar.gz
tar -xvf etcd-v3.4.13-linux-amd64.tar.gz
rm etcd-v3.4.13-linux-amd64.tar.gz
mv etcd-v3.4.13-linux-amd64 etcd
cd etcd
sudo cp -a etcd etcdctl /usr/bin/
cd ..
nohup etcd /dev/null 2>&1 &安装LuaRocks
# 官网的脚本(https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh)在笔者的环境并不能正常安装(E.g. OPENRESTY_PREFIX="/usr/local/openresty" 位置不正确,缺少sudo等)
curl https://raw.githubusercontent.com/ideal-world/bios/main/gateway/utils/linux-install-luarocks.sh -sL | bash -下载APISIX
# 可修改成需要的版本
wget https://mirrors.bfsu.edu.cn/apache/apisix/2.8/apache-apisix-2.8-src.tgz
tar -cvf apisix.tar apisix
tar -xf apache-apisix-2.8-src.tgz -C apisix
tar -xf apisix.tar
rm apisix.tar
rm apache-apisix-2.8-src.tgz安装依赖
cd apisix
make deps下载test-nginx并安装依赖
git clone --depth=1 https://github.com/iresty/test-nginx.git
rm -rf test-nginx/.git
sudo cpanm --notest Test::Nginx IPC::Run > build.log 2>&1 || (cat build.log && exit 1)
export PERL5LIB=.:$PERL5LIB
插件开发
如果一切顺利,接下来我们就可以开发插件了。
APISIX的插件位于 apisix/plugins/
下,新建一个 auth-bios
的目录及 auth-bios.lua
文件,前者存放的是核心逻辑,后者为插件入口。
新插件还需要添加到 conf/config-default.yaml
的 plugins:
下:
plugins:
...
- serverless-post-function
- ext-plugin-post-req
- auth-bios
Tip | 如果是流式插件需要添加到 stream_plugins 下。 |
auth-bios.lua
作为插件定义文件需要注意几个版式化的约定:
-- 引入一堆依赖
local core = require("apisix.core")
local m_redis = require("apisix.plugins.auth-bios.redis")
...
-- 标识插件名称
local plugin_name = "auth-bios"
-- 定义插件配置参数
local schema = {
type = "object",
properties = {
-- 一堆配置参数
redis_host = { type = "string" },
redis_port = { type = "integer", default = 6379 },
redis_password = { type = "string" },
...
},
-- 必选参数列表
required = { "redis_host" }
}
-- 定义插件信息
local _M = {
version = 0.1,
-- 优先级,官方推荐1~99,过高的优先级先抢夺一些基础插件的优先执行权
priority = 5001,
-- 由于需要与consumer配合,故设置成auth,详见:https://apisix.apache.org/zh/docs/apisix/architecture-design/consumer
type = 'auth',
name = plugin_name,
schema = schema,
}
-- check_schema方法在安装插件时调用,用于检查插件是否合法
function _M.check_schema(conf)
-- 此方法会检查插件配置是否合法(E.g. 是否缺少必选参考)
local check_ok, check_err = core.schema.check(schema, conf)
if not check_ok then
core.log.error("Configuration parameter error")
return false, check_err
end
-- 此方法亦可以用于全局初始化,E.g. 建立redis连接
local _, redis_err = m_redis.init(conf.redis_host, conf.redis_port, conf.redis_database, conf.redis_timeout, conf.redis_password)
if redis_err then
core.log.error("Connect redis error", redis_err)
return false, redis_err
end
...
-- 如果检查通过返回true
return true
end
-- rewrite方法在每次请求命中插件时调用,也是插件处理的核心逻辑
function _M.rewrite(conf, ctx)
-- 这里调用了 auth-bios 目录下的各个具体处理逻辑
-- 先根据请求入参获取请求身份(谁发起的请求,可以是人、租户、应用、设备等)及请求的资源(基于uri)
local ident_code, ident_message = m_ident.ident(conf, ctx)
if ident_code ~= 200 then
return ident_code, ident_message
end
-- 然后判断该身份有没有访问对应资源的权限
local auth_code, auth_message = m_auth.auth(ctx.ident_info)
if auth_code ~= 200 then
return auth_code, auth_message
end
-- 如果有权限则组装请求信息传向目标服务
core.request.set_header(ctx, conf.ident_flag, ngx_encode_base64(json.decode({
res_action = ctx.ident_info.resource_action,
res_uri = ctx.ident_info.resource_uri,
app_id = ctx.ident_info.app_id,
tenant_id = ctx.ident_info.tenant_id,
account_id = ctx.ident_info.account_id,
token = ctx.ident_info.token,
token_kind = ctx.ident_info.token_kind,
ak = ctx.ident_info.ak,
roles = ctx.ident_info.roles,
groups = ctx.ident_info.groups,
})))
end
-- 返回插件信息
return _M
Tip | 完整文件见:https://github.com/ideal-world/bios/blob/main/gateway/apisix/apisix/plugins/auth-bios.lua |
auth-bios
目录包含核心处理逻辑及一些辅助方法,与APISIX插件开发的规约关系不大,本文不展开介绍,有兴趣的读者可到对应的github工程下查看。
插件单元测试
Tip | APISIX的测试基于 test-nginx ,如果是熟悉Java、Go、Node、Rust等项目的开发,那么对这种测试方案一定很难适应。笔者觉得并不优雅且使用复杂,当然也许是没有领会到其精髓。 |
测试文件约定写在 t/plugin/
下,我们创建同名的 auth-bios
目录,这里举一个简单的例子。
use t::APISIX 'no_plan';
no_long_string();
no_root_location();
no_shuffle();
run_tests;
__DATA__
=== TEST 1: test redis
--- config
location /t {
content_by_lua_block {
local m_utils = require("apisix.plugins.auth-bios.utils")
local m_redis = require("apisix.plugins.auth-bios.redis")
local m_redis1 = require("apisix.plugins.auth-bios.redis")
m_redis.init("127.0.0.1", 6379, 1, 1000, "123456")
m_redis1.set("test", "测试1")
ngx.say(m_redis.get("test"))
m_redis.hset("test_hash","api://xx/?1","{\"a\":\"xx1\"}")
m_redis.hset("test_hash","api://xx/?2","{\"a\":\"xx2\"}")
m_redis.hset("test_hash","api://xx/?3","{\"a\":\"xx3\"}")
m_redis.hset("test_hash","api://xx/?4","{\"a\":\"xx4\"}")
m_redis.hset("test_hash","api://xx/?5","{\"a\":\"xx5\"}")
m_redis.hscan("test_hash","*",2, function(k,v) ngx.say(k..":"..v) end)
m_redis.hscan("not_exist","*",2, function(k,v) ngx.say(k..":"..v) end)
}
}
--- request
GET /t
--- response_body
测试1
api://xx/?1:{"a":"xx1"}
api://xx/?2:{"a":"xx2"}
api://xx/?3:{"a":"xx3"}
api://xx/?4:{"a":"xx4"}
api://xx/?5:{"a":"xx5"}
--- no_error_log
[error]
测试逻辑写在 __DATA__
下,通过 ngx.say
返回数据,然后在 response_body
中输出并比对是否匹配。
单元测试的方式如下:
export PERL5LIB=.:$PERL5LIB
TEST_NGINX_BINARY=/usr/bin/openresty prove -Itest-nginx/lib -r t/plugin/auth-bios/redis.t
插件集成测试
插件的集成测试需要先编译并启动APISIX,常用命令如下:
# initialize NGINX config file and etcd
make init
# start Apache APISIX server
make run
# stop Apache APISIX server gracefully
make quit
# stop Apache APISIX server immediately
make stop
然后就可以发起测试,示例如下:
# 添加upstream
curl "http://127.0.0.1:9080/apisix/admin/upstreams/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}'
# 添加route
curl "http://127.0.0.1:9080/apisix/admin/routes/1" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
"uri": "/cache/**",
"upstream_id": "1"
}'
# 测试成功
curl -i -X GET "http://127.0.0.1:9080/cache/1"
# 添加全局插件(需要开启Redis)
curl "http://127.0.0.1:9080/apisix/admin/global_rules/1" -H "Content-Type: application/json" -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -X PUT -d '
{
"plugins": {
"auth-bios": {
"redis_host": "127.0.0.1",
"redis_password": "123456",
"redis_database": 1
}
}
}'
# 获取全局插件列表
curl http://127.0.0.1:9080/apisix/admin/global_rules -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1'
# 测试失败,缺少 BIOS-Host
curl -i -X GET "http://127.0.0.1:9080/cache/1"
# 测试成功
curl -i -X GET "http://127.0.0.1:9080/cache/1" -H 'BIOS-Host: app1.tenant1'
# 测试失败,Token错误
curl -i -X GET "http://127.0.0.1:9080/cache/1" -H 'BIOS-Host: app1.tenant1' -H 'BIOS-Token: token001'
# 测试成功(先执行单元测试)
curl -i -X GET "http://127.0.0.1:9080/cache/1" -H 'BIOS-Host: app1.tenant1' -H 'BIOS-Token: tokenxxx'
小结
APISIX的插件开发并不算复杂,但需要学习lua、perl及相关的知识,还是有一定的门槛,所以APISIX也推出了插件扩展机制,支持使用自己熟悉的语言开发插件。但从运行机制看毕竟有本地RPC交互,在性能上可能会有些损失。说到性能,插件质量的优劣对APISIX的影响很大,本文示例的插件也有许多可优化的地方,比如更少的序列化逻辑、使用pb代替json、计算复杂度高的逻辑使用C/Rust封装等。