-- keycloak-authz APISIX custom plugin -- Kong keycloak-authz 플러그인을 APISIX로 포팅 -- -- 동작: -- 1. X-Access-Token 헤더에서 JWT 추출 → preferred_username 파싱 -- 2. 외부 API (/gwapi/v1/projectusers/{username}) POST 호출로 권한 확인 -- 3. 결과를 TTL 기반으로 in-memory 캐싱 (lrucache) -- 4. 미인가 시 401, 내부 오류 시 500 반환 local core = require("apisix.core") local http = require("resty.http") local jwt = require("resty.jwt") local lrucache = require("resty.lrucache") local cjson = require("cjson.safe") local plugin_name = "keycloak-authz" -- 최대 1024개의 사용자 인증 결과를 캐싱 local auth_cache, err = lrucache.new(1024) if not auth_cache then error("failed to create lrucache: " .. (err or "unknown")) end local schema = { type = "object", properties = { api_url = { type = "string", description = "권한 확인 API의 base URL (예: https://api.example.com)", }, basic_auth_token = { type = "string", description = "API 호출 시 사용할 Basic 인증 토큰 (Base64 인코딩된 값)", default = "", }, timeout = { type = "integer", description = "API 호출 타임아웃 (밀리초)", default = 5000, minimum = 100, }, skip_ssl_verify = { type = "boolean", description = "API 호출 시 SSL 인증서 검증 생략 여부", default = false, }, ttl = { type = "integer", description = "인증 결과 캐싱 시간 (초)", default = 300, minimum = 1, }, }, required = {"api_url"}, } local _M = { version = 0.1, priority = 950, -- Kong의 PRIORITY = 950과 동일 name = plugin_name, schema = schema, } function _M.check_schema(conf) return core.schema.check(schema, conf) end -- 외부 API 호출로 사용자 권한 확인 local function fetch_auth_status(api_url, preferred_username, basic_auth_token, timeout, skip_ssl_verify) local httpc = http.new() local scheme = ngx.var.scheme local host = ngx.var.host local url = scheme .. "://" .. host local body, encode_err = cjson.encode({ url = url }) if encode_err then return nil, "failed to encode request body: " .. encode_err end local headers = { ["Content-Type"] = "application/json" } if basic_auth_token and basic_auth_token ~= "" then headers["Authorization"] = "Basic " .. basic_auth_token end local full_url = api_url .. "/gwapi/v1/projectusers/" .. preferred_username local res, req_err = httpc:request_uri(full_url, { method = "POST", body = body, headers = headers, timeout = timeout, ssl_verify = not skip_ssl_verify, keepalive_timeout = 60000, keepalive_pool = 10, }) httpc:close() if not res then return nil, "API request failed: " .. (req_err or "unknown error") end core.log.debug("auth API response status=", res.status, " user=", preferred_username) return res.status == 200, nil end function _M.access(conf, ctx) -- X-Access-Token 헤더 추출 local access_token = core.request.header(ctx, "X-Access-Token") if not access_token then return core.response.exit(401, { message = "Missing X-Access-Token header" }) end -- JWT 디코딩 (서명 검증 없이 페이로드만 파싱) local jwt_obj = jwt:load_jwt(access_token) if not jwt_obj or not jwt_obj.payload then core.log.warn("failed to decode JWT token") return core.response.exit(401, { message = "Failed to decode JWT token" }) end local preferred_username = jwt_obj.payload["preferred_username"] if not preferred_username then return core.response.exit(401, { message = "Missing preferred_username in JWT token" }) end local cache_key = "auth_status:" .. preferred_username -- 캐시 조회 local cached_result = auth_cache:get(cache_key) if cached_result ~= nil then core.log.debug("cache hit for user=", preferred_username, " result=", tostring(cached_result)) if not cached_result then return core.response.exit(401, { message = "User is not authorized" }) end ctx.authenticated_user = preferred_username return end -- 캐시 미스: 외부 API 호출 core.log.debug("cache miss, fetching auth status for user=", preferred_username) local is_authorized, fetch_err = fetch_auth_status( conf.api_url, preferred_username, conf.basic_auth_token or "", conf.timeout or 5000, conf.skip_ssl_verify or false ) if fetch_err then core.log.error("auth check failed user=", preferred_username, " err=", fetch_err) return core.response.exit(500, { message = "Internal server error" }) end -- 결과 캐싱 (TTL 적용) auth_cache:set(cache_key, is_authorized, conf.ttl or 300) core.log.debug("cached auth result user=", preferred_username, " authorized=", tostring(is_authorized)) if not is_authorized then core.log.warn("authorization denied for user=", preferred_username) return core.response.exit(401, { message = "User is not authorized" }) end ctx.authenticated_user = preferred_username end return _M