使用`Cache-Control: public, s-maxage=0`可以在nginx缓存后立即过期/重新验证。

9
我希望使用HTTP代理(如nginx)来缓存大型/昂贵的请求。对于任何已授权的用户,这些资源都是相同的,但它们的身份验证/授权需要在每个请求上由后端进行检查。
听起来像是使用Cache-Control: public, max-age=0以及nginx指令proxy_cache_revalidate on;来实现这一点。代理可以缓存请求,但每个后续请求都需要进行条件GET到后端以确保经过授权后才返回缓存的资源。然后,如果用户未经授权,后端将发送403;如果用户经过授权且缓存的资源未过期,则发送304;如果资源已过期,则发送200和新资源。
在nginx中,如果设置了max-age=0,则根本不会缓存该请求。如果设置了max-age=1,那么如果我在初始请求之后等待1秒钟,那么nginx确实执行条件GET请求,但在1秒钟之前,它直接从缓存中提供它,这对需要进行身份验证的资源显然非常糟糕。
有没有办法让nginx缓存请求,但立即要求重新验证?
请注意,这在Apache中是有效的。以下是nginx和Apache的示例,前两个使用max-age=5,最后两个使用max-age=0:
# Apache with `Cache-Control: public, max-age=5`

$ while true; do curl -v http://localhost:4001/ >/dev/null 2>&1 | grep X-Cache; sleep 1; done
< X-Cache: MISS from 172.x.x.x
< X-Cache: HIT from 172.x.x.x
< X-Cache: HIT from 172.x.x.x
< X-Cache: HIT from 172.x.x.x
< X-Cache: HIT from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x
< X-Cache: HIT from 172.x.x.x

# nginx with `Cache-Control: public, max-age=5`

$ while true; do curl -v http://localhost:4000/ >/dev/null 2>&1 | grep X-Cache; sleep 1; done
< X-Cached: MISS
< X-Cached: HIT
< X-Cached: HIT
< X-Cached: HIT
< X-Cached: HIT
< X-Cached: HIT
< X-Cached: REVALIDATED
< X-Cached: HIT
< X-Cached: HIT

# Apache with `Cache-Control: public, max-age=0`
# THIS IS WHAT I WANT

$ while true; do curl -v http://localhost:4001/ >/dev/null 2>&1 | grep X-Cache; sleep 1; done
< X-Cache: MISS from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x
< X-Cache: REVALIDATE from 172.x.x.x

# nginx with `Cache-Control: public, max-age=0`

$ while true; do curl -v http://localhost:4000/ >/dev/null 2>&1 | grep X-Cache; sleep 1; done
< X-Cached: MISS
< X-Cached: MISS
< X-Cached: MISS
< X-Cached: MISS
< X-Cached: MISS
< X-Cached: MISS

正如您在前两个示例中所看到的,Apache和nginx都能够缓存请求,而且Apache可以正确地缓存max-age = 0的请求,但是nginx不能。


你能改变后端逻辑吗? - Dmitry MiksIr
很棒的问题!我认为X-Accel-Redirect就是你要找的!祝你好运! - cnst
是的,我也考虑过 X-Accel-Redirect。缺点是每个前端请求需要向后端发送两个请求。优点是 Nginx 配置简单且可以拆分后端逻辑。 - Dmitry MiksIr
资源并非静态的,但我希望将其缓存一段时间。 - tlrobinson
查询可能需要长达5分钟的计算时间,并且可能会被缓存1分钟到1小时左右,可以为数百个用户提供服务。 - tlrobinson
显示剩余5条评论
3个回答

