基于APISIX的ABAC模型鑒權插件開發(fā)
APISIX是一個非常優(yōu)秀的開源全流量網(wǎng)關,內置了很多插件。但如果要擴展實現(xiàn)自定義的插件,網(wǎng)上可參考的文章非常少。本文將以簡化版ABAC模型的鑒權需求為例介紹APISIX插件的開發(fā)。
前置知識
如果不了解APISIX請先移步官網(wǎng):?https://apisix.apache.org/
如果不會lua請先學習基礎的語法:https://www.lua.org/start.html 或是 https://learnxinyminutes.com/docs/lua/
也許你還需要知道一點點perl語法:https://learnxinyminutes.com/docs/perl/
最后還需要先學習下APISIX官方的插件開發(fā)教程,核心的內容已經(jīng)講得明明白白了: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)項目需要使用到全流量、高性能網(wǎng)關,選型為APISIX,在此基礎上需要擴展實現(xiàn)簡化版ABAC模型的鑒權插件。
環(huán)境準備
Tip | 如果覺得麻煩可嘗試使用筆者寫的一鍵安裝腳本:https://github.com/ideal-world/bios/blob/main/gateway/init-ubuntu.sh |
Tip | 下文基于ubuntu(含wsl2)介紹,如操作系統(tǒng)有差異請自行修改。 |
添加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及各類開發(fā)類庫/工具
# 注意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
# 官網(wǎng)的腳本(https://raw.githubusercontent.com/apache/apisix/master/utils/linux-install-luarocks.sh)在筆者的環(huán)境并不能正常安裝(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
插件開發(fā)
如果一切順利,接下來我們就可以開發(fā)插件了。
APISIX的插件位于?apisix/plugins/?下,新建一個?auth-bios?的目錄及?auth-bios.lua?文件,前者存放的是核心邏輯,后者為插件入口。
新插件還需要添加到?conf/config-default.yaml?的?plugins:?下:
plugins:
...
- serverless-post-function
- ext-plugin-post-req
- auth-biosTip | 如果是流式插件需要添加到?stream_plugins?下。 |
auth-bios.lua?作為插件定義文件需要注意幾個版式化的約定:
-- 引入一堆依賴
local core = require("apisix.core")
local m_redis = require("apisix.plugins.auth-bios.redis")
...
-- 標識插件名稱
local plugin_name = "auth-bios"
-- 定義插件配置參數(shù)
local schema = {
type = "object",
properties = {
-- 一堆配置參數(shù)
redis_host = { type = "string" },
redis_port = { type = "integer", default = 6379 },
redis_password = { type = "string" },
...
},
-- 必選參數(shù)列表
required = { "redis_host" }
}
-- 定義插件信息
local _M = {
version = 0.1,
-- 優(yōu)先級,官方推薦1~99,過高的優(yōu)先級先搶奪一些基礎插件的優(yōu)先執(zhí)行權
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 目錄下的各個具體處理邏輯
-- 先根據(jù)請求入?yún)@取請求身份(誰發(fā)起的請求,可以是人、租戶、應用、設備等)及請求的資源(基于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 _MTip | 完整文件見:https://github.com/ideal-world/bios/blob/main/gateway/apisix/apisix/plugins/auth-bios.lua |
auth-bios?目錄包含核心處理邏輯及一些輔助方法,與APISIX插件開發(fā)的規(guī)約關系不大,本文不展開介紹,有興趣的讀者可到對應的github工程下查看。
插件單元測試
Tip | APISIX的測試基于?test-nginx,如果是熟悉Java、Go、Node、Rust等項目的開發(fā),那么對這種測試方案一定很難適應。筆者覺得并不優(yōu)雅且使用復雜,當然也許是沒有領會到其精髓。 |
測試文件約定寫在?t/plugin/?下,我們創(chuàng)建同名的?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?返回數(shù)據(jù),然后在?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
然后就可以發(fā)起測試,示例如下:
# 添加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'
# 測試成功(先執(zhí)行單元測試)
curl -i -X GET "http://127.0.0.1:9080/cache/1" -H 'BIOS-Host: app1.tenant1' -H 'BIOS-Token: tokenxxx'小結
APISIX的插件開發(fā)并不算復雜,但需要學習lua、perl及相關的知識,還是有一定的門檻,所以APISIX也推出了插件擴展機制,支持使用自己熟悉的語言開發(fā)插件。但從運行機制看畢竟有本地RPC交互,在性能上可能會有些損失。說到性能,插件質量的優(yōu)劣對APISIX的影響很大,本文示例的插件也有許多可優(yōu)化的地方,比如更少的序列化邏輯、使用pb代替json、計算復雜度高的邏輯使用C/Rust封裝等。
