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可视化配置,也可以用命令

image

 

image

 

posted @ 2026-01-19 16:38  吃吃吃大王  阅读(30)  评论(0)    收藏  举报