
## 前言 Kong 內建的 ACL 有些時候,並不符合需求單位的情境,因此需要客製,這邊做就個POC。
在這篇教學中,你將學會:
在開始之前,請確保你已經安裝:
custom-acl/ ├── handler.lua # 主要邏輯處理 ├── schema.lua # 配置定義 ├── api.lua # 管理 API ├── init.sql # 資料庫初始化 ├── Dockerfile # 容器化配置 └── docker-compose.yml
用戶請求 → Kong Gateway → Custom ACL 插件 → 檢查權限 → 允許/拒絕 ↓ PostgreSQL 資料庫
-- 建立客戶 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);
說明:
customer_id
:客戶識別碼api_path
:API 路徑method
:HTTP 方法is_active
:權限是否啟用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
程式碼說明:
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 }} } }} } }
配置說明:
error_code
:權限拒絕時的 HTTP 狀態碼error_message
:權限拒絕時的錯誤訊息enable_logging
:是否啟用詳細日誌debug
:是否啟用除錯模式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 功能說明:
GET /custom-acl/permissions
:查詢客戶權限列表POST /custom-acl/permissions
:新增客戶權限GET /custom-acl/test
:測試插件是否正常運作# 使用官方 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"]
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
# 建立並啟動所有服務 docker-compose up --build # 檢查服務狀態 docker-compose ps
# 連接到資料庫容器 docker exec -it kong-database psql -U kong -d kong # 檢查表是否建立 \dt customer_api_permissions # 查看測試資料 SELECT * FROM customer_api_permissions;
# 測試插件是否正常運作 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 }'
症狀: Kong 啟動時出現插件載入錯誤 解決方案:
# 檢查插件檔案權限 docker exec -it kong-gateway ls -la /usr/local/share/lua/5.1/kong/plugins/custom-acl # 檢查 Kong 日誌 docker-compose logs kong
症狀: 插件無法連接資料庫 解決方案:
# 檢查資料庫服務狀態 docker-compose ps kong-database # 檢查環境變數 docker exec -it kong-gateway env | grep KONG_PG
症狀: 有權限的請求被拒絕 解決方案:
# 檢查資料庫中的權限資料 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 "權限檢查"
你已經成功學會如何開發 Kong Custom ACL 插件!這個專案涵蓋了: