Introduction
Kong's built-in ACL feature can't fully meet real-world requirements in certain scenarios, so it often needs to be customized according to an enterprise's or project's authorization logic. This article uses a simple POC as an example to demonstrate how to develop a Kong Custom ACL plugin from scratch to implement custom API access control.
🎯 Learning Objectives
In this tutorial, you will learn how to:
- Understand the basic concepts of the Kong plugin system
- Design a simple and practical authorization management database
- Develop a custom ACL plugin and connect it to a database
- Deploy and test the entire flow using Docker
📋 Prerequisites
Before you begin, please ensure you have the following environment and knowledge:
- Docker and Docker Compose
- Basic concepts of Lua programming
- Basic operations of PostgreSQL databases and fundamental SQL
🚀 Step 1: Understand the Project Architecture
Project Structure
custom-acl/
├── handler.lua # 主要邏輯處理
├── schema.lua # 配置定義
├── api.lua # 管理 API
├── init.sql # 資料庫初始化
├── Dockerfile # 容器化配置
└── docker-compose.yml
System Flowchart
用戶請求 → Kong Gateway → Custom ACL 插件 → 檢查權限 → 允許 / 拒絕
↓
PostgreSQL 資料庫
🗄️ Step 2: Design the Database Schema
Create the Authorization Management Table
-- 建立客戶 API 權限表
CREATE TABLE IF NOT EXISTS customer_api_permissions (
id SERIAL PRIMARY KEY,
customer_id VARCHAR(100) NOT NULL,
service_id VARCHAR(100),
route_id VARCHAR(100),
api_path VARCHAR(500) NOT NULL,
method VARCHAR(10) DEFAULT 'GET',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
created_by VARCHAR(100) DEFAULT 'system',
updated_by VARCHAR(100) DEFAULT 'system'
);
-- 建立索引提升查詢效能
CREATE INDEX IF NOT EXISTS idx_customer_permissions_customer_id
ON customer_api_permissions(customer_id);
CREATE INDEX IF NOT EXISTS idx_customer_permissions_api_path
ON customer_api_permissions(api_path);
CREATE INDEX IF NOT EXISTS idx_customer_permissions_active
ON customer_api_permissions(is_active);
-- 插入測試資料
INSERT INTO customer_api_permissions (customer_id, api_path, method, is_active) VALUES
('customer_001', '/api/users', 'GET', true),
('customer_001', '/api/orders', 'POST', true),
('customer_002', '/api/products', 'GET', true),
('customer_002', '/api/analytics', 'GET', false);
Field Descriptions:
customer_id: Consumer identifierapi_path: API pathmethod: HTTP methodis_active: Whether the permission is enabled- The index is used to speed up common query conditions (consumer, path, enabled status).
🔧 Step 3: Develop the Plugin Handler
Create handler.lua
The implementation practice demonstrated here is to directly use OpenResty's underlying components (e.g., resty.postgres) to handle database connections. This reduces coupling with Kong version changes, making the plugin relatively stable when upgrading Kong.
local kong_meta = require "kong.meta"
local cjson = require "cjson.safe"
local postgres = require "resty.postgres"
local kong = kong
local _M = {}
local CustomAclHandler = {
PRIORITY = 1000, -- 執行優先順序
VERSION = "1.0.0",
}
-- 步驟 1:從 Kong 取得客戶 ID
local function extract_customer_id_from_keyauth()
local consumer = kong.client.get_consumer()
if consumer and consumer.custom_id then
kong.log.debug("取得到客戶 ID: ", consumer.custom_id)
return consumer.custom_id
end
return nil
end
-- 步驟 2:連接資料庫
local function get_postgres_connection()
local config = {
host = os.getenv("KONG_PG_HOST") or "kong-database",
port = tonumber(os.getenv("KONG_PG_PORT")) or 5432,
database = os.getenv("KONG_PG_DATABASE") or "kong",
user = os.getenv("KONG_PG_USER") or "kong",
password = os.getenv("KONG_PG_PASSWORD") or "kong"
}
local db, err = postgres:new()
if not db then
return nil, "無法建立資料庫連接: " .. tostring(err)
end
db:set_timeout(5000)
local ok, err = db:connect(config.host, config.port, config.database, config.user, config.password)
if not ok then
return nil, "連接資料庫失敗: " .. tostring(err)
end
return db
end
-- 步驟 3:檢查權限
local function check_permissions(customer_id, api_path)
local query = string.format([[
SELECT id, api_path, is_active
FROM customer_api_permissions
WHERE customer_id = '%s' AND is_active = true
]], customer_id)
kong.log.debug("檢查權限: ", customer_id, " -> ", api_path)
local db, err = get_postgres_connection()
if not db then
return false, "資料庫連接失敗: " .. tostring(err)
end
local res, err = db:query(query)
db:close()
if not res or #res == 0 then
return false, "無權限記錄"
end
-- 檢查路徑是否匹配
for _, permission in ipairs(res) do
if permission.is_active and permission.api_path then
-- 完全匹配或路徑前綴匹配
if permission.api_path == api_path or
string.sub(api_path, 1, string.len(permission.api_path)) == permission.api_path then
kong.log.notice("✅ 權限通過: ", permission.id)
return true, "權限匹配: " .. permission.id
end
end
end
return false, "無匹配權限"
end
-- 主要處理函數
function CustomAclHandler:access(conf)
kong.log.notice("開始 ACL 權限檢查")
-- 步驟 1:取得客戶 ID
local customer_id = extract_customer_id_from_keyauth()
if not customer_id then
return kong.response.exit(403, {
error = "需要客戶 ID",
message = "請先通過身份驗證"
})
end
-- 步驟 2:取得請求路徑
local api_path = kong.request.get_path()
kong.log.debug("檢查路徑: ", customer_id, " -> ", api_path)
-- 步驟 3:檢查權限
local allowed, reason = check_permissions(customer_id, api_path)
if not allowed then
kong.log.warn("❌ 權限拒絕: ", customer_id, " -> ", api_path)
return kong.response.exit(conf.error_code or 403, {
error = conf.error_message or "存取被拒絕",
customer_id = customer_id,
api_path = api_path,
reason = reason
})
end
-- 步驟 4:權限通過
kong.log.notice("✅ 權限檢查通過: ", reason)
end
return CustomAclHandler
Code Highlights:
- Get Consumer ID: Read
consumer.custom_idfrom Kong's existing authentication plugins to use as the consumer identifier. - Establish Database Connection: Use
resty.postgresto connect to PostgreSQL based on environment variable settings. - Check Permissions: Query the available API paths for the consumer according to
customer_idand determine whether to allow the request through an "exact match" or "prefix match". - Respond with Result: If the permission is denied, return a 403 status with an error message; otherwise, allow the request to proceed.
⚙️ Step 4: Define the Plugin Schema
Create schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "custom-acl",
fields = {
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
-- 錯誤回應設定
{ error_code = { type = "number", default = 403 }},
{ error_message = { type = "string", default = "存取被拒絕" }},
-- 日誌設定
{ enable_logging = { type = "boolean", default = true }},
-- 除錯模式
{ debug = { type = "boolean", default = false }}
}
}}
}
}
Configuration Description:
error_code: The HTTP status code to return when permission is denied.error_message: The default error message for when permission is denied.enable_logging: Whether to output more detailed permission check logs.debug: Whether to enable debug mode (you can add more logs or debugging behavior as needed).
🌐 Step 5: Create the Management API
Create api.lua
local cjson = require "cjson"
local postgres = require "resty.postgres"
-- 資料庫連接函數
local function get_postgres_connection()
local config = {
host = os.getenv("KONG_PG_HOST") or "kong-database",
port = tonumber(os.getenv("KONG_PG_PORT")) or 5432,
database = os.getenv("KONG_PG_DATABASE") or "kong",
user = os.getenv("KONG_PG_USER") or "kong",
password = os.getenv("KONG_PG_PASSWORD") or "kong"
}
local db, err = postgres:new()
if not db then
return nil, err
end
db:set_timeout(5000)
local ok, err = db:connect(config.host, config.port, config.database, config.user, config.password)
if not ok then
return nil, err
end
return db
end
-- 執行查詢
local function execute_query(query, params)
local db, err = get_postgres_connection()
if not db then
return nil, err
end
local res, err = db:query(query, params)
db:close()
if not res then
return nil, err
end
return res
end
return {
-- 查詢權限列表
["/custom-acl/permissions"] = {
GET = function(self, dao_factory, helpers)
local customer_id = kong.request.get_query()["customer_id"]
if not customer_id then
return kong.response.exit(400, { error = "需要提供 customer_id 參數" })
end
local query = [[
SELECT id, customer_id, api_path, method, is_active, created_at
FROM customer_api_permissions
WHERE customer_id = $1 AND is_active = true
ORDER BY created_at DESC
]]
local res, err = execute_query(query, {customer_id})
if not res then
return kong.response.exit(500, {
error = "資料庫查詢失敗",
details = tostring(err)
})
end
return kong.response.exit(200, {
customer_id = customer_id,
permissions = res,
total = #res
})
end,
-- 新增權限
POST = function(self, dao_factory, helpers)
local body = kong.request.get_body()
if not body.customer_id then
return kong.response.exit(400, { error = "需要提供 customer_id" })
end
if not body.api_path then
return kong.response.exit(400, { error = "需要提供 api_path" })
end
local insert_query = [[
INSERT INTO customer_api_permissions
(customer_id, api_path, method, is_active, created_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, customer_id, api_path, method, is_active, created_at
]]
local res, err = execute_query(insert_query, {
body.customer_id,
body.api_path,
body.method or 'GET',
body.is_active ~= false,
body.created_by or 'system'
})
if not res then
return kong.response.exit(500, {
error = "建立權限失敗",
details = tostring(err)
})
end
return kong.response.exit(201, {
message = "權限建立成功",
permission = res[1]
})
end
},
-- 測試 API
["/custom-acl/test"] = {
GET = function(self, dao_factory, helpers)
return kong.response.exit(200, {
message = "Custom ACL 插件運作正常!",
timestamp = ngx.time(),
version = "1.0.0"
})
end
}
}
API Functionality:
GET /custom-acl/permissions: Query the list of enabled permissions for a specifiedcustomer_id.POST /custom-acl/permissions: Add a new consumer API permission setting.GET /custom-acl/test: Quickly check if the Custom ACL plugin is enabled and responsive.
🐳 Step 6: Containerize the Deployment
Create Dockerfile
# 使用官方 Kong 映像
FROM kong:3.4
# 安裝必要工具
USER root
RUN apk add --no-cache postgresql-client
# 複製插件檔案
COPY custom-acl /usr/local/share/lua/5.1/kong/plugins/custom-acl
RUN chown -R kong:kong /usr/local/share/lua/5.1/kong/plugins/custom-acl
# 將插件加入 Kong 插件列表
RUN sed -i '/local plugins *= *{/a\ "custom-acl",' /usr/local/share/lua/5.1/kong/constants.lua
# 設定環境變數
ENV KONG_PLUGINS=bundled,custom-acl
# 切換回 kong 用戶
USER kong
# 啟動命令
CMD ["kong", "docker-start"]
Create docker-compose.yml
version: '3.8'
services:
# PostgreSQL 資料庫
kong-database:
image: postgres:15-alpine
container_name: kong-database
environment:
POSTGRES_USER: kong
POSTGRES_DB: kong
POSTGRES_PASSWORD: kong
volumes:
- kong-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kong"]
interval: 10s
timeout: 5s
retries: 5
networks:
- kong-net
# Kong API Gateway
kong:
build: .
container_name: kong-gateway
environment:
KONG_DATABASE: postgres
KONG_PG_HOST: kong-database
KONG_PG_PORT: 5432
KONG_PG_DATABASE: kong
KONG_PG_USER: kong
KONG_PG_PASSWORD: kong
KONG_PLUGINS: bundled,custom-acl
KONG_ADMIN_LISTEN: 0.0.0.0:8001
KONG_PROXY_LISTEN: 0.0.0.0:8000
ports:
- "8000:8000" # Proxy
- "8001:8001" # Admin API
depends_on:
kong-database:
condition: service_healthy
networks:
- kong-net
volumes:
kong-data:
networks:
kong-net:
driver: bridge
🚀 Step 7: Deploy and Test
1. Start the Services
# 建立並啟動所有服務
docker-compose up --build
# 檢查服務狀態
docker-compose ps
2. Check the Database
# 連接到資料庫容器
docker exec -it kong-database psql -U kong -d kong
# 檢查表是否建立
\dt customer_api_permissions
# 查看測試資料
SELECT * FROM customer_api_permissions;
3. Test the Plugin
# 測試插件是否正常運作
curl http://localhost:8001/custom-acl/test
# 查詢權限列表
curl "http://localhost:8001/custom-acl/permissions?customer_id=customer_001"
# 新增權限
curl -X POST http://localhost:8001/custom-acl/permissions \
-H "Content-Type: application/json" \
-d '{
"customer_id": "customer_003",
"api_path": "/api/test",
"method": "GET",
"is_active": true
}'
🔍 Step 8: Common Troubleshooting
Problem 1: Plugin Fails to Load
Symptom: A plugin loading error appears when Kong starts.
Troubleshooting Steps:
# 檢查插件檔案與權限
docker exec -it kong-gateway ls -la /usr/local/share/lua/5.1/kong/plugins/custom-acl
# 檢查 Kong 容器日誌
docker-compose logs kong
Problem 2: Database Connection Fails
Symptom: The plugin cannot connect to PostgreSQL.
Troubleshooting Steps:
# 檢查資料庫服務狀態
docker-compose ps kong-database
# 檢查 Kong 容器中的資料庫相關環境變數
docker exec -it kong-gateway env | grep KONG_PG
Problem 3: Permission Check Results Are Not as Expected
Symptom: A request that should be permitted is denied, or a request that should be denied is allowed.
Troubleshooting Steps:
# 檢查資料庫中的權限資料是否設定正確
docker exec -it kong-database psql -U kong -d kong -c "SELECT * FROM customer_api_permissions WHERE customer_id = 'your_customer_id';"
# 檢視 Kong 日誌中的權限檢查流程
docker-compose logs kong | grep "權限檢查"
🎉 Congratulations!
At this point, you have completed a working Kong Custom ACL plugin and have gone through the following key steps:
- ✅ Planned and created the authorization management database schema
- ✅ Developed a custom Kong plugin using Lua
- ✅ Completed containerized deployment using Docker / Docker Compose
- ✅ Performed testing and validation using the management API and actual requests
This POC can serve as a foundation for future extensions, such as:
- More granular permission dimensions (by Service / Route / Method)
- A backend management interface (integrated with a CMS or backend system)
- Auditing and logging for permission changes




























Comments