nginx中如何获取upstream返回的"Set-Cookie"header中所有cookie的内容

How can I get the content of all cookies in "Set-Cookie" header returned by upstream in nginx

类似于这里提到的问题https://forum.openresty.us/d/6503-get-content-of-second-set-cookie-header

我有一个 NGINX 配置,它通过上游 auth_request 获取存储在 Set-Cookie 中的 cookie,我需要 return 那些 set-cookie 给客户端,但是每当我尝试 return 只有第一个 set-cookie 被 return 发送给客户端的那些 cookie。

下面是演示该问题的示例配置

 location /auth/ {
    proxy_pass         http://auth/;
    proxy_pass_request_body off;
    proxy_redirect     off;
  }

  location / {
    auth_request       /auth/loggedin;
    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;
    proxy_set_header Cookie "$http_cookie; $auth_cookie";
    proxy_pass         http://someservice/;
  }

在我上面的示例中,我希望可以在 Set-Cookie header a=12; PATH:"/", b=2; PATH:/" 中 return 编辑多个 cookie,我想传递任何 set-cookie s 来自客户端浏览器的上游服务,通过 add_header。目前只有 cookie a 正在发送给客户端,b 总是丢失。

注意: 我希望它是通用的,所以我无法从 header.

中获取确切的 cookie 名称

感谢您提供的任何帮助!

不幸的是,不可能按照您想要的方式进行。使用多个 Set-Cookie header 设置 cookies 是一种常见的方法, Set-Cookie header 上的 MDN documentation 明确表示:

To send multiple cookies, multiple Set-Cookie headers should be sent in the same response.

