mushenglon-sudo opened a new issue, #12752: URL: https://github.com/apache/apisix/issues/12752
### Current Behavior -- Licensed to the Apache Software Foundation (ASF) under one or more -- contributor license agreements. See the NOTICE file distributed with -- this work for additional information regarding copyright ownership. -- The ASF licenses this file to You under the Apache License, Version 2.0 -- (the "License"); you may not use this file except in compliance with -- the License. You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- local core = require("apisix.core") local helper = require("apisix.plugins.ext-plugin.helper") local constants = require("apisix.constants") local http = require("resty.http") local cjson = require("cjson.safe") local ngx = ngx local ngx_print = ngx.print local ngx_flush = ngx.flush local string = string local str_sub = string.sub local schema = { type = "object", properties = { xCeaiServiceId = {type = "string"}, allow_degradation = {type = "boolean", default = false}, status_on_error = {type = "integer", minimum = 200, maximum = 599, default = 403}, ssl_verify = { type = "boolean", default = true, }, request_method = { type = "string", default = "POST", enum = {"GET", "POST"}, description = "the method for client to request the authorization service" }, request_headers = { type = "array", default = {}, items = {type = "string"}, description = "client request header that will be sent to the authorization service" }, extra_headers = { type = "object", minProperties = 1, patternProperties = { ["^[^:]+$"] = { type = "string", description = "header value as a string; may contain variables" .. "like $remote_addr, $request_uri" } }, description = "extra headers sent to the authorization service; " .. "values must be strings and can include variables" .. "like $remote_addr, $request_uri." }, upstream_headers = { type = "array", default = {}, items = {type = "string"}, description = "authorization response header that will be sent to the upstream" }, client_headers = { type = "array", default = {}, items = {type = "string"}, description = "authorization response header that will be sent to" .. "the client when authorizing failed" }, timeout = { type = "integer", minimum = 1, maximum = 60000, default = 3000, description = "timeout in milliseconds", }, keepalive = {type = "boolean", default = true}, keepalive_timeout = {type = "integer", minimum = 1000, default = 60000}, keepalive_pool = {type = "integer", minimum = 1, default = 5}, }, required = {"xCeaiServiceId"} } local name = "custom-output-check" local _M = { version = 0.1, priority = 2006, name = name, schema = schema, } function _M.check_schema(conf) core.utils.check_tls_bool({"ssl_verify"}, conf, _M.name) return core.schema.check(_M.schema, conf) end local function include_req_headers(ctx) return core.request.headers(ctx) end local function close(http_obj) local ok, err = http_obj:close() if not ok and err ~= "closed" then core.log.error("close http object failed: ", err) end end local function get_response(ctx,conf) local http_obj = http.new() local ok, err = http_obj:connect({ scheme = ctx.upstream_scheme, host = ctx.picked_server.host, port = ctx.picked_server.port, }) if not ok then return nil, err end http_obj:set_timeout(conf.timeout) local uri = ctx.var.uri local args = ctx.var.args or "" -- 生成唯一请求标识 local request_id = ngx.md5(ngx.now() .. math.random(10000, 99999)) -- 构建防缓存参数 local cache_buster = "&_nc=" .. request_id local new_args = args .. cache_buster local params = { path = uri, query = new_args, headers = include_req_headers(ctx), method = core.request.get_method(), } -- 强制设置防缓存头 local headers = params.headers headers["X-Request-ID"] = request_id headers["Cache-Control"] = "no-cache, no-store, must-revalidate" headers["Pragma"] = "no-cache" headers["Expires"] = "0" headers["X-No-Cache"] = "1" -- 如果是POST请求,添加时间戳到body(如果body是JSON) local body, err = core.request.get_body() local modified_body = body if body and not err then local content_type = headers["Content-Type"] or headers["content-type"] or "" if content_type:find("application/json") then local ok, json_data = pcall(cjson.decode, body) if ok and type(json_data) == "table" then json_data._request_id = request_id json_data._timestamp = math.floor(ngx.now() * 1000) modified_body = cjson.encode(json_data) headers["Content-Length"] = tostring(#modified_body) end end end if modified_body then params["body"] = modified_body end core.log.error("=====================防缓存请求=====================") core.log.error("Request ID: ", request_id) core.log.error("URL: ", uri .. "?" .. new_args) local res, err = http_obj:request(params) local body_content = "" if res and res.body_reader then local chunk, err while true do chunk, err = res.body_reader() if err then core.log.error("read response body error: ", err) break end if not chunk then break end body_content = body_content .. chunk end end core.log.error("=====================响应信息=====================") core.log.error("Status: ", res and res.status or "no response") core.log.error("Request ID: ", request_id) core.log.error("=====================认证服务响应=====================") core.log.error("认证服务状态码: ", res.status) core.log.error("认证服务响应体: ", body_content) if not res then close(http_obj) return nil, err end close(http_obj) return res, body_content, nil end local function send_chunk(body_content) local ok, print_err = ngx_print(body_content) if not ok then return "output response failed: ".. (print_err or "") end local ok, flush_err = ngx_flush(true) if not ok then core.log.warn("flush response failed: ", flush_err) end return nil end local function send_response(ctx, code, body_content) ngx.status = code or 500 -- 发送响应体 local err = send_chunk(body_content) if err then return err end return nil end function _M.before_proxy(conf, ctx) local res, body_content, err = get_response(ctx,conf) if not res or err then return 502 end local code = (res and res.status) or 500 if res and res.status == 200 then -- 业务逻辑 local auth_headers = { ["X-Forwarded-Proto"] = core.request.get_scheme(ctx), ["X-Forwarded-Method"] = core.request.get_method(), ["X-Forwarded-Host"] = core.request.get_host(ctx), ["X-Ceai-Service-Id"] = conf.xCeaiServiceId, -- 添加模型ID到请求头 ["X-Forwarded-Uri"] = ctx.var.request_uri, ["X-Forwarded-For"] = core.request.get_remote_client_ip(ctx), } if conf.request_method == "POST" then auth_headers["Content-Length"] = core.request.header(ctx, "content-length") auth_headers["Expect"] = core.request.header(ctx, "expect") auth_headers["Transfer-Encoding"] = core.request.header(ctx, "transfer-encoding") auth_headers["Content-Encoding"] = core.request.header(ctx, "content-encoding") end if conf.extra_headers then for header, value in pairs(conf.extra_headers) do if type(value) == "number" then value = tostring(value) end local resolve_value, err = core.utils.resolve_var(value, ctx.var) if not err then auth_headers[header] = resolve_value end if err then core.log.error("failed to resolve variable in extra header '", header, "': ",value,": ",err) end end end -- append headers that need to be get from the client request header if #conf.request_headers > 0 then for _, header in ipairs(conf.request_headers) do if not auth_headers[header] then auth_headers[header] = core.request.header(ctx, header) end end end local params = { headers = auth_headers, keepalive = conf.keepalive, ssl_verify = conf.ssl_verify, method = "POST", -- 固定使用POST方法 body = body_content, } if conf.keepalive then params.keepalive_timeout = conf.keepalive_timeout params.keepalive_pool = conf.keepalive_pool end local httpc = http.new() httpc:set_timeout(conf.timeout) local res, err = httpc:request_uri(conf.uri, params) close(httpc) if not res and conf.allow_degradation then return elseif not res then core.log.warn("failed to process forward auth, err: ", err) err = send_response(ctx,conf.status_on_error,"") return end -- 修改:处理鉴权响应的逻辑 if res.status >= 300 then local client_headers = {} if #conf.client_headers > 0 then for _, header in ipairs(conf.client_headers) do client_headers[header] = res.headers[header] end end core.response.set_header(client_headers) err = send_response(ctx, res.status, res.body) return end -- 2. 解析响应体判断鉴权结果 local auth_body, parse_err = cjson.decode(res.body) if not auth_body then -- 记录详细错误:原始响应体+解析错误 core.log.error("failed to parse authorization response: ", "body=", res.body, ", error=", parse_err) err = send_response(ctx, 500, parse_err) return end if not auth_body.statusCode then core.log.error("statusCode is nil auth_body=", auth_body) err = send_response(ctx, 500, "statusCode is nil") return end -- 3. 检查statusCode字段,statusCode不等于0表示鉴权失败 if auth_body.statusCode and auth_body.statusCode ~= 0 then local client_headers = {} if #conf.client_headers > 0 then for _, header in ipairs(conf.client_headers) do client_headers[header] = res.headers[header] end end core.response.set_header(client_headers) err = send_response(ctx, res.status, res.body) return end -- 4. 鉴权成功,传递指定的响应头给上游服务 for _, header in ipairs(conf.upstream_headers) do local header_value = res.headers[header] if header_value then core.request.set_header(ctx, header, header_value) end end end -- send origin response, status maybe changed. err = send_response(ctx, code, body_content) if err then core.log.error("send response error: ", tostring(err)) return not ngx.headers_sent and 502 or nil end return end return _M [{"finish_reason":"length","index":0,"logprobs":null,"prompt_logprobs":null,"prompt_token_ids":null,"stop_reason":null,"text":"M78星云与成都有什么关联吗?M78星云与成都有什么关联吗?M78星云与成都有什么关联吗?M78星云与成都有什么关联吗?M78星云与成都有什么关联吗?M78星云与成都有什么关联吗?M78星","token_ids":null}],"created":1763014030,"id":"cmpl-ad0ecc9b60954d059f77aef4c5f62c3a","kv_transfer_params":null,"model":"model","object":"text_completion","service_tier":null,"system_fingerprint":null,"usage":{"completion_tokens":70,"prompt_tokens":13,"prompt_tokens_details":null,"total_tokens":83}}}, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 192.168.65.1 - - [16/Nov/2025:15:22:26 +0000] 127.0.0.1:9080 "POST /protected-word-manager/api/v1/Content HTTP/1.1" 400 63 0.015 "-" "PostmanRuntime/7.50.0" - - - "http://127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:176: get_response(): =====================防缓存请求=====================, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:177: get_response(): Request ID: 081dfff041ec1be194c82307cd1f3014, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:178: get_response(): URL: /protected-word-manager/api/v1/Content?&_nc=081dfff041ec1be194c82307cd1f3014, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:198: get_response(): =====================响应信息=====================, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:199: get_response(): Status: 400, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:200: get_response(): Request ID: 081dfff041ec1be194c82307cd1f3014, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:201: get_response(): =====================认证服务响应=====================, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:202: get_response(): 认证服务状态码: 400, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 2025/11/16 15:22:29 [error] 49#49: *954 [lua] custom-output-check.lua:203: get_response(): 认证服务响应体: 400 Bad Request, client: 192.168.65.1, server: _, request: "POST /protected-word-manager/api/v1/Content HTTP/1.1", host: "127.0.0.1:9080" 192.168.65.1 - - [16/Nov/2025:15:22:29 +0000] 127.0.0.1:9080 "POST /protected-word-manager/api/v1/Content HTTP/1.1" 400 25 0.003 "-" "PostmanRuntime/7.50.0" - - - "http://127.0.0.1:9080" ### Expected Behavior _No response_ ### Error Logs _No response_ ### Steps to Reproduce Run APISIX via the Docker image ### Environment - APISIX version (run `apisix version`): - Operating system (run `uname -a`): - OpenResty / Nginx version (run `openresty -V` or `nginx -V`): - etcd version, if relevant (run `curl http://127.0.0.1:9090/v1/server_info`): - APISIX Dashboard version, if relevant: - Plugin runner version, for issues related to plugin runners: - LuaRocks version, for installation issues (run `luarocks --version`): -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
