基于APISIX的ABAC模型鉴权插件开发

Posted by Coding Ideal World on September 3, 2021

APISIX是一个非常优秀的开源全流量网关,内置了很多插件。但如果要扩展实现自定义的插件,网上可参考的文章非常少。本文将以简化版ABAC模型的鉴权需求为例介绍APISIX插件的开发。

前置知识

  1. 如果不了解APISIX请先移步官网: https://apisix.apache.org/

  2. 如果不会lua请先学习基础的语法: https://www.lua.org/start.html 或是 https://learnxinyminutes.com/docs/lua/

  3. 也许你还需要知道一点点perl语法: https://learnxinyminutes.com/docs/perl/

  4. 最后还需要先学习下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)介绍,如操作系统有差异请自行修改。
  1. 添加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
  2. 安装Lua及各类开发类库/工具

    # 注意lua必须为dev版本
    sudo apt-get -y install git curl liblua5.1-0-dev openresty openresty-openssl111-dev cpanminus
  3. 安装并启动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 >/dev/null 2>&1 &
  4. 安装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 -
  5. 下载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
  6. 安装依赖

    cd apisix
    make deps
  7. 下载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.yamlplugins: 下:

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

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封装等。