[译]Nginx和Lua

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 这几个月里,我们逐步把Lua集成到Mixlr的前端Nginx配置中。 Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。

这几个月里,我们逐步把Lua集成到Mixlr的前端Nginx配置中。

Lua是一个可以嵌入到Nginx配置文件中的动态脚本语言,从而可以在Nginx请求处理的任何阶段执行各种Lua代码。刚开始我们只是用Lua 把请求路由到后端服务器,但是它对我们架构的作用超出了我们的预期。下面就讲讲我们所做的工作。

强制搜索引擎只索引mixlr.com

Google把子域名当作完全独立的网站,我们不希望爬虫抓取子域名的页面,降低我们的Page rank。

location /robots.txt {
  rewrite_by_lua '
    if ngx.var.http_host ~= "mixlr.com" then
      return ngx.exec("/robots_disallow.txt");
    end
  ';
}


如果对robots.txt的请求不是mixlr.com域名的话,则内部重写到robots_diallow.txt,虽然标准的重写指令也可以实现这个需求,但是 Lua的实现更容易理解和维护。

根据程序逻辑设置响应头

Lua提供了比Nginx默认配置规则更加灵活的设置方式。 在下面的例子中,我们要保证正确设置响应头,这样浏览器如果发送了指定请求头后,就可以 无限期缓存静态文件,是的用户只需下载一次即可。

这个重写规则使得任何静态文件,如果请求参数中包含时间戳值,那么就设置相应的Expires和Cache-Control响应头。

location / {
  header_filter_by_lua '
    if ngx.var.query_string and ngx.re.match( ngx.var.query_string, "^([0-9]{10})$" ) then
      ngx.header["Expires"] = ngx.http_time( ngx.time() + 31536000 );
      ngx.header["Cache-Control"] = "max-age=31536000";
    end
  ';

  try_files $uri @dynamic;
}


删除jQuery JSONP请求的时间戳参数

很多外部客户端请求JSONP接口时,都会包含一个时间戳类似的参数,从而导致Nginx proxy缓存无法命中(因为无法忽略指定的HTTP参数)。下面的 规则删除了时间戳参数,使得Nginx可以缓存upstream server的响应内容,减轻后端服务器的负载。

location / {
  rewrite_by_lua '
    if ngx.var.args ~= nil then
      -- /some_request?_=1346491660 becomes /some_request
      local fixed_args, count = ngx.re.sub( ngx.var.args, "&?_=[0-9]+", "" );
      if count > 0 then
        return ngx.exec(ngx.var.uri, fixed_args);
      end
    end
  ';
}


把后端的慢请求日志记录到Nginx的错误日志

如果后端请求响应很慢,可以把它记录到Nginx的错误日志,以备后续追查。

location / {
  log_by_lua '
    if tonumber(ngx.var.upstream_response_time) >= 1 then
      ngx.log(ngx.WARN, "[SLOW] Ngx upstream response time: " .. ngx.var.upstream_response_time .. "s from " .. ngx.var.upstream_addr);
    end
  ';
}


基于Redis的实时IP封禁

某些情况下,需要阻止流氓爬虫的抓取,这可以通过专门的封禁设备去做,但是通过Lua,也可以实现简单版本的封禁。

lua_shared_dict banned_ips 1m;

location / {
  access_by_lua '
    local banned_ips = ngx.shared.banned_ips;
    local updated_at = banned_ips:get("updated_at");

    -- only update banned_ips from Redis once every ten seconds:
    if updated_at == nil or updated_at < ( ngx.now() - 10 ) then
      local redis = require "resty.redis";
      local red = redis:new();
      red:set_timeout(200);

      local ok, err = red:connect("your-redis-hostname", 6379);
      if not ok then
        ngx.log(ngx.WARN, "Redis connection error retrieving banned_ips: " .. err);
      else
        local updated_banned_ips, err = red:smembers("banned_ips");
        if err then
          ngx.log(ngx.WARN, "Redis read error retrieving banned_ips: " .. err);
        else
          -- replace the locally stored banned_ips with the updated values:
          banned_ips:flush_all();
          for index, banned_ip in ipairs(updated_banned_ips) do
            banned_ips:set(banned_ip, true);
          end
          banned_ips:set("updated_at", ngx.now());
        end
      end
    end

    if banned_ips:get(ngx.var.remote_addr) then
      ngx.log(ngx.WARN, "Banned IP detected and refused access: " .. ngx.var.remote_addr);
      return ngx.exit(ngx.HTTP_FORBIDDEN);
    end
  ';
}