3
我想解决一些关于使用 X-Accel-Redirect (以及如果需要兼容Apache,使用X-Sendfile) 的简单回答之后,在对话中出现的额外问题/担忧。您所寻找的“最佳”解决方案(没有X-Accel-Redirect)是不正确的,原因如下:
  1. 只需一个未经身份验证的用户请求即可清除缓存。

    • 如果每个其他请求都来自未经身份验证的用户,则你实际上根本没有缓存。

    • 任何人都可以向资源的公共URL发出请求,以使您的缓存始终保持清除。

  2. 如果提供的文件实际上是静态的,则您将浪费额外的内存、时间、磁盘和vm/缓存空间来保留每个文件的多个副本。

  3. 如果所提供的内容是动态的:

    • 执行身份验证与资源生成的成本是否相同?那么当始终需要重新验证时,您实际上通过缓存获得了什么收益?少于2x的常量因素?你不妨放弃缓存,仅仅为了打勾号,因为真正的实际改进可能是相当微不足道的。

    • 生成视图比执行身份验证的成本呈指数级增长?那么,缓存该视图并在高峰时期为成千上万的请求提供服务听起来像个好主意!但是,为了成功地实现这一点,你最好没有任何未经身份验证的用户潜伏(因为即使只有几个这样的用户也会导致不可预测的巨额开支,需要重新生成视图)。

  4. 在各种边缘情况下,缓存会发生什么情况?如果用户被拒绝访问而开发人员未使用适当的代码,则会将其缓存怎么办?如果下一个管理员决定调整一些设置,例如proxy_cache_use_stale?突然间,未经身份验证的用户就会收到机密信息。您正在通过不必要地将应用程序的独立部分结合在一起,留下各种缓存污染攻击向量。

  5. 我认为对于需要身份验证的页面返回 Cache-Control: public, max-age=0 不是技术上正确的。我认为正确的响应可能是must-revalidateprivate替换public

nginx设计中故意缺少对于立即重验证的支持,例如max-age=0,这被认为是一种“不足之处”(类似于其不支持.htaccess)。 基于以上观点,立即要求重验证给给定资源带来的意义很小,这只是一种无法扩展的方法,特别是当你需要处理每秒“荒谬”的请求数量时,必须使用最小的资源,并且毫不含糊地满足所有请求。 如果您需要一个由“委员会”设计的Web服务器,具有每个厨房水槽应用程序和任何RFC的每个可疑部分的向后兼容性,则nginx显然不是正确的解决方案。
另一方面,X-Accel-Redirect非常简单,易于操作,也是事实上的标准。它可以让您以非常整洁的方式将内容与访问控制分开。它真的很简单。它确保您的内容将被缓存,而不是您的缓存被随意清除。这是值得追求的“正确”解决方案。试图在高峰期每10K服务时避免“额外”的请求,代价是在第一次不需要缓存时只有“一个”请求,并且当10K个请求到达时实际上没有缓存,这不是设计可扩展架构的正确方法。

感谢您的回答。这些请求可能是对数据仓库的昂贵查询,计算时间可能长达几分钟,并且可能需要缓存1-60分钟。认证成本可以忽略不计。 - tlrobinson
我不太清楚在这种情况下如何使用 X-Accel-Redirect。如果是非静态资源,它会如何工作? - tlrobinson
@tlrobinson,你可以通过使用proxy_set_header使nginx(以及X-Accel-Redirect)可检测;随后,向/cache/ location发出X-Accel-Redirect,并将其定义为internal;在/cache/中,您可以使用rewrite重写$uri以删除/cache部分,设置缓存选项,并进行proxy_pass,这将被缓存,并且您必须确保结果是可共享的(否则返回非200代码,并确保不会被缓存/共享)。请注意,在location /cache/之外,没有任何内容会被缓存,但也不会生成视图。 - cnst

0

我认为你最好的选择是修改你的后端以支持X-Accel-Redirect

它的功能默认启用,并在proxy_ignore_headers的文档中有描述:

“X-Accel-Redirect”对指定的URI执行内部重定向;

然后,您将缓存该内部资源,并自动为任何已经过身份验证的用户返回它。

由于重定向必须是internal的,因此没有其他方式可以访问它(例如,没有某种内部重定向),因此根据您的要求,未经授权的用户将无法访问它,但它仍然可以像任何其他location一样被缓存。


如果您需要同时支持nginx和apache,并通过其他方式运行应用程序,请查看https://dev59.com/YG865IYBdhLWcg3wCp9A,了解有关如何在发送整个内容的情况下使用`X-Accel-Redirect`和`X-Sendfile`的更多信息以及向后兼容性。 - cnst

