3.8.0-Kong网关自定义插件-增强版CORS跨域插件
能力增加:
1、origins支持正则、不需要指定协议、不需要指定端口
例如:*.demo.com:*
2、增强安全性,不允许非指定的主机名访问(返回403),kong自带的插件也可以,但必须写一堆域名,支持通配符和正则方便多了
1、新建自定义插件目录
mkdir -p /usr/local/share/lua/5.1/kong/plugins/dynamic-cors/ && chown -R kong.kong /usr/local/share/lua/5.1/kong/plugins/dynamic-cors
2、新建Lua文件
schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "dynamic-cors",
fields = {
{ consumer = typedefs.no_consumer },
{ protocols = typedefs.protocols_http },
{ config = {
type = "record",
fields = {
{ origins = {
type = "array",
default = { "*" },
elements = { type = "string" },
description = "允许的域名列表,支持通配符如 *.example.com"
}},
{ methods = {
type = "array",
default = { "*" },
elements = { type = "string" },
description = "允许的 HTTP 方法"
}},
{ headers = {
type = "array",
default = { "*" },
elements = { type = "string" },
description = "允许的请求头"
}},
{ exposed_headers = {
type = "array",
default = {},
elements = { type = "string" },
description = "允许客户端访问的响应头"
}},
{ credentials = {
type = "boolean",
default = true,
description = "是否允许发送 Cookie/认证信息"
}},
{ max_age = {
type = "integer",
default = 3600,
description = "预检请求缓存时间(秒)"
}},
{ allow_wildcard_subdomains = {
type = "boolean",
default = true,
description = "是否启用通配符子域名匹配"
}}
}
}}
}
}
handler.lua
local DynamicCorsHandler = { PRIORITY = 999, VERSION = "1.8" } local function safe_wildcard_match(origin, pattern) -- 1. 完全匹配 if origin == pattern then return true end -- 2. 全局通配符 if pattern == "*" then return true end -- 3. 提取 Origin 的主机名 local origin_host if origin:find("://") then local rest = origin:match("://(.+)$") if rest then origin_host = rest:match("^([^:]+)") end else origin_host = origin:match("^([^:]+)") end if not origin_host then return false end -- 4. 处理带端口通配符的模式 if pattern:find(":*$") then local host_pattern = pattern:sub(1, -3) if host_pattern == "*" then return true end -- 安全匹配逻辑:必须以 .domain 结尾 if host_pattern:sub(1, 2) == "*." then local domain = host_pattern:sub(3) if origin_host == domain or origin_host:sub(-#domain - 1) == "." .. domain then if origin_host ~= domain then return true end end end if origin_host == host_pattern then return true end end -- 5. 处理不带端口通配符的模式 if pattern:sub(1, 2) == "*." then local domain = pattern:sub(3) if origin_host == domain or origin_host:sub(-#domain - 1) == "." .. domain then if origin_host ~= domain then return true end end end -- 6. 直接主机名匹配 if origin_host == pattern then return true end return false end -- 生成格式化的 Origin 允许列表 local function format_allowed_origins(origins) if not origins or #origins == 0 then return "All origins are allowed" end local formatted = {} for i, origin in ipairs(origins) do if origin == "*" then return "All origins are allowed (*)" end table.insert(formatted, string.format("[%d] %s", i, origin)) end return table.concat(formatted, ", ") end -- 生成格式化的 Methods 允许列表 local function format_allowed_methods(methods) if not methods or #methods == 0 then return "All methods are allowed" end local formatted = {} for i, method in ipairs(methods) do if method == "*" then return "All methods are allowed (*)" end table.insert(formatted, string.format("[%d] %s", i, method)) end return table.concat(formatted, ", ") end function DynamicCorsHandler:access(conf) local origin = kong.request.get_header("Origin") local request_method = kong.request.get_method() -- 如果不是跨域请求,直接返回 if not origin then return end -- 1. 检查 Origin local origin_allowed = false local matched_pattern = nil if not conf.origins or #conf.origins == 0 then origin_allowed = true else for _, pattern in ipairs(conf.origins) do if safe_wildcard_match(origin, pattern) then origin_allowed = true matched_pattern = pattern break end end end if not origin_allowed then -- 生成格式化的允许 Origin 列表 local allowed_origins_formatted = format_allowed_origins(conf.origins) -- 使用专门的 CORS 错误状态码和更明确的错误信息 local error_message = string.format( "CORS Policy: Origin '%s' is not allowed. Please use one of the allowed origins:\n\n%s\n\n", origin, allowed_origins_formatted ) -- 添加使用示例 if conf.origins and #conf.origins > 0 then error_message = error_message .. "Examples of valid requests:\n" for i, pattern in ipairs(conf.origins) do if pattern:find(":*$") then local host_part = pattern:sub(1, -3) if host_part:sub(1, 2) == "*." then local domain = host_part:sub(3) error_message = error_message .. string.format("- https://subdomain.%s\n", domain) else error_message = error_message .. string.format("- https://%s\n", host_part) end elseif pattern:sub(1, 2) == "*." then local domain = pattern:sub(3) error_message = error_message .. string.format("- https://subdomain.%s\n", domain) else error_message = error_message .. string.format("- %s\n", pattern) end end end -- 返回更明确的响应 return kong.response.exit(400, { message = error_message, error = "CORS_ORIGIN_NOT_ALLOWED", code = "CORS001", allowed_origins = conf.origins, allowed_origins_formatted = allowed_origins_formatted, received_origin = origin, hint = "Make sure your Origin header matches one of the allowed patterns" }, { ["Content-Type"] = "application/json", ["X-CORS-Error"] = "Origin not allowed", ["X-Allowed-Origins"] = table.concat(conf.origins or {}, ", "), ["X-Allowed-Origins-Count"] = tostring(#(conf.origins or {})), ["X-Received-Origin"] = origin }) end -- 2. 如果是预检请求,检查 Access-Control-Request-Method if request_method == "OPTIONS" then local requested_method = kong.request.get_header("Access-Control-Request-Method") if requested_method then local method_allowed = false local allowed_method = nil if conf.methods and #conf.methods > 0 then for _, method in ipairs(conf.methods) do if method == "*" or method:upper() == requested_method:upper() then method_allowed = true allowed_method = method break end end else method_allowed = true -- 没有配置方法限制,允许所有 end if not method_allowed then local allowed_methods_formatted = format_allowed_methods(conf.methods) local error_message = string.format( "CORS Policy: Method '%s' is not allowed for preflight request. Allowed methods:\n\n%s", requested_method, allowed_methods_formatted ) return kong.response.exit(400, { message = error_message, error = "CORS_METHOD_NOT_ALLOWED", code = "CORS002", allowed_methods = conf.methods, allowed_methods_formatted = allowed_methods_formatted, requested_method = requested_method, hint = "Use one of the allowed methods for your request" }, { ["Content-Type"] = "application/json", ["X-CORS-Error"] = "Method not allowed", ["X-Allowed-Methods"] = table.concat(conf.methods or {}, ", "), ["X-Requested-Method"] = requested_method }) end end else -- 3. 对于非预检的实际请求,检查请求方法 if conf.methods and #conf.methods > 0 then local method_allowed = false local allowed_method = nil for _, method in ipairs(conf.methods) do if method == "*" or method:upper() == request_method:upper() then method_allowed = true allowed_method = method break end end if not method_allowed then local allowed_methods_formatted = format_allowed_methods(conf.methods) local error_message = string.format( "CORS Policy: Method '%s' is not allowed. Allowed methods:\n\n%s", request_method, allowed_methods_formatted ) local allow_header_value = table.concat(conf.methods, ", ") return kong.response.exit(405, { message = error_message, error = "CORS_METHOD_NOT_ALLOWED", code = "CORS003", allowed_methods = conf.methods, allowed_methods_formatted = allowed_methods_formatted, requested_method = request_method, hint = "Use one of the allowed methods for your request" }, { ["Content-Type"] = "application/json", ["X-CORS-Error"] = "Method not allowed", ["Allow"] = allow_header_value, ["X-Allowed-Methods"] = allow_header_value, ["X-Requested-Method"] = request_method }) end end end end function DynamicCorsHandler:header_filter(conf) local origin = kong.request.get_header("Origin") if origin then local should_set_cors = false if not conf.origins or #conf.origins == 0 then should_set_cors = true else for _, pattern in ipairs(conf.origins) do if safe_wildcard_match(origin, pattern) then should_set_cors = true break end end end if should_set_cors then -- 基础 CORS 头 kong.response.set_header("Access-Control-Allow-Origin", origin) kong.response.set_header("Vary", "Origin") if conf.credentials then kong.response.set_header("Access-Control-Allow-Credentials", "true") end if conf.exposed_headers and #conf.exposed_headers > 0 then local exposed_str if type(conf.exposed_headers) == "table" then exposed_str = table.concat(conf.exposed_headers, ", ") else exposed_str = conf.exposed_headers end kong.response.set_header("Access-Control-Expose-Headers", exposed_str) end -- 获取当前请求方法 local request_method = kong.request.get_method() -- 如果是 OPTIONS 请求,设置预检头 if request_method == "OPTIONS" then local methods if conf.methods then if type(conf.methods) == "table" and #conf.methods > 0 then -- 只返回实际配置的方法,不包含通配符 local allowed_methods = {} for _, method in ipairs(conf.methods) do if method ~= "*" then table.insert(allowed_methods, method:upper()) end end if #allowed_methods > 0 then methods = table.concat(allowed_methods, ", ") else methods = "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD" end elseif conf.methods ~= "" then methods = conf.methods else methods = "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD" end else methods = "GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD" end kong.response.set_header("Access-Control-Allow-Methods", methods) local headers if conf.headers then if type(conf.headers) == "table" and #conf.headers > 0 then headers = table.concat(conf.headers, ", ") elseif conf.headers ~= "" then headers = conf.headers else headers = "*" end else headers = "*" end kong.response.set_header("Access-Control-Allow-Headers", headers) -- 缓存时间 kong.response.set_header("Access-Control-Max-Age", conf.max_age or "86400") -- 添加自定义 CORS 响应头,便于调试 kong.response.set_header("X-CORS-Policy", "enforced") if conf.origins and #conf.origins > 0 then kong.response.set_header("X-CORS-Allowed-Origins", table.concat(conf.origins, ", ")) end end -- 删除敏感头信息 if conf.remove_headers and #conf.remove_headers > 0 then for _, header in ipairs(conf.remove_headers) do kong.response.clear_header(header) end end end end end return DynamicCorsHandler
3、在配置文件里将自定义插件加上
/etc/kong/kong.conf
plugins = bundled,dynamic-cors
4、可以通过KongA可视化配置,也可以用命令



浙公网安备 33010602011771号