APISIX 鉴权插件与独立鉴权服务集成方案

APISIX 鉴权插件与独立鉴权服务集成方案

集成方案概述

  1. 核心思路
    将鉴权逻辑委托给独立服务,APISIX 鉴权插件在请求转发前调用该服务验证权限。
    支持两种实现方式:

    • 直接调用独立鉴权服务 API
    • 复用现有鉴权插件(如 authz-keycloak
  2. 方案对比

    方案 适用场景 优点 缺点
    自定义鉴权插件 需要高度定制鉴权逻辑,或鉴权服务有特殊协议 完全控制鉴权流程,灵活性强 需开发维护插件,有一定学习成本
    使用 authz-keycloak 插件 鉴权服务符合 OAuth2/OpenID Connect 标准 开箱即用,无需编码 受限于协议规范,灵活性较低

方案一:自定义鉴权插件开发(推荐)

实现步骤
  1. 编写鉴权插件逻辑

    • access 阶段调用鉴权服务 API
    • 根据响应状态码决定是否放行请求
  2. 添加缓存优化性能

    • 使用 Redis 缓存鉴权结果
    • 设置合理的 TTL 避免数据过时
  3. 配置安全通信

    • 使用 HTTPS 调用鉴权服务
    • 添加双向 TLS 认证(可选)
  4. 错误处理与降级

    • 超时自动熔断
    • 服务不可用时降级为默认策略
示例代码
  1. 自定义鉴权插件 (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
  1. 部署插件
# 将插件文件复制到APISIX插件目录
cp custom-auth.lua /usr/local/apisix/plugins/

# 修改apisix配置文件(config.yaml)
plugins:
  - ...其他插件
  - custom-auth

# 重新加载APISIX配置
apisix reload
  1. 路由配置示例
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 插件

配置步骤
  1. 启用插件
# config.yaml
plugins:
  - ...其他插件
  - authz-keycloak
  1. 创建消费者
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
        }
    }
}'
  1. 路由配置
routes:
  - uri: /keycloak-protected/*
    plugins:
      authz-keycloak: {}
    upstream:
      nodes:
        backend-service: 8080

性能优化建议

  1. 缓存策略

    custom-auth:
      cache_enabled: true
      redis_host: "redis-cluster.prod"
      cache_ttl: 600  # 根据业务调整缓存时间
    
  2. 连接池配置

    -- 在插件中优化HTTP连接复用
    httpc:set_keepalive(60000, 100)  -- 保持连接60秒,最大100个连接
    
  3. 批量鉴权

    • 当需要验证多个API时,可设计批量验证接口
    • 使用HTTP/2提升连接效率

监控指标建议

  1. 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"})
    
  2. Grafana 看板

    • 监控关键指标:
      • 鉴权服务响应时间(P99 < 500ms)
      • 缓存命中率(目标 > 85%)
      • 错误率(5分钟内 < 0.5%)

安全增强措施

  1. 请求签名验证

    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
    
  2. 动态令牌吊销

    # 通过Redis PUBLISH命令通知所有网关节点
    redis-cli publish auth_revoked_tokens <token_md5>
    

总结建议

  1. 技术选型建议

    • 优先使用 自定义插件方案,适用于需要深度定制鉴权逻辑的场景
    • 如果鉴权服务符合标准协议,可选用 authz-keycloak 减少开发量
  2. 生产环境部署要点

    # 必须配置项示例
    custom-auth:
      auth_endpoint: "https://auth-service.prod/verify"
      redis_host: "redis-cluster.prod"
      cache_ttl: 600
      timeout: 5000  # 超时时间需大于鉴权服务P99响应时间
    
  3. 典型错误处理流程
    鉴权流程图
    (流程图应包含:请求拦截 → 缓存检查 → 鉴权服务调用 → 结果处理 → 请求转发/阻断)

基于 APISIX 实现接口按次数和按时长收费的架构方案

核心需求

  1. 按次数收费

    • 根据 API 调用次数计费
    • 支持不同接口设置不同费率
  2. 按时长收费

    • 根据 API 调用时长(如请求处理时间)计费
    • 支持按秒、分钟等时间单位计费
  3. 实时计费与限流

    • 实时计算费用并检查余额
    • 余额不足时拒绝请求或降级服务
  4. 数据统计与报表

    • 提供详细的计费日志
    • 支持生成账单和运营报表

架构设计

  1. 核心组件

    • APISIX 网关:负责流量转发和计费插件执行
    • 计费服务:独立服务,处理计费逻辑和余额管理
    • Redis:用于缓存计费数据和限流计数
    • Prometheus + Grafana:监控计费数据和系统性能
    • MySQL/PostgreSQL:存储计费日志和用户余额
  2. 数据流

    • 请求到达 APISIX
    • 调用计费插件,检查余额并计算费用
    • 转发请求到后端服务
    • 记录计费日志并更新余额

实现方案

方案一:基于自定义插件实现
  1. 插件功能

    • access 阶段检查余额并计算费用
    • log 阶段记录计费日志
  2. 插件配置

    plugins:
      - name: billing
        enable: true
        config:
          billing_service_url: "https://billing-service/charge"
          redis_host: "redis-cluster.prod"
          cache_ttl: 600
    
  3. 插件代码 (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
  1. 计费服务示例
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 实现统计
  1. 配置 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
  1. Grafana 看板
    • 监控关键指标:
      • 每个用户的 API 调用次数
      • 每个 API 的收入分布
      • 实时余额变化

性能优化建议

  1. 缓存策略

    billing:
      redis_host: "redis-cluster.prod"
      cache_ttl: 600  # 根据业务调整缓存时间
    
  2. 批量计费

    • 当需要同时计费多个API时,可设计批量计费接口
    • 使用HTTP/2提升连接效率

安全增强措施

  1. 请求签名验证

    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
    
  2. 动态费率调整

    # 通过Redis PUBLISH命令通知所有网关节点
    redis-cli publish rate_change <new_rate>
    

总结建议

  1. 技术选型建议

    • 优先使用 自定义插件方案,适用于需要深度定制计费逻辑的场景
    • 如果计费逻辑简单,可选用现有插件(如 prometheus)减少开发量
  2. 生产环境部署要点

    # 必须配置项示例
    billing:
      billing_service_url: "https://billing-service.prod/charge"
      redis_host: "redis-cluster.prod"
      cache_ttl: 600
      timeout: 5000  # 超时时间需大于计费服务P99响应时间
    
  3. 典型错误处理流程
    计费流程图
    (流程图应包含:请求拦截 → 余额检查 → 计费服务调用 → 结果处理 → 请求转发/阻断)

posted @ 2025-03-12 10:35  CharyGao  阅读(234)  评论(0)    收藏  举报