0
如果您无法按建议修改后端应用程序,或者认证方式很简单,例如基本身份验证,另一种方法是在Nginx中进行认证。
实施此认证过程并定义缓存有效期将是您需要做的全部工作,而Nginx将根据以下流程图处理其余部分。 Nginx伪代码流程:
If (user = unauthorised) then
    Nginx declines request;
else
    if (cache = stale) then
        Nginx gets resource from backend;
        Nginx caches resource;
        Nginx serves resource;
    else 
        Nginx gets resource from cache;
        Nginx serves resource;
    end if
end if

缺点是根据您拥有的身份验证类型,您可能需要类似Nginx Lua模块来处理逻辑。

编辑

看到了额外的讨论和提供的信息。现在,虽然不完全了解后端应用程序的工作方式,但查看用户anki-code在GitHub上提供的示例配置(您在此处进行了评论),下面的配置将避免您提出的问题,即之前缓存的资源未运行后端应用程序的身份验证/授权检查。

我假设后端应用程序为未经身份验证的用户返回HTTP 403代码。我还假设您已经安装了Nginx Lua模块,因为GitHub配置依赖于此,尽管我注意到您测试的部分不需要该模块。

配置:

server {
    listen 80;
    listen [::]:80;
    server_name 127.0.0.1;
    
    location / {
        proxy_pass http://127.0.0.1:3000; # Metabase here
    }
    location ~ /api/card((?!/42/|/41/)/[0-9]*/)query {
        access_by_lua_block {
            -- HEAD request to a location excluded from caching to authenticate
            res = ngx.location.capture( "/api/card/42/query", { method = ngx.HTTP_HEAD } )
            if res.status = 403 then
                return ngx.exit(ngx.HTTP_FORBIDDEN)
            else
                ngx.exec("@metabase")
            end if
        }
    }
    
    location @metabase {
        # cache all cards data without card 42 and card 41 (they have realtime data)
        if ($http_referer !~ /dash/){ 
            #cache only cards on dashboard
            set $no_cache 1;
        }
        proxy_no_cache $no_cache;
        proxy_cache_bypass $no_cache;
        proxy_pass http://127.0.0.1:3000;
        proxy_cache_methods POST;
        proxy_cache_valid 8h;
        proxy_ignore_headers Cache-Control Expires;
        proxy_cache cache_all;
        proxy_cache_key "$request_uri|$request_body";
        proxy_buffers 8 32k;
        proxy_buffer_size 64k;
        add_header X-MBCache $upstream_cache_status;
    }
    location ~ /api/card/\d+ {
        proxy_pass http://127.0.0.1:3000;
        if ($request_method ~ PUT) {
            # when the card was edited reset the cache for this card
            access_by_lua 'os.execute("find /var/cache/nginx -type f -exec grep -q \\"".. ngx.var.request_uri .."/\\"  {} \\\; -delete ")';
            add_header X-MBCache REMOVED;
        }
    }
}

有了这个,我期望使用$ curl 'http://localhost:3001/api/card/1/query'进行的测试将如下运行:

第一次运行(需要Cookie)

  1. 请求命中location ~ /api/card((?!/42/|/41/)/[0-9]*/)query
  2. 在Nginx访问阶段,向/api/card/42/query发出“HEAD”子请求。该位置在给定的配置中被排除在缓存之外。
  3. 后端应用程序返回非403等响应,因为用户已经通过身份验证。
  4. 然后发出一个子请求到@metabase命名位置块,处理实际请求并将内容返回给用户。

第二次运行(不需要Cookie)

  1. 请求命中 location ~ /api/card((?!/42/|/41/)/[0-9]*/)query
  2. 在 Nginx 访问阶段,向后端发出一个 "HEAD" 子请求,路径为 /api/card/42/query
  3. 后端应用返回 403 Forbidden 响应,因为用户未经身份验证。
  4. 用户的客户端收到 403 Forbidden 响应。

如果资源密集型,可以考虑创建一个简单的卡查询,仅用于进行身份验证,而不是使用 /api/card/42/query

这似乎是一种简单直接的方法。后端保持原样,无需进行任何调整,您只需在 Nginx 中配置缓存细节。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接