当从上游收到多个具有相同名称的 header 时,只有第一个可以使用 $upstream_http_<header_name> 变量访问(除了少数例外,例如 Cache-Control 一个,如果我没记错的话)。下面有一个ticket for that on nginx bug tracker (although I did't consider it a bug). Set-Cookie header is really a special case that can't be folded in opposite to many other headers, check this的回答和评论,看看为什么会这样。 (当然,您仍然可以自由使用任何 $upstream_cookie_<cookie_name> per-cookie 变量)。

但是可以使用 OpenResty(在您的问题中提到)或 lua-nginx-module. The bad news, it will be incompatible with the auth_request directive since it is impossible to add those lua_... handlers to the auth location (or any other subrequest location that can be used, e.g., by add_before_body or add_after_body directives from the ngx_http_addition_module). You didn't get an error, but those handlers won't be fired on a subrequest. The good news, functionality similar to auth_request can be implemented using ngx.location.capture API 调用来完成。

因此,如果使用 nginx-lua-module 适合您table(如果不适合,也许该解决方案会对其他人有所帮助),可以通过以下方式完成:

location /auth/ {
    internal;
    proxy_pass http://auth/;
    proxy_redirect off;
}

location / {
    # -- this one is taken from the official lua-nginx-module documentation example
    # -- see https://github.com/openresty/lua-nginx-module#access_by_lua
    access_by_lua_block {
        local res = ngx.location.capture("/auth/loggedin", {body = ""})
        if res.status == ngx.HTTP_OK then
            ngx.ctx.auth_cookies = res.header["Set-Cookie"]
            return
        end
        if res.status == ngx.HTTP_FORBIDDEN then
            ngx.exit(res.status)
        end
        ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
    }

    # -- it doesn't matter where the 'proxy_pass' line actually would be, it doesn't change the workflow
    # -- see https://cloud.githubusercontent.com/assets/2137369/15272097/77d1c09e-1a37-11e6-97ef-d9767035fc3e.png
    proxy_pass http://someservice/;

    header_filter_by_lua_block {
        local function merge_cookies(a, b)
            local c = a or b
            -- if either "a" or "b" is empty, "c" already has the required result
            if (a and b) == nil then return c end
            -- neither "a" nor "b" are empty, result will be a table, "c" now equals to "a"
            -- if "c" is a string, lets made it a table instead
            if type(c) == "string" then c = {c} end
            -- append "b" to "c"
            if type(b) == "string" then table.insert(c, b) else
                for _, v in ipairs(b) do table.insert(c, v) end
            end
            return c
        end
        ngx.header["Set-Cookie"] = merge_cookies(ngx.header["Set-Cookie"], ngx.ctx.auth_cookies)
    }
}

此代码不检查可能的 cookie 名称交集,否则它将 更复杂。但是我认为(没有在实践中检查过)这并不重要,因为即使两个 Set-Cookie 请求具有相同的 cookie 名称但不同的值将返回给客户端,最后一个将被使用。此代码使来自身份验证服务器的 Set-Cookie 请求在来自主要上游的 Set-Cookie 请求之后到达。要执行相反的操作,您需要更改

ngx.header["Set-Cookie"] = merge_cookies(ngx.header["Set-Cookie"], ngx.ctx.auth_cookies)

线到

ngx.header["Set-Cookie"] = merge_cookies(ngx.ctx.auth_cookies, ngx.header["Set-Cookie"])

特别感谢 @wilsonzlin for the extremely helpful answerngx.header["Set-Cookie"] table 的合作。

更新

虽然你没有在你的问题中提到它,但看看你的例子,我看到你不仅想将 Set-Cookie header 传递给客户端,而且还想将这些 cookie 发送到someservice 上游。同样,您正试图以错误的方式进行操作。使用 proxy_set_header Cookie "$http_cookie; $auth_cookie"; 您将附加这些 cookie,包括它们的属性,如 PathMax-Age 等,而 Cookie header 应仅包含 name=value对。好吧,使用 lua-nginx-module 这也是可能的。您需要将上面的 access_by_lua_block 更改为下面的

access_by_lua_block {

    local res = ngx.location.capture("/auth/loggedin", {body = ""})
    if res.status == ngx.HTTP_OK then
        ngx.ctx.auth_cookies = res.header["Set-Cookie"]

        if ngx.ctx.auth_cookies then

            -- helper functions
            -- strip all Set-Cookie attributes, e.g. "Name=value; Path=/; Max-Age=2592000" => "Name=value"
            local function strip_attributes(cookie)
                return string.match(cookie, "[^;]+")
            end
            -- iterator for use in "for in" loop, works both with strings and tables
            local function iterate_cookies(cookies)
                local i = 0
                return function()
                    i = i + 1
                    if type(cookies) == "string" then
                        if i == 1 then return strip_attributes(cookies) end
                    elseif type(cookies) == "table" then
                        if cookies[i] then return strip_attributes(cookies[i]) end
                    end
                end
            end

            local cookies = ngx.req.get_headers()["Cookie"]
            -- at the first loop iteration separator should be an empty string if client browser send no cookies or "; " otherwise
            local separator = cookies and "; " or ""
            -- if there are no cookies in original request, make "cookies" variable an empty string instead of nil to prevent errors
            cookies = cookies or ""

            for cookie in iterate_cookies(ngx.ctx.auth_cookies) do
                cookies = cookies .. separator .. cookie
                -- next separator definitely should be a "; "
                separator = "; "
            end

            ngx.req.set_header("Cookie", cookies)

        end

        return
    end
    if res.status == ngx.HTTP_FORBIDDEN then
        ngx.exit(res.status)
    end
    ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
}

以上示例均经过测试(使用OpenResty 1.17.8.2),确认可以运行。

除了使用 lua 模块,我别无他法。这应该部分回答您的问题,它确实回答了“我如何获得所有 Set-Cookie header”的问题。

location / {
    log_by_lua_block {
        local cookies = ngx.resp.get_headers()["Set-Cookie"]
        if cookies~=nil then
            ngx.log(ngx.ERR, table.concat(cookies, ','))
        end
    }
...
}

如果您使用与 log_by_lua_block 不同的指令,我不能保证您能够看到特定的 header,而 access_by_lua_block 我不能。 如果你想把东西传递给客户,也许 log_by_lua_block 在工作流程中有点晚,也许可以尝试更早的。