CDN+OpenResty 实现丝滑访问的登录态缓存站

CDN+OpenResty 实现丝滑访问的登录态缓存站

去年在博客上分享过用Nginx反向代理实现的zsxq登录态网站,但是实际体验并不友好。

  1. 第一次访问加载js时间非常长
  2. 服务端接口故意返回报错,导致页面空白
  3. 多人访问导致Nginx反向代理替换大文件字符串效率低下
  4. Nginx部署在境外,国内访问速度很慢

而上面出现的问题,实际上都可以通过CDN缓存加速的方式解决。

腾讯云CDN配置

CDN如果要对境内加速的话,是要求加速的域名备案的。

备案过的域名

我自己的域名备案过期了,找朋友借一个备案过的域名作为子域名。

CDN控制台配置

在控制台添加一个要加上的域名。

配置都选默认配置即可,然后就是为要加速的域名添加CNAME记录。

配置完成后,通过CDN加速的域名访问Nginx,速度已经有了明显的提升,所有的静态资源js、css都被CDN缓存了。

配置CDN https(可选)

在腾讯云控制台申请一个域名免费的ssl证书。

这样即使源站是http请求也无所谓,在CDN这一层配置https即可。

顺便开启302强制跳转。

开启OpenResty自带的缓存功能

OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。OpenResty 可以理解为Nginx的升级版。

先在http中配置全局的proxy_cache_path。如果想要在log中显示当前请求是否命中Nginx的缓存需要修改日志格式如下。

    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    log_format main      '$remote_addr - $remote_user [$time_local] "$request" '
                         '$status $body_bytes_sent "$http_referer" '
                         '"$http_user_agent" "$upstream_cache_status" '
                         '$request_time $upstream_response_time $pipe';

然后在Location中指定proxy_cache的名字。

        location /api/ {
                proxy_pass https://api.xxxx.com/;

                proxy_cache my_cache;
                proxy_cache_valid 200 302 24h;
                proxy_cache_valid 404 1m;
        }

然后重启Nginx即可。但是会发现Nginx确实也缓存了js、css等静态资源。但是却不会缓存接口的数据?

查看日志中请求是否有命中缓存。

为什么不会缓存接口数据?

CDN和Nginx同样不会对接口数据缓存,通过对header的逐一调试,发现当服务端当存在Cache-Control、Expires 、Set-Cookie等header时,中间件就不会进行缓存。

进一步修改配置文件如下:

        location /api/ {
                proxy_pass https://api.xxxx.com/;

                proxy_cache my_cache;
                proxy_cache_valid 200 302 24h;
                proxy_cache_valid 404 1m;

                proxy_hide_header Set-Cookie;
                proxy_hide_header Expires;
                proxy_hide_header X-Expire-In;
                proxy_ignore_headers Cache-Control Expires Set-Cookie ; 
        }
  • proxy_hide_header:用于隐藏指定的响应头,使其不传递给客户端。
  • proxy_ignore_headers:用于忽略指定的响应头,使其对 Nginx 的缓存逻辑无效,但仍然可以传递给客户端。

因为Nginx前面还有CDN所以需要proxy_hide_header和proxy_ignore_headers配合使用。

接口报错数据也被缓存?

由于zsxq时不时返回一个1059的接口报错返回,导致错误的数据被OpenResty和CDN缓存,这是非常致命的问题。如何让OpenResty判断服务端返回的数据是否存在错误状态码?有的话就不缓存错误数据。

这个问题我思考了好多天,尝试过很多种方式。比如通过先判断body中是否包含特定的状态码,再决定是否在header中添加Cache-Control使其不缓存错误数据。但是Nginx在返回数据时是先返回header,再返回body内容。这就导致Nginx获取到body内容后,header已经返回给客户端了,就无法成功修改header。

我甚至动了手动改造Nginx源码的念头,最终还是忍住了。

下面是我曲线救国的配置实现,思路非常巧妙。

            header_filter_by_lua_block {
                if ngx.var.upstream_cache_status == "HIT" then
                    ngx.log(ngx.ERR, "Cache hit, removing Cache-Control header for request: ", ngx.var.request_uri)
                    ngx.header["Cache-Control"] = nil
                end
            }

            body_filter_by_lua_block {
                local chunk = ngx.arg[1]
                local eof = ngx.arg[2]

                if ngx.ctx.response_body == nil then
                    ngx.ctx.response_body = chunk
                else
                    ngx.ctx.response_body = ngx.ctx.response_body .. chunk
                end

                if eof then
                    local body = ngx.ctx.response_body
                    local body_length = #body

                if body_length < 512 then
                        local json = require "cjson"
                        local data = json.decode(body)

                if data and data.code == 1059 then
                    local upstream_scheme = "https"
                    local upstream_host = "api.xxxx.com"
                    local new_url = ngx.var.request_uri:gsub("^/api", "")
                    local upstream_url = upstream_scheme .. "://" .. upstream_host .. new_url

                        local cache_key = ngx.md5(upstream_url)
                        local cache_dir = "/var/cache/nginx/"
                        local subdir1 = string.sub(cache_key, -1)
                        local subdir2 = string.sub(cache_key, -3, -2)
                        local cache_file_path = cache_dir .. subdir1 .. "/" .. subdir2 .. "/" .. cache_key

                            local res = os.remove(cache_file_path)
                            if res then
                                ngx.log(ngx.ERR, "Cache for ", ngx.var.request_uri, " has been purged due to error code 1059")
                            else
                                ngx.log(ngx.ERR, "Failed to purge cache for ",cache_file_path, " ", upstream_url)
                            end
                        end
                    end
                end
            }

先判断body中是否存在错误状态码,如果有的话就删除对应的缓存文件。这样下一次请求同意的url,由于缓存文件不存在,同样不会命中缓存。但是第二次接口返回正常数据时,就不会触发删除规则,就能正常被CDN给缓存,极为巧妙。

如何解决Nginx 401 认证被CDN缓存绕过?

        location / {
                        auth_basic "zsxq";
                        auth_basic_user_file /etc/nginx/htpasswd.txt;

                        if ($uri = '/'){
                                return 301 https://zsxq.xxxxx.top/dweb2/index/group/xxxxxxx;
                        }
        }

上面的配置虽然对 / 配置了密码认证,同时对url进行了跳转。但是经过CDN缓存后401认证就失效了。这是因为接口数据都被静态缓存了就不会弹窗认证了。

需要重新配置要认证的页面不经过CDN缓存。对于页面的URL需要单独配置认证。

        location ~ ^/dweb2/index/group/\d+$ {
                auth_basic "zsxq";
                auth_basic_user_file /etc/nginx/htpasswd.txt;

                proxy_pass https://wx.xxxx.com;

                add_header Cache-Control no-store;
        }

通过add_header Cache-Control no-store;就能成功配置当前路由不被CDN缓存,正常弹窗认证。

赞赏

微信赞赏支付宝赞赏

Zgao

愿有一日,安全圈的师傅们都能用上Zgao写的工具。

发表评论