APISIX 鉴权插件与独立鉴权服务集成方案
APISIX 鉴权插件与独立鉴权服务集成方案
集成方案概述
-
核心思路
将鉴权逻辑委托给独立服务,APISIX 鉴权插件在请求转发前调用该服务验证权限。
支持两种实现方式:- 直接调用独立鉴权服务 API
- 复用现有鉴权插件(如
authz-keycloak)
-
方案对比
方案 适用场景 优点 缺点 自定义鉴权插件 需要高度定制鉴权逻辑,或鉴权服务有特殊协议 完全控制鉴权流程,灵活性强 需开发维护插件,有一定学习成本 使用 authz-keycloak插件鉴权服务符合 OAuth2/OpenID Connect 标准 开箱即用,无需编码 受限于协议规范,灵活性较低
方案一:自定义鉴权插件开发(推荐)
实现步骤
-
编写鉴权插件逻辑
- 在
access阶段调用鉴权服务 API - 根据响应状态码决定是否放行请求
- 在
-
添加缓存优化性能
- 使用 Redis 缓存鉴权结果
- 设置合理的 TTL 避免数据过时
-
配置安全通信
- 使用 HTTPS 调用鉴权服务
- 添加双向 TLS 认证(可选)
-
错误处理与降级
- 超时自动熔断
- 服务不可用时降级为默认策略
示例代码
- 自定义鉴权插件 (
custom-auth.lua)
local core = require("apisix.core")
local http = require("resty.http")
local redis = require("resty.redis")
local plugin_name = "custom-auth"
local schema = {
type = "object",
properties = {
auth_endpoint = { type = "string", default = "https://auth-service/verify" },
cache_enabled = { type = "boolean", default = true },
redis_host = { type = "string", default = "127.0.0.1" },
redis_port = { type = "integer", default = 6379 },
cache_ttl = { type = "integer", default = 300 }
}
}
local _M = {
version = 1.0,
priority = 2000, -- 确保高于其他插件
name = plugin_name,
schema = schema
}
local function get_redis_conn(conf)
local red = redis:new()
red:set_timeout(1000) -- 1秒超时
local ok, err = red:connect(conf.redis_host, conf.redis_port)
if not ok then
core.log.error("Redis连接失败: ", err)
return nil
end
return red
end
function _M.access(conf, ctx)
-- 1. 获取访问令牌
local token = core.request.header(ctx, "Authorization")
if not token then
core.response.set_header("WWW-Authenticate", "Bearer realm=\"example\"")
return 401, { message = "Missing Authorization header" }
end
-- 2. 检查缓存
if conf.cache_enabled then
local red = get_redis_conn(conf)
if red then
local cached, err = red:get("auth_cache:" .. ngx.md5(token))
if cached == "true" then
red:set_keepalive(10000, 100)
return -- 缓存命中,放行请求
end
end
end
-- 3. 调用鉴权服务
local httpc = http.new()
httpc:set_timeout(3000) -- 3秒超时
local res, err = httpc:request_uri(conf.auth_endpoint, {
method = "POST",
headers = {
["Content-Type"] = "application/json",
["X-Real-IP"] = core.request.get_remote_addr(ctx)
},
body = core.json.encode({ token = token }),
ssl_verify = false -- 生产环境应启用证书验证
})
if not res then
core.log.error("鉴权服务调用失败: ", err)
-- 降级策略:生产环境可配置是否拒绝请求
return 500, { message = "Authentication service unavailable" }
end
-- 4. 处理响应
if res.status >= 200 and res.status < 300 then
-- 缓存成功结果
if conf.cache_enabled and red then
local ok, err = red:setex("auth_cache:" .. ngx.md5(token), conf.cache_ttl, "true")
if not ok then
core.log.warn("缓存写入失败: ", err)
end
red:set_keepalive(10000, 100)
end
-- 添加用户信息到上游
local user_info = core.json.decode(res.body)
core.request.set_header(ctx, "X-User-ID", user_info.user_id)
core.request.set_header(ctx, "X-User-Roles", table.concat(user_info.roles, ","))
else
return res.status, { message = "Access denied: " .. (res.body or "") }
end
end
return _M
- 部署插件
# 将插件文件复制到APISIX插件目录
cp custom-auth.lua /usr/local/apisix/plugins/
# 修改apisix配置文件(config.yaml)
plugins:
- ...其他插件
- custom-auth
# 重新加载APISIX配置
apisix reload
- 路由配置示例
routes:
- uri: /protected/*
plugins:
custom-auth:
auth_endpoint: "https://auth-service.prod/verify"
redis_host: "redis-cluster.prod"
cache_ttl: 600
upstream:
nodes:
backend-service: 8080
方案二:使用 authz-keycloak 插件
配置步骤
- 启用插件
# config.yaml
plugins:
- ...其他插件
- authz-keycloak
- 创建消费者
curl http://127.0.0.1:9080/apisix/admin/consumers -H 'X-API-KEY: <admin-key>' -X PUT -d '
{
"username": "keycloak_user",
"plugins": {
"authz-keycloak": {
"token_endpoint": "https://keycloak-server/auth/realms/master/protocol/openid-connect/token",
"permissions": ["api:read", "api:write"],
"client_id": "apisix-gateway",
"client_secret": "2e43d9f4-ae31-4d59-8ad0-xxxxxxxx",
"grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket",
"timeout": 3000
}
}
}'
- 路由配置
routes:
- uri: /keycloak-protected/*
plugins:
authz-keycloak: {}
upstream:
nodes:
backend-service: 8080
性能优化建议
-
缓存策略
custom-auth: cache_enabled: true redis_host: "redis-cluster.prod" cache_ttl: 600 # 根据业务调整缓存时间 -
连接池配置
-- 在插件中优化HTTP连接复用 httpc:set_keepalive(60000, 100) -- 保持连接60秒,最大100个连接 -
批量鉴权
- 当需要验证多个API时,可设计批量验证接口
- 使用HTTP/2提升连接效率
监控指标建议
-
Prometheus 指标
local prometheus = require("apisix.plugins.prometheus") -- 在插件中记录指标 prometheus:histogram("auth_request_duration_seconds", "Auth service latency", {0.1, 0.5, 1}) prometheus:counter("auth_requests_total", "Total auth requests", {"status"}) -
Grafana 看板
- 监控关键指标:
- 鉴权服务响应时间(P99 < 500ms)
- 缓存命中率(目标 > 85%)
- 错误率(5分钟内 < 0.5%)
- 监控关键指标:
安全增强措施
-
请求签名验证
local hmac = require("resty.hmac") -- 验证请求签名 local sign = core.request.header(ctx, "X-Signature") local secret = "your_shared_secret" local hmac_sha256 = hmac:new(secret, hmac.ALGOS.SHA256) hmac_sha256:update(token) local real_sign = hmac_sha256:final() if real_sign ~= sign then return 403, {message = "Invalid signature"} end -
动态令牌吊销
# 通过Redis PUBLISH命令通知所有网关节点 redis-cli publish auth_revoked_tokens <token_md5>
总结建议
-
技术选型建议
- 优先使用 自定义插件方案,适用于需要深度定制鉴权逻辑的场景
- 如果鉴权服务符合标准协议,可选用
authz-keycloak减少开发量
-
生产环境部署要点
# 必须配置项示例 custom-auth: auth_endpoint: "https://auth-service.prod/verify" redis_host: "redis-cluster.prod" cache_ttl: 600 timeout: 5000 # 超时时间需大于鉴权服务P99响应时间 -
典型错误处理流程

(流程图应包含:请求拦截 → 缓存检查 → 鉴权服务调用 → 结果处理 → 请求转发/阻断)
基于 APISIX 实现接口按次数和按时长收费的架构方案
核心需求
-
按次数收费
- 根据 API 调用次数计费
- 支持不同接口设置不同费率
-
按时长收费
- 根据 API 调用时长(如请求处理时间)计费
- 支持按秒、分钟等时间单位计费
-
实时计费与限流
- 实时计算费用并检查余额
- 余额不足时拒绝请求或降级服务
-
数据统计与报表
- 提供详细的计费日志
- 支持生成账单和运营报表
架构设计
-
核心组件
- APISIX 网关:负责流量转发和计费插件执行
- 计费服务:独立服务,处理计费逻辑和余额管理
- Redis:用于缓存计费数据和限流计数
- Prometheus + Grafana:监控计费数据和系统性能
- MySQL/PostgreSQL:存储计费日志和用户余额
-
数据流
- 请求到达 APISIX
- 调用计费插件,检查余额并计算费用
- 转发请求到后端服务
- 记录计费日志并更新余额
实现方案
方案一:基于自定义插件实现
-
插件功能
- 在
access阶段检查余额并计算费用 - 在
log阶段记录计费日志
- 在
-
插件配置
plugins: - name: billing enable: true config: billing_service_url: "https://billing-service/charge" redis_host: "redis-cluster.prod" cache_ttl: 600 -
插件代码 (
billing.lua)
local core = require("apisix.core")
local http = require("resty.http")
local redis = require("resty.redis")
local plugin_name = "billing"
local schema = {
type = "object",
properties = {
billing_service_url = { type = "string" },
redis_host = { type = "string", default = "127.0.0.1" },
redis_port = { type = "integer", default = 6379 },
cache_ttl = { type = "integer", default = 600 }
},
required = { "billing_service_url" }
}
local _M = {
version = 1.0,
priority = 1000,
name = plugin_name,
schema = schema
}
local function get_redis_conn(conf)
local red = redis:new()
red:set_timeout(1000)
local ok, err = red:connect(conf.redis_host, conf.redis_port)
if not ok then
core.log.error("Redis连接失败: ", err)
return nil
end
return red
end
function _M.access(conf, ctx)
-- 获取用户ID和API信息
local user_id = core.request.header(ctx, "X-User-ID")
local api_path = ctx.var.uri
if not user_id then
return 401, { message = "Missing user identification" }
end
-- 检查缓存
local red = get_redis_conn(conf)
local cache_key = "billing_cache:" .. user_id .. ":" .. api_path
local cached_balance = red and red:get(cache_key) or nil
if cached_balance and tonumber(cached_balance) <= 0 then
return 403, { message = "Insufficient balance" }
end
-- 调用计费服务
local httpc = http.new()
httpc:set_timeout(3000)
local res, err = httpc:request_uri(conf.billing_service_url, {
method = "POST",
headers = {
["Content-Type"] = "application/json",
["X-User-ID"] = user_id
},
body = core.json.encode({ api_path = api_path }),
ssl_verify = false
})
if not res then
core.log.error("计费服务调用失败: ", err)
return 500, { message = "Billing service unavailable" }
end
if res.status ~= 200 then
return res.status, { message = "Billing error: " .. (res.body or "") }
end
-- 更新缓存
local balance = tonumber(res.body)
if red and balance then
red:setex(cache_key, conf.cache_ttl, balance)
red:set_keepalive(10000, 100)
end
-- 添加计费信息到请求头
core.request.set_header(ctx, "X-Billing-Info", res.body)
end
function _M.log(conf, ctx)
-- 记录计费日志
local user_id = core.request.header(ctx, "X-User-ID")
local api_path = ctx.var.uri
local billing_info = core.request.header(ctx, "X-Billing-Info")
if user_id and api_path and billing_info then
core.log.info("Billing log - User: ", user_id, ", API: ", api_path, ", Info: ", billing_info)
end
end
return _M
- 计费服务示例
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class ChargeRequest(BaseModel):
api_path: str
@app.post("/charge")
def charge(request: ChargeRequest, x_user_id: str = Header(...)):
# 模拟计费逻辑
balance = get_user_balance(x_user_id)
cost = calculate_cost(request.api_path)
if balance < cost:
raise HTTPException(status_code=403, detail="Insufficient balance")
new_balance = balance - cost
update_user_balance(x_user_id, new_balance)
return {"balance": new_balance}
方案二:基于 Prometheus 和 Grafana 实现统计
- 配置 Prometheus 指标
local prometheus = require("apisix.plugins.prometheus")
function _M.access(conf, ctx)
-- 在计费逻辑中记录指标
prometheus:counter("billing_requests_total", "Total billing requests", {user_id, api_path})
prometheus:histogram("billing_cost", "API cost distribution", {0.1, 1, 10})
end
- Grafana 看板
- 监控关键指标:
- 每个用户的 API 调用次数
- 每个 API 的收入分布
- 实时余额变化
- 监控关键指标:
性能优化建议
-
缓存策略
billing: redis_host: "redis-cluster.prod" cache_ttl: 600 # 根据业务调整缓存时间 -
批量计费
- 当需要同时计费多个API时,可设计批量计费接口
- 使用HTTP/2提升连接效率
安全增强措施
-
请求签名验证
local hmac = require("resty.hmac") -- 验证请求签名 local sign = core.request.header(ctx, "X-Signature") local secret = "your_shared_secret" local hmac_sha256 = hmac:new(secret, hmac.ALGOS.SHA256) hmac_sha256:update(token) local real_sign = hmac_sha256:final() if real_sign ~= sign then return 403, {message = "Invalid signature"} end -
动态费率调整
# 通过Redis PUBLISH命令通知所有网关节点 redis-cli publish rate_change <new_rate>
总结建议
-
技术选型建议
- 优先使用 自定义插件方案,适用于需要深度定制计费逻辑的场景
- 如果计费逻辑简单,可选用现有插件(如
prometheus)减少开发量
-
生产环境部署要点
# 必须配置项示例 billing: billing_service_url: "https://billing-service.prod/charge" redis_host: "redis-cluster.prod" cache_ttl: 600 timeout: 5000 # 超时时间需大于计费服务P99响应时间 -
典型错误处理流程

(流程图应包含:请求拦截 → 余额检查 → 计费服务调用 → 结果处理 → 请求转发/阻断)
摘抄自网络,便于检索查找。

浙公网安备 33010602011771号