Lua是一個(gè)可以嵌入到Nginx配置文件中的動(dòng)態(tài)腳本語言,從而可以在Nginx請求處理的任何階段執(zhí)行各種Lua代碼。剛開始我們只是用Lua 把請求路由到后端服務(wù)器,但是它對我們架構(gòu)的作用超出了我們的預(yù)期。下面就講講我們所做的工作。

強(qiáng)制搜索引擎只索引mixlr.com:
Google把子域名當(dāng)作完全獨(dú)立的網(wǎng)站,我們不希望爬蟲抓取子域名的頁面,降低我們的Page rank。
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
';
如果對robots.txt的請求不是mixlr.com域名的話,則內(nèi)部重寫到robots_diallow.txt,雖然標(biāo)準(zhǔn)的重寫指令也可以實(shí)現(xiàn)這個(gè)需求,但是 Lua的實(shí)現(xiàn)更容易理解和維護(hù)。
根據(jù)程序邏輯設(shè)置響應(yīng)頭:
Lua提供了比Nginx默認(rèn)配置規(guī)則更加靈活的設(shè)置方式。 在下面的例子中,我們要保證正確設(shè)置響應(yīng)頭,這樣瀏覽器如果發(fā)送了指定請求頭后,就可以 無限期緩存靜態(tài)文件,是的用戶只需下載一次即可。 這個(gè)重寫規(guī)則使得任何靜態(tài)文件,如果請求參數(shù)中包含時(shí)間戳值,那么就設(shè)置相應(yīng)的Expires和Cache-Control響應(yīng)頭。
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請求的時(shí)間戳參數(shù):
很多外部客戶端請求JSONP接口時(shí),都會(huì)包含一個(gè)時(shí)間戳類似的參數(shù),從而導(dǎo)致Nginx proxy緩存無法命中(因?yàn)闊o法忽略指定的HTTP參數(shù))。下面的 規(guī)則刪除了時(shí)間戳參數(shù),使得Nginx可以緩存upstream server的響應(yīng)內(nèi)容,減輕后端服務(wù)器的負(fù)載。
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的錯(cuò)誤日志:
如果后端請求響應(yīng)很慢,可以把它記錄到Nginx的錯(cuò)誤日志,以備后續(xù)追查。
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的實(shí)時(shí)IP封禁:
某些情況下,需要阻止流氓爬蟲的抓取,這可以通過專門的封禁設(shè)備去做,但是通過Lua,也可以實(shí)現(xiàn)簡單版本的封禁。
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
';}
現(xiàn)在就可以阻止特定IP的訪問:
ruby> $redis.sadd("banned_ips","200.1.35.4")
Nginx進(jìn)程每隔10秒從Redis獲取一次最新的禁止IP名單。需要注意的是,如果架構(gòu)中使用了Haproxy這樣類似的負(fù)載均衡服務(wù)器時(shí), 需要把$remote_addr設(shè)置為正確的遠(yuǎn)端IP地址。
這個(gè)方法還可以用于HTTP User-Agent字段的檢查,要求滿足指定條件。
使用Nginx輸出CSRF(form_authenticity_token)
Mixlr大量使用頁面緩存,由此引入的一個(gè)問題是如何給每個(gè)頁面輸出會(huì)話級別的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:
classCsrfTokenEndpointdefself.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
endend