---
title: "Kong Custom ACL 套件開發教學"
description: "跟著步驟學習如何開發 Kong Custom ACL 套件，從基礎概念到完整實作，打造專屬的 API 權限管理系統。"
canonical_url: "https://blog.markkulab.net/post/kong-custom-acl-plugin-development"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2025-08-20 01:01:00 +0800"
category: "DevOps"
tags: ["kong", "api gateway", "custom plugin", "lua", "postgresql", "權限管理", "devops", "acl"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# Kong Custom ACL 套件開發教學

## 前言

Kong 內建的 ACL 功能在某些情境下，無法完全滿足實際需求，因此常常需要依照企業或專案的權限邏輯進行客製化。本篇文章+以一個簡單的 POC 為例，示範如何從零開始開發一個 Kong Custom ACL 插件，實現自訂的 API 權限控管。

## 🎯 學習目標

在這篇教學中，你將學會：

- 了解 Kong 插件系統的基本概念
- 設計簡單且實用的權限管理資料庫
- 開發自訂 ACL 插件並串接資料庫
- 以 Docker 方式部署與測試整體流程

## 📋 前置準備

在開始之前，請先確認你已具備／安裝以下環境與知識：

- Docker 與 Docker Compose
- 基本 Lua 程式設計概念
- PostgreSQL 資料庫基本操作與 SQL 基礎

---

## 🚀 第一步：了解專案架構

### 專案結構

```text
custom-acl/
├── handler.lua      # 主要邏輯處理
├── schema.lua       # 配置定義
├── api.lua          # 管理 API
├── init.sql         # 資料庫初始化
├── Dockerfile       # 容器化配置
└── docker-compose.yml
```

### 系統流程圖

```text
用戶請求 → Kong Gateway → Custom ACL 插件 → 檢查權限 → 允許 / 拒絕
                ↓
            PostgreSQL 資料庫
```

---

## 🗄️ 第二步：設計資料庫結構

### 建立權限管理表

```sql:custom-acl/init.sql
-- 建立客戶 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`：該筆權限是否啟用中
- 索引則用來加速常見查詢條件（客戶、路徑、啟用狀態）

---

## 🔧 第三步：開發套件處理常式

### 建立 `handler.lua`

這邊示範的實作習慣，是直接使用 OpenResty 的底層元件（例如 `resty.postgres`）來處理資料庫連線，減少對 Kong 版本變動的耦合，讓插件在升級 Kong 時相對穩定。

```lua:custom-acl/handler.lua
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
```

**程式碼重點說明：**

1. **取得客戶 ID**：從 Kong 既有的身份驗證插件中讀取 `consumer.custom_id` 作為客戶識別。
2. **建立資料庫連線**：使用 `resty.postgres` 依環境變數設定連線至 PostgreSQL。
3. **檢查權限**：依照 `customer_id` 查詢該客戶可用的 API 路徑，並透過「完整比對」或「前綴比對」判斷是否允許。
4. **回應結果**：若無權限則直接回傳 403 與錯誤訊息，否則放行請求。

---

## ⚙️ 第四步：定義插件配置

### 建立 `schema.lua`

```lua:custom-acl/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 }}
            }
        }}
    }
}
```

**配置說明：**

- `error_code`：權限被拒絕時回傳的 HTTP 狀態碼。
- `error_message`：權限被拒絕時的預設錯誤訊息。
- `enable_logging`：是否輸出較詳細的權限檢查日誌。
- `debug`：是否開啟除錯模式（可視需求增加更多 log 或 debug 行為）。

---

## 🌐 第五步：建立管理 API

### 建立 `api.lua`

```lua:custom-acl/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 功能說明：**

- `GET /custom-acl/permissions`：查詢指定 `customer_id` 的啟用中權限列表。
- `POST /custom-acl/permissions`：新增一筆客戶 API 權限設定。
- `GET /custom-acl/test`：快速檢查 Custom ACL 插件是否正常啟用且可回應。

---

## 🐳 第六步：容器化部署

### 建立 `Dockerfile`

```dockerfile:custom-acl/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"]
```

### 建立 `docker-compose.yml`

```yaml:custom-acl/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
```

---

## 🚀 第七步：部署和測試

### 1. 啟動服務

```bash
# 建立並啟動所有服務
docker-compose up --build

# 檢查服務狀態
docker-compose ps
```

### 2. 檢查資料庫

```bash
# 連接到資料庫容器
docker exec -it kong-database psql -U kong -d kong

# 檢查表是否建立
\dt customer_api_permissions

# 查看測試資料
SELECT * FROM customer_api_permissions;
```

### 3. 測試插件

```bash
# 測試插件是否正常運作
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
  }'
```

---

## 🔍 第八步：常見問題排解

### 問題 1：插件無法載入

**症狀：** Kong 啟動時出現插件載入錯誤。

**排解步驟：**

```bash
# 檢查插件檔案與權限
docker exec -it kong-gateway ls -la /usr/local/share/lua/5.1/kong/plugins/custom-acl

# 檢查 Kong 容器日誌
docker-compose logs kong
```

---

### 問題 2：資料庫連接失敗

**症狀：** 插件無法連線到 PostgreSQL。

**排解步驟：**

```bash
# 檢查資料庫服務狀態
docker-compose ps kong-database

# 檢查 Kong 容器中的資料庫相關環境變數
docker exec -it kong-gateway env | grep KONG_PG
```

---

### 問題 3：權限檢查結果不符合預期

**症狀：** 理論上有權限的請求卻被拒絕，或是沒有權限的請求被放行。

**排解步驟：**

```bash
# 檢查資料庫中的權限資料是否設定正確
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 插件，並且走過以下幾個重要步驟：

- ✅ 規劃並建立權限管理資料庫結構  
- ✅ 使用 Lua 開發 Kong 自訂插件  
- ✅ 透過 Docker / Docker Compose 完成容器化部署  
- ✅ 利用管理 API 與實際請求進行測試與驗證  

這個 POC 可以作為日後延伸功能的基礎，例如：

- 更細緻的權限維度（依 Service / Route / Method）
- 後台管理介面（搭配 CMS 或後台系統）
- 權限異動的稽核與日誌記錄

### 相關資源

- [Kong 官方文件](https://docs.konghq.com/)
- [Lua 程式設計指南](https://www.lua.org/pil/)
- [PostgreSQL 官方文件](https://www.postgresql.org/docs/)

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/kong-custom-acl-plugin-development)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