现在就可以阻止特定IP的访问:
ruby> $redis.sadd("banned_ips", "200.1.35.4")


Nginx进程每隔10秒从Redis获取一次最新的禁止IP名单。需要注意的是,如果架构中使用了Haproxy这样类似的负载均衡服务器时, 需要把$remote_addr设置为正确的远端IP地址。

这个方法还可以用于HTTP User-Agent字段的检查,要求满足指定条件。

使用Nginx输出CSRF(form_authenticity_token)

Mixlr大量使用页面缓存,由此引入的一个问题是如何给每个页面输出会话级别的CSRF token。我们通过Nginx的子请求,从upstream web server 获取token,然后利用Nginx的SSI(server-side include)功能输出到页面中。这样既解决了CSRF攻击问题,也保证了cache能被正常利用。

location /csrf_token_endpoint {
  internal;
  
  include /opt/nginx/conf/proxy.conf;
  proxy_pass "http://upstream";
}

location @dynamic {
  ssi on;
  set $csrf_token '';

  rewrite_by_lua '
    -- Using a subrequest, we our upstream servers for the CSRF token for this session:
    local csrf_capture = ngx.location.capture("/csrf_token_endpoint");
    if csrf_capture.status == 200 then
      ngx.var.csrf_token = csrf_capture.body;

      -- if this is a new session, ensure it sticks by passing through the new session_id
      -- to both the subsequent upstream request, and the response:
      if not ngx.var.cookie_session then
        local match = ngx.re.match(csrf_capture.header["Set-Cookie"], "session=([a-zA-Z0-9_+=/+]+);");
        if match then
          ngx.req.set_header("Cookie", "session=" .. match[1]);
          ngx.header["Set-Cookie"] = csrf_capture.header["Set-Cookie"];
        end
      end
    else
      ngx.log(ngx.WARN, "No CSRF token returned from upstream, ignoring.");
    end
  ';

  try_files /maintenance.html /rails_cache$uri @thin;
}


CSRF token生成 app/metal/csrf_token_endpoint.rb:

class CsrfTokenEndpoint
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/csrf_token_endpoint/
      session = env["rack.session"] || {}

      token = session[:_csrf_token]
      if token.nil?
        token = SecureRandom.base64(32)
        session[:_csrf_token] = token
      end

      [ 200, { "Content-Type" => "text/plain" }, [ token ] ]
    else     
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end


我们的模版文件示例:

<meta name="csrf-param" value="authenticity_token"/>

<meta name="csrf-token" value="<!--# echo var="csrf_token" default="" encoding="none" -->"/>

Again you could make use of lua_shared_dict to store in memory the CSRF token for a particular session. This minimises the number of trips made to /csrf_token_endpoint.

 

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
存储 缓存 Java
Openresty(lua+nginx)-Guava-Redis做多级缓存
Openresty(lua+nginx)-Guava-Redis做多级缓存
73 1
浅谈基于openresty(nginx+lua)开发轻量级,按流量控制的灰度模块(下)
浅谈基于openresty(nginx+lua)开发轻量级,按流量控制的灰度模块
146 0
|
3月前
|
应用服务中间件 nginx
[nginx]lua读取请求体
[nginx]lua读取请求体
|
3月前
|
缓存 应用服务中间件 nginx
[nginx]lua控制响应头
[nginx]lua控制响应头
106 0
|
3月前
|
应用服务中间件 API nginx
[nginx]lua控制请求头
[nginx]lua控制请求头
|
消息中间件 NoSQL 关系型数据库
Linux安装 OpenResty、Nginx、PHP、Mysql、Redis、Lua、Node、Golang、MongoDB、Kafka等
Linux安装 OpenResty、Nginx、PHP、Mysql、Redis、Lua、Node、Golang、MongoDB、Kafka等
175 0
|
6月前
|
NoSQL 关系型数据库 应用服务中间件
Linux安装 OpenResty、Nginx、PHP、Mysql、Redis、Lua、Node、Golang、MongoDB、Kafka等
Linux安装 OpenResty、Nginx、PHP、Mysql、Redis、Lua、Node、Golang、MongoDB、Kafka等
172 0
分布式接口幂等性、分布式限流(Guava 、nginx和lua限流)
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。
|
缓存 NoSQL 应用服务中间件
基于Nginx+Lua优化多级缓存架构
基于Nginx+Lua优化多级缓存架构
167 0
|
负载均衡 应用服务中间件 测试技术
浅谈基于openresty(nginx+lua)开发轻量级,按流量控制的灰度模块(上)
浅谈基于openresty(nginx+lua)开发轻量级,按流量控制的灰度模块
440 0