運維實踐-最新Nginx二進位制構建編譯lua-nginx-module動態連結Lua指令碼存取Redis資料庫讀取靜態資源隱式展現

2022-08-06 18:01:04

關注「WeiyiGeek」公眾號

設為「特別關注」每天帶你玩轉網路安全運維、應用開發、物聯網IOT學習!

希望各位看友【關注、點贊、評論、收藏、投幣】,助力每一個夢想。


本章目錄


首發地址: https://mp.weixin.qq.com/s/u-zb-BxG6VyaLY4EQLKlOQ


0x0n 前言簡述

為啥有此篇文章?
描述: 在進行公司的圖片儲存解決方案研究中,最開始準備使用的是FastDFS,但是經過深思熟慮,以及後期運維成本考慮還是放棄了,只能轉而使用儲存直接存放圖片檔案,直接請求效率提示槓槓的,但如何最大限度保證業務安全以及減少業務對資料庫增刪改查的壓力? 在 Google 、Github一番查詢後發現可以直接使用 Nginx + Lua 進行存取資料進行獲取靜態資源資訊,而不用業務系統進行存取資料庫直接獲取靜態資源路徑,而顯式的展現資源真實暴露給外部,非常容易被批次抓取。

其次筆者在實踐中發現當前搜尋到的安裝部署Nginx+Lua可能已將不適用最新穩定版本的Nginx版本,基本上都是 1.15.x ~ 1.18.x,對於當前Nginx 1.22.0 版本來說顯然是太老了。
所以本章就該問題進行 Nginx + Lua + Redis 模組環境的安裝以及簡單的實踐,希望能幫助到各位有相同需求的Person。


基礎知識:

  • Nginx: 是一個高效能的HTTP和反向代理web伺服器,同時也提供了IMAP/POP3/SMTP服務, 其三大核心功能,包含靜態資源、反向代理、api模組擴充套件,對於lua指令碼的擴充套件,例如由lua-nginx-module模組,就是api模組擴充套件的一部分,並且nginx可以通過lua指令碼直接呼叫redis伺服器;
  • Lua: 是一種功能強大,高效,輕量級,可嵌入的指令碼語言,非常容易嵌入到我們應用程式中, 它用於各種應用程式,從遊戲到Web應用程式和影象處理。
  • lua-nginx-module : 該模組是 OpenResty 的核心元件,目錄是將lua的功能嵌入到Nginx http伺服器中。
  • lua-resty-redis : 該模組是在 OpenResty 專案下基於 cosocket API 的 ngx_lua 的 Lua redis 使用者端驅動。

溫馨提示: 如果不是現有業務大量使用Nginx進行承載不能直接替換其它優秀的解決方案,只能一步一步來,從而轉入 OpenResty 或者 caddy 搭建能夠處理超高並行、擴充套件性極高的動態 Web 應用、Web 服務和動態閘道器。

原文地址: https://blog.weiyigeek.top


知識引入

Nginx 的指令的都是安裝執行順序的嗎?

答: 既然我都這樣問了答案則是顯而易見的,這也是大多數新手頻繁遇到的一個困惑,當然也困惑了筆者,否則我也不會這樣問。

那我們下來來看這麼一個範例: (驗證此範例你可能需要先按照下一章的【0x01 部署環境】進行準備相關環境), 此時你可能會說輸出不就是WeiyiGeek嗎?

location /sequence_demo_1 {
 set $a Weiyi;
 echo $a;
 set $a Geek;
 echo $a;
}

但如果請求該URL你會發現實時並非如此。

$ curl http://demo.weiyigeek.top/sequence_demo_1
Geek
Geek

那為什麼出現了這種不合常理的現象呢?

答: 為了解釋此現象, 我們不得不介紹Nginx的請求處理的11階段,分別是post-read、server-rewrite、find-config、rewrite、post-rewrite、preaccess、access、post-access、precontent、content以及log,其中3個比較常見的按照執行時的先後順序依次是rewrite階段、access階段以及content階段。
Nginx 設定指令一般只會註冊並執行在其中的某一個處理階段,比如 set 指令就是在rewrite階段執行的,而echo指令只會在content階段執行, 在一次請求處理流程中rewrite階段總是在content階段之前執行。

因此,屬於rewrite階段的設定指令(範例中的set)總是會無條件地在content階段的設定指令(範例中的echo)之前執行,即便是echo指令出現在set指令的前面, 上面例子中的指令按照請求處理階段的先後次序排序,實際的執行次序如下:

location /sequence_demo_1 {
 # rewrite階段的設定指令,執行在前面
 set $a Weiyi;
 set $a Geek ;
 # content階段的設定指令,執行在後面
 echo $a;
 echo $a;
}

所以,輸出的結果就是Weiyi Geek了。


Lua模組指令階段

各階段使用Lua模組指令
描述: 由於本章 Nginx 也是使用 OpenResty Lua 模組實現的解析Lua指令碼,所以其指令我們也需要做一個簡單瞭解,對於後續學習有非常大的幫助。

指令語法: https://github.com/openresty/lua-nginx-module

使用Lua來構建nginx指令碼就是通過一條條指令來完成的,指令常用於指定 Lua 程式碼是什麼時候執行的以及如何使用執行的結果,lua 指令分為設定指令、控制指令, 而控制指令分為兩種方式。

  • lua指令碼塊 :*_by_lua_block
  • lua指令碼檔案 :*_by_lua_file

下圖展示了指令執行的順序:從上至下:初始化、重寫/存取、內容處理、紀錄檔輸出四個階段


lua-nginx-module Directives Document(Lua Nginx 模組指令檔案):


值得注意的是Nginx可以提前終止請求(至少),這意味著跳過正常執行的階段,例如重寫或存取階段。這也意味著,不管執行的後期階段(例如log_by_lua)將無法存取通常在這些階段中設定的資訊。

400 (Bad Request)
405 (Not Allowed)
408 (Request Timeout)
413 (Request Entity Too Large)
414 (Request URI Too Large)
494 (Request Headers Too Large)
499 (Client Closed Request)
500 (Internal Server Error)
501 (Not Implemented)

好了,此處就只是先簡單點一下,在後續實踐中您在回過頭來看即可。


0x01 部署環境

安裝說明

環境描述:

# 系統資訊
$ cat /etc/issue.net
Ubuntu 20.04.3 LTS
$ uname -a
Linux weiyigeek.top 5.4.0-92-generic \#103-Ubuntu SMP Fri Nov 26 16:13:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

# 軟體版本
Nginx - 1.22.0  (stable 版本) 
pcre - 8.45
zlib - 1.2.12
Lua - 5.4
openssl - 1.1.1q
ngx_devel_kit - v0.3.1
lua-nginx-module - v0.10.21
echo-nginx-module - v0.62
lua-resty-core - v0.1.23
lua-resty-lrucache - v0.13
lua-resty-redis - v0.29

溫馨提示: 此處使用的是 Ubuntu 20.04 作業系統, 該系統已做安全加固和核心優化符合等保2.0要求【SecOpsDev/Ubuntu-InitializeSecurity.sh at master · WeiyiGeek/SecOpsDev 】, 如你的Linux未進行相應設定環境可能與讀者有些許差異, 如需要進行(windows server、Ubuntu、CentOS)安全加固請參照如下加固指令碼進行加固, 請大家瘋狂的 star 。
加固指令碼地址:【 https://github.com/WeiyiGeek/SecOpsDev/blob/master/OS-作業系統/Linux/Ubuntu/Ubuntu-InitializeSecurity.sh

為了節省大家的實踐時間,我已經把需要用到的原始碼包上傳到空間中,有需要的朋友可以看一下,下載地址: [http://share.weiyigeek.top/d/36158960-50338508-7c5982?p=2088](http://share.weiyigeek.top/d/36158960-50338508-7c5982?p=2088)(存取密碼:2088)
溫馨提示: 如提示證書不對,請點選高階繼續存取即可.


安裝部署

原始碼編譯構建
Step 1.在 Ubuntu 20.04 LTS 系統安裝編譯所需環境.

apt install -y gcc g++ make perl net-tools

Step 2.下載 Nginx、PCRE、zlib、OpenSSL 原始碼包,並編譯構建 PCRE、zlib、OpenSSL.

cd /usr/local/src
# Nginx 輕量級的Web代理伺服器。
# 官網: https://nginx.org/en/download.html
wget -c https://nginx.org/download/nginx-1.22.0.tar.gz -O /usr/local/src/nginx-1.22.0.tar.gz
tar -zxf nginx-1.22.0.tar.gz

# PCRE – 支援正規表示式,NGINX Core 和 Rewrite 模組需要
# 官網: http://pcre.org/
wget -c https://nchc.dl.sourceforge.net/project/pcre/pcre/8.45/pcre-8.45.tar.bz2
tar -jxf pcre-8.45.tar.bz2 && cd pcre-8.45
./configure
make && sudo make install

# zlib – 支援檔頭壓縮, NGINX Gzip 模組需要。
# 官網:http://www.zlib.net/
wget -c http://www.zlib.net/zlib-1.2.12.tar.gz
tar -zxf zlib-1.2.12.tar.gz && cd zlib-1.2.12
./configure
make && sudo make install

# OpenSSL – 支援 HTTPS 協定, NGINX SSL 模組和其他模組需要。
# 官網: https://www.openssl.org/source/
wget -c https://www.openssl.org/source/openssl-1.1.1q.tar.gz
tar -zxf openssl-1.1.1q.tar.gz && cd openssl-1.1.1q
./config --prefix=/usr/local/openssl
make && sudo make install
ln -s /usr/local/openssl/bin/openssl /usr/local/bin/openssl
# lib 庫載入到系統
echo "/usr/local/openssl/lib" >> /etc/ld.so.conf.d/libc.conf
ldconfig
# 執行命令驗證系統的 OpenSSL 版本
/usr/local/bin/openssl version
OpenSSL 1.1.1q 5 Jul 2022

溫馨提示: 如./configure未指定--prefix引數的將會直接安裝在/usr/local目錄下的bin、lib、share等子目錄中。


Step 3.下載編譯構建Lua解析器以及Nginx所需的開發工具包和Lua模組。

cd /usr/local/src
# ngx_devel_kit - 是Nginx開發工具包,實際上可以看做一個Nginx模組,它新增了額外的通用工具,模組開發人員可以在自己的模組中使用這些工具。
# 專案地址: https://github.com/simpl/ngx_devel_kit
# 專案地址: https://github.com/vision5/ngx_devel_kit
wget -c https://github.com/vision5/ngx_devel_kit/archive/refs/tags/v0.3.1.tar.gz -O ngx_devel_kit-v0.3.1.tar.gz
tar -zxf ngx_devel_kit-v0.3.1.tar.gz && ls ngx_devel_kit-0.3.1
  # auto  config  docs  examples  LICENSE  ngx_auto_lib_core  notes  objs  patches  README_AUTO_LIB.md  README.md  src

# lua-nginx-module - 將Lua的強大功能嵌入到NGINX HTTP伺服器中
# 專案地址: https://github.com/openresty/lua-nginx-module
wget -c https://github.com/openresty/lua-nginx-module/archive/refs/tags/v0.10.21.tar.gz -O /usr/local/src/lua-nginx-module-v0.10.21.tar.gz
tar -zxf lua-nginx-module-v0.10.21.tar.gz && ls lua-nginx-module-0.10.21
  # config  doc  dtrace  misc  README.markdown  src  t  tapset  util  valgrind.suppress

# echo-nginx-module - 一個Nginx的輸出模組,用於將「echo」、「sleep」、「time」等功能引入Nginx的組態檔, 此模組不隨Nginx源一起分發。
# 專案地址: https://github.com/openresty/echo-nginx-module
wget --no-check-certificate -c https://github.com/openresty/echo-nginx-module/archive/refs/tags/v0.62.tar.gz -O /usr/local/src/echo-nginx-module-v0.62.tar.gz
tar -zxf echo-nginx-module-v0.62.tar.gz && ls echo-nginx-module-0.62
  # config  LICENSE  README.markdown  src  t  util  valgrind.suppress

# luajit2 - lua 解析器 LuaJIT 2 OpenResty 的分支,且注意解析器的Lua版本為5.1
# 專案地址: https://github.com/openresty/luajit2 
wget -c  https://github.com/openresty/luajit2/archive/refs/tags/v2.1-20220411.tar.gz -O /usr/local/src/luajit2-v2.1-20220411.tar.gz
tar -zxvf luajit2-v2.1-20220411.tar.gz && cd luajit2-2.1-20220411
make PREFIX=/usr/local/luajit && make install PREFIX=/usr/local/luajit
ln -s /usr/local/luajit/bin/luajit /usr/local/bin/luajit

# 連結庫設定
echo "/usr/local/luajit/lib" >> /etc/ld.so.conf.d/libc.conf
ldconfig

# 臨時生效
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1

/usr/local/bin/luajit -v
  # LuaJIT 2.1.0-beta3 -- Copyright (C) 2005-2017 Mike Pall. http://luajit.org/

溫馨提示: 上述 lua 解析器此處採用的是 LuaJIT 官方的 OpenResty 分支, 而不是 luajit 的主分支https://luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz,後面入坑出坑會解釋為啥這樣做。


Step 4.為了使Nginx可以連線到redis資料庫中執行一些列操作,此處藉助於lua-nginx-module模組下載並解壓所需的lua-resty-core、lua-resty-lrucache、lua-resty-redis。

cd /usr/local/src
# 基於 FFI 的 lua-nginx-module API
# 專案地址: https://github.com/openresty/lua-resty-core
wget -c https://github.com/openresty/lua-resty-core/archive/refs/tags/v0.1.23.tar.gz -O /usr/local/src/lua-resty-core.tar.gz
tar -zxvf lua-resty-core.tar.gz && ls lua-resty-core-0.1.23
  # dist.ini  lib  Makefile  README.markdown  t  valgrind.suppress

# 基於 LuaJIT FFI 的 Lua-land LRU Cache
# 專案地址: https://github.com/openresty/lua-resty-lrucache
wget -c https://github.com/openresty/lua-resty-lrucache/archive/refs/tags/v0.13.tar.gz -O /usr/local/src/lua-resty-lrucache-v0.13.tar.gz
tar -zxvf lua-resty-lrucache-v0.13.tar.gz && ls lua-resty-lrucache-0.13/
  # dist.ini  lib  Makefile  README.markdown  t  valgrind.suppress

# 基於 cosocket API 的 ngx_lua 的 Lua redis 使用者端驅動
# 專案地址: https://github.com/openresty/lua-resty-redis
wget -c https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz -O /usr/local/src/lua-resty-redis-v0.29.tar.gz
tar -zxvf lua-resty-redis-v0.29.tar.gz && ls lua-resty-redis-0.29/
# 在使用時可將lua指令碼放入到nginx設定目錄中。
mkdir -vp /usr/local/nginx/lua/
cp -a  /usr/local/src/lua-resty-redis-0.29/lib /usr/local/nginx/lua/
# 以樹形結構顯示該目錄
$ tree /usr/local/nginx/lua/
/usr/local/nginx/lua/
├── hello.lua
└── lib
    └── resty
        └── redis.lua

Step 5.在上面步驟操作完畢之後,我們便可以進行nginx編譯安裝了,構建流程如下(在博主的前面講解的Nginx系列教學就已經有詳細講述 【[Nginx進階學習之最佳設定實踐指南][https://blog.weiyigeek.top/2019/9-1-124.html]】,此處就不在大篇幅累述了):

# 建立允許使用者和組,不需要家目錄不登入bash
useradd -M -s /sbin/nologin nginx 

# 建立 Nginx 所需目錄
sudo mkdir -vp /usr/local/nginx/{module,modules,lua} /var/cache/nginx/{client_temp,proxy_temp,fastcgi_temp,uwsgi_temp,scgi_temp}
cd /usr/local/src/nginx-1.22.0

# Nginx 預編譯引數設定
./configure \
--prefix=/usr/local/nginx \
--user=nginx --group=nginx \
--with-pcre=../pcre-8.45 \
--with-zlib=../zlib-1.2.12 \
--with-openssl=../openssl-1.1.1q \
--sbin-path=/usr/sbin/nginx \
--conf-path=/usr/local/nginx/nginx.conf \
--pid-path=/usr/local/nginx/nginx.pid \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--lock-path=/var/run/nginx.lock \
--modules-path=/usr/local/nginx/modules \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--with-threads \
--with-http_sub_module --with-http_v2_module \
--with-http_auth_request_module --with-http_realip_module --with-http_secure_link_module \
--with-http_gunzip_module --with-http_gzip_static_module --with-http_ssl_module \
--with-http_slice_module --with-http_stub_status_module \
--with-http_dav_module --with-http_flv_module --with-http_mp4_module \
--with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-stream_geoip_module \
--with-mail --with-mail_ssl_module \
--with-http_addition_module --with-http_random_index_module \
--with-compat --with-file-aio \
--with-cc-opt='-Os -fomit-frame-pointer -g'  \
--with-ld-opt='-Wl,-rpath,/usr/local/luajit/lib,--as-needed,-O1,--sort-common' \
--add-module=/usr/local/src/ngx_devel_kit-0.3.1 \
--add-module=/usr/local/src/lua-nginx-module-0.10.21 \
--add-dynamic-module=/usr/local/src/echo-nginx-module-0.62 \

# 編譯構建安裝
make & make install

溫馨提示: 上述 ./configure 編譯設定中使用靜態連結庫方式來新增ngx_devel_kit-0.3.1/lua-nginx-module-0.10.21 模組, 又為了演示加入動態連結庫的使用方式,此處使用--add-dynamic-module引數指定echo-nginx-module-0.62的解壓目錄,如果使用動態連線庫的方式載入模組將會在後續實踐中展示。

構建結果:

# configure 結果
Configuration summary
  # + using threads
  # + using PCRE library: ../pcre-8.45
  # + using OpenSSL library: ../openssl-1.1.1q
  # + using zlib library: ../zlib-1.2.12
  # nginx path prefix: "/usr/local/nginx"
  # ....................................
  # nginx http scgi temporary files: "/var/cache/nginx/scgi_temp"

# Make 構建安裝後提示lib動態連結庫地址。
- add LIBDIR to the 'LD_LIBRARY_PATH' environment variable during execution
- add LIBDIR to the 'LD_RUN_PATH' environment variable during linking
- use the '-Wl,-rpath -Wl,LIBDIR' linker flag # 或者在編譯是新增依賴的Lib目錄。
- have your system administrator add LIBDIR to '/etc/ld.so.conf'

/usr/local/src/nginx-1.22.0# ls objs/
  # ls objs/
  # addon         ngx_auto_config.h               
  # autoconf.err  ngx_auto_headers.h              
  # Makefile      ngx_http_echo_module_modules.c  
  # nginx         ngx_http_echo_module_modules.o  
  # ngx_modules.c src
  # nginx.8       ngx_http_echo_module.so   ngx_modules.o


Step 6.在Nginx安裝部署成功後,為了驗證Nginx + Lua安裝環境,我們需要再 nginx 主組態檔入口設定如下關鍵內容,注意下面帶有文字註釋部分。

$ grep -v "^#|^$|#"  /usr/local/nginx/conf.d/nginx.conf
worker_processes  1;
events {
  worker_connections  1024;
}
http {
  include       mime.types;
  default_type  application/octet-stream;
  # 去除 log_format 前者的註釋符 `#`
  log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
  sendfile        on;
  keepalive_timeout  65;
  # lua 包模組依賴路徑
  lua_package_path '/usr/local/src/lua-resty-core-0.1.23/lib/?.lua;/usr/local/src/lua-resty-lrucache-0.13/lib/?.lua;';
  ...
  # 新增載入nginx家目錄下的conf.d/目錄子組態檔 (萬用字元)
  include conf.d/*.conf;
}

然後再建立子設定目錄與demo.weiyigeek.top站點設定demo.conf檔案中,新增如下server欄位內容片段。

mkdir /usr/local/nginx/conf.d
tee /usr/local/nginx/conf.d/demo.conf <<'EOF'
# https - demo.weiyigeek.top
server {
  listen       80;
  server_name  demo.weiyigeek.top;
  charset utf-8;
  access_log /var/log/nginx/demo-access.log main buffer=128k flush=1m;
  # 方式1.content_by_lua_block lua 片段
  location /hello-lua {
    default_type 'text/plain';
    content_by_lua_block {
      ngx.say("Hello World! Lua & Nginx .")
    }
  }
    
  # 方式2.content_by_lua_file lua 指令碼檔案路徑
  location /hello-lua-file {
    default_type 'text/html';
    content_by_lua_file ./lua/hello.lua;
  }

  # 方式3.access_by_lua 在請求存取階段處理用於存取控制。
  location /hello-lua-access {
    default_type 'text/html';
    access_by_lua '
      local message = "403 - Hello World! Lua & Nginx  access_by_lua"
      ngx.say(message)
    ';
  }

  # 方式4.content_by_lua 在內容處理階段接受請求並輸出響應。
  location /hello-lua-content {
    default_type 'text/html';
    content_by_lua "ngx.print('Hello World!')";
  }
}
EOF

溫馨提示:access_by_lua content_by_lua 的區別是對於Nginx請求的不同處理階段,前者是存取階段處理用於存取控制(適用於http、server、location、location if),後者內容處理器接受請求並輸出響應,適用於location、location if


Step 7.上述設定完成後為了驗證組態檔是否存在問題,可執行如下命令如果顯示 successful 表示設定沒有問題,之後就可過載 nginx 服務。

$ nginx -t 
  # nginx: the configuration file /usr/local/nginx/nginx.conf syntax is ok
  # nginx: configuration file /usr/local/nginx/nginx.conf test is successful

$ /usr/sbin/nginx -s reload
$ ps -ef | grep "nginx"
  # root      244962       1  0 16:40 ?        00:00:00 nginx: master process nginx
  # nginx     245707  244962  0 21:42 ?        00:00:00 nginx: worker process
  # root      245710  245523  0 21:42 pts/0    00:00:00 grep nginx

Step 8.驗證基本的Nginx+Lua環境,我們存取上述組態檔中的域名和子目錄,存取結果如下圖所示則表示環境OK,否則請排查錯誤或者檢視是否存在下述的入坑出坑中相關問題。

curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua
Hello World! Lua & Nginx .

curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-file
<h2> Hello world! Lua & Nginx with Hello.lua. </h2>

curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-access
Hello World! Lua & Nginx  access_by_lua

curl -H "host:demo.weiyigeek.top" 10.20.172.201/hello-lua-content
Hello World!


知識擴充套件: 編譯構建nginx後我們可通過nginx -V命令檢視曾經./configure預編譯引數的設定。

$ nginx -V
nginx version: nginx/1.22.0
built by gcc 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
built with OpenSSL 1.1.1q  5 Jul 2022
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx  
....
--add-module=/usr/local/src/lua-nginx-module-0.10.21 --add-dynamic-module=/usr/local/src/echo-nginx-module-0.62 -

0x02 使用實踐

1.Nginx 實踐使用 echo-nginx-module 模組之動態載入連結庫

描述: 從 NGINX 1.9.11 開始,您還可以將此模組編譯為動態模組,方法是在上面的 ./configure 命令列中使用 --add-dynamic-module=PATH 選項而不是--add-module=PATH選項,然後你可以通過 load_module 指令在你的 nginx.conf 中顯式載入模組,注意必須在 events{} 片段之前.

模組語法: https://github.com/openresty/echo-nginx-module

Step 1.在Nginx.conf檔案中設定load_module指令以動態載入 echo-nginx-module 模組。

# 方式1.絕對路徑
load_module /usr/local/nginx/modules/ngx_http_echo_module.so;
# 方式2.相對路徑
load_module ./modules/ngx_http_echo_module.so;
.....
events {
  worker_connections  1024;
}

Step 2.同樣在demo.conf檔案中的進行該模組常規的使用實踐。

$ cat conf.d/demo.conf

server {
  ...
  # 範例1.常規輸出(注意文字型別則網頁中反饋展現資料也不相同)。
  location /nginx-module/echo {
    default_type 'text/html';
    echo -n "<b>Domain: demo.weiyigeek.top</b> <br/>";
    echo "Hello World! by ngx_http_echo_module.so";
  }

  # 範例2.請求延時顯示以及重置時間定時器。
  location /nginx-module/timed {
    default_type 'text/plain';
    echo "Hello World! by ngx_http_echo_module.so \r";
    echo_reset_timer;
    echo "1.takes about $echo_timer_elapsed sec \r";
    echo_flush;
    echo_sleep 2.5;  # in sec
    echo "2.takes about $echo_timer_elapsed sec.";
    echo "End";
  }

  # 範例3.Body檔案前後插入資料以及在中部插嵌入反向代理網站原始碼。
  location /nginx-module/body {
    resolver 223.6.6.6;
    default_type 'text/html';
    echo "Hello World! by ngx_http_echo_module.so";
    echo_before_body 'Blog - ';
    proxy_pass $scheme://www.weiyigeek.top:$server_port/index.html;
    echo_before_body 'www.WeiyiGeek.top';
    echo_after_body '[END]';
  }
  
  # 範例4.多次輸出同一個字串以及顯示使用者端請求header與請求body主體引數
  location /nginx-module/duplicate {
    default_type 'text/plain';
    echo_duplicate 3 "--";
    echo_duplicate 1 "\rHello World! by ngx_http_echo_module.so \r\r";
    # echo_duplicate 1000_000 "Hello World! by ngx_http_echo_module.so";
    echo "\r";
    echo_duplicate 1 $echo_client_request_headers;
    echo "\r";
    echo_read_request_body;
    echo "\r";
    echo_request_body;
    echo_duplicate 3 "--";
    echo;
   }
 
  # 範例5.正則匹配請求引數,注意`$arg_var`後面的var是可以自定義設定,此處為flag引數。
  location ^~ /nginx-module/if {
    default_type 'text/plain';
    set $res default;
    echo $arg_flag;
    if ($arg_flag ~* '^a') {
      set $res change;
      echo $arg_flag, $res;
    }
    echo $res;
  }
  ....
}

補充 echo_subrequest_async 非同步請求
描述: 使用 HTTP 方法、可選的 url 引數(或查詢字串)和可選的請求主體發起非同步子請求,請求主體可以定義為字串或包含主體的檔案的路徑。

  # GET /multi will yields
   #   querystring: foo=Foo
   #   method: POST
   #   body: hi
   #   content length: 2
   #   ///
   #   querystring: bar=Bar
   #   method: PUT
   #   body: hello
   #   content length: 5
   #   ///
   location /multi {
       echo_subrequest_async POST '/sub' -q 'foo=Foo' -b 'hi';
       echo_subrequest_async PUT '/sub' -q 'bar=Bar' -b 'hello';
   }
   location /sub {
       echo "querystring: $query_string";
       echo "method: $echo_request_method";
       echo "body: $echo_request_body";
       echo "content length: $http_content_length";
       echo '///';
   }

Step 3.完成設定後過載nginx服務, 通過瀏覽器存取上述路徑驗證模組使用與輸出,效果如下圖所示:

該模組的其它使用請參考其專案地址Readme檔案,此處演示瞭如何載入動態連結庫到nginx,並且使用連結庫中的模組。


2.Nginx 實踐使用 lua-resty-redis 模組連線 Redis 進行資料操作與展示

描述: 前面環境部署中已下載 ngx_lua_nginx 模組的 Redis 使用者端驅動程式Lua庫, 下面將演示如何在 Nginx 基於 ngx_lua_nginx 模組連線到Redis記憶體資料庫進行相應資料查詢,好了本小節就直奔主題。

語法參考: https://github.com/openresty/lua-resty-redis

廢話不多說,實踐出真知

Step 1.在前面環境安裝中我們解壓在 ngx_lua_nginx 模組使用 Redis 使用者端驅動程式Lua庫,並將其 Lib 目錄複製到 /usr/local/nginx/lua/ 目錄中,其次我也準備了Redis資料庫環境,針對安裝部署步驟就不在詳述了, 想要快速安裝的朋友可以參考我的此篇文章【[Redis記憶體資料庫環境快速搭建部署][https://blog.weiyigeek.top/2022/4-24-653.html]】。

$ tree /usr/local/nginx/lua/lib/
/usr/local/nginx/lua/lib/
└── resty
    └── redis.lua

# Redis 資料庫 & 為了演示資料準備兩個Key即domain/blog
192.168.1.22 6379 weiyigeek.top
/data # redis-cli
127.0.0.1:6379> auth  weiyigeek.top
OK
127.0.0.1:6379> ping
PONG
127.0.0.1:6379> set domain www.weiyigeek.top 
OK
127.0.0.1:6379> set blog blog.weiyigeek.top
OK

Step 2.想要在Nginx使用該redis.lua連結到資料庫,首先我們需要在nginx.conf組態檔中加入該lua包路徑/usr/local/nginx/lua/lib/,例如:

$ grep "lua_package_path" /usr/local/nginx/nginx.conf
lua_package_path '/usr/local/nginx/lua/lib/?.lua;/usr/local/src/lua-resty-core-0.1.23/lib/?.lua;/usr/local/src/lua-resty-lrucache-0.13/lib/?.lua;'

Step 3.此處也是在 demo.conf 中進行設定使用Redis使用者端驅動程式Lua庫,連線到Redis資料庫中, 此處為了方便演示就直接在該組態檔content_by_lua_block 程式碼塊中使用lua語法,在企業生產實踐環境中一定要將其寫入到lua檔案檔案中。

# /usr/local/nginx/conf.d/demo.conf
server {
...
  location /redis/get {
    default_type 'text/html';
    set $key $arg_key;
    content_by_lua_block {
      -- # 引入resty.redis模組與建立範例化物件
      local redis = require "resty.redis"
      local client = redis:new()
      local REDIS_HOST = "192.168.1.22"
      local REDIS_PROT = 6379
      local REDIS_AUTH = "weiyigeek.top"
      -- # ngx.log(ngx.ERR, ngx.var.key)
      -- # 分別設定連線、傳送和讀取超時閾值(以毫秒為單位),用於後續通訊端操作。
      client:set_timeouts(1000, 1000, 1000)
  
      -- # 建立連結物件, 連線到Redis資料庫
      ngx.say("1.connect redis server..... <br>");
      local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
      if not ok then
        ngx.say("failed to connect: ", err)
        return
      end
  
      -- # 認證
      ngx.say("2.auth redis server..... <br>");
      local res, err = client:auth(REDIS_AUTH)
      if not res then
        ngx.say("failed to authenticate: ", err)
        return
      end
  
      -- # 獲取指定請求鍵值
      ngx.say("3.get custom KV for redis server, Key = ",ngx.var.key," <br>");
      local res, err = client:get(ngx.var.key)
      if not res then
          ngx.say("failed to get key: ", err)
          return
      end
      if res == ngx.null then
          ngx.say("key not found.")
          return
      end

      -- # 輸出結果
      ngx.say("<b style='color:red'>4.result value: ",res,"</b><br/>")

      -- # 使用完畢後立即關閉銷燬Redis連線(短連線可以如此使用,如果是長連結建議回收該連線池物件即可)
      local ok, err = client:close()
      if not ok then
        ngx.say("failed to close: ", err)
        return
      else
        ngx.say("5.just close the Redis connection right away <br/>")
      end
    }
  }
...
}


Step 5.在演示一個範例,我們可以一次性執行多個redis操作命令 lua-resty-redis 庫支援pipeline提交,下面我們演示使用 content_by_lua_file 關鍵字指定連線操作redis的lua指令碼地址(/usr/local/nginx/lua/custom/nginx-redis.lua)實踐, 該方式線上上環境中推薦使用。

# 1) 操作 redis 資料庫的 lua 指令碼範例。
tee /usr/local/nginx/lua/custom/nginx-redis.lua <<'EOF'
-- # 引入resty.redis模組與建立範例化物件
local redis = require "resty.redis"
local client = redis:new()
local REDIS_HOST = "192.168.1.22"
local REDIS_PROT = 6379
local REDIS_AUTH = "weiyigeek.top"
-- # ngx.log(ngx.ERR, ngx.var.key)
-- # 分別設定連線、傳送和讀取超時閾值(以毫秒為單位),用於後續通訊端操作。
client:set_timeouts(1000, 1000, 1000)

-- # 驗證請求的引數是否存在
if (ngx.var.key == ngx.null and ngx.var.value == ngx.null) 
then
  ngx.say("Request parameters : key + value not found!")
  ngx.exit(404)
end

-- # 建立連結物件, 連線到Redis資料庫
ngx.say("1.connect redis server..... <br>");
local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
if not ok then
  ngx.say("failed to connect: ", err)
  return
end

-- # 認證
ngx.say("2.auth redis server..... <br>");
local res, err = client:auth(REDIS_AUTH)
if not res then
  ngx.say("failed to authenticate: ", err)
  return
end

-- # 使用 pipeline 通道方式進行redis 資料庫操作
client:init_pipeline()
client:set(ngx.var.key, ngx.var.value)
client:get(ngx.var.key)
client:get("domain")
local results, err = client:commit_pipeline()
if not results then
  ngx.say("failed to commit the pipelined requests: ", err)
  return
end

-- 結果遍歷
for i, res in ipairs(results) do
  if type(res) == "table" then
      if res[1] == false then
          ngx.say("failed to run command ", i, ": ", res[2],"<br/>")
      else
        -- process the table value
        ngx.say("3) 3.",i, ": ", res[2],"<br/>")
      end
  else
    -- process the scalar value
    ngx.say("<p style='color:red'>3) ",i,"---",res,"</p>")
  end
end

-- 將當前 Redis 連線立即放入 ngx_lua cosocket 連線池(將其放入大小為100的連線池中,最大空閒時間為10秒)。
local ok, err = client:set_keepalive(10000, 100)
if not ok then
  ngx.say("failed to set keepalive: ", err)
  return
end
ngx.say("4.將當前 Redis 連線立即放入 ngx_lua cosocket 連線池<br/>")
EOF


# 2) 設定 demo.conf 檔案 同樣在 server 片段中加入如下 location 片段。
server {
....
  location /redis/pipeline {
    default_type 'text/html';
    # 獲取請求引數中key的值與value的值並存放到nginx環境變數中
    set $key $arg_key;
    set $value $arg_value;
    # 呼叫並執行指定的lua指令碼
    content_by_lua_file ./lua/custom/nginx-redis.lua;
  }
....
}

在設定完成後我們便可以過載nginx,並使用存取瀏覽器存取上述路徑,例如: http://demo.weiyigeek.top/redis/pipeline?key=name&value=WeiyiGeek,此處我演示的結果如下圖所示。


3.Nginx 實踐讀取Redis資料庫中圖片繫結對應鍵值並進行圖片展示

描述: 假如在這樣一個場景中,為了避免惡意使用者遍歷有規律的圖片進行下載,那如何解決這個問題呢?

方法是有得但也不限於本節實踐的案例,此處我們可以將其圖片名稱或者圖片md5值存入到Redis資料庫中作為Key,而實際的圖片路徑作為Value,在請求時我們將該md5值作為引數進行傳入,經過 Nginx 對請求引數的處理,使用前面的方式在 Lua 指令碼中連線Redis,並將URL傳遞的md5引數作為key進行get查詢,並將查詢到的圖片路徑,反饋給set指令設定的變數之中,然後我們便可以通過 proxy_pass 進行代理存取(位址列中的url不會變化,保證實際的圖片路徑),或者也可以加上一個頭Content-Disposition直接進行圖片下載。

不在多說廢話了,只有實踐才是王道。

實踐流程:

  • Step 1.準備一個圖片目錄以及放入幾張圖片進行演示,此處你可以使用圖片名稱md5也可使用圖形檔案本身md5效驗值。
$ tree /usr/local/nginx/html/
/usr/local/nginx/html/
├── 50x.html
├── images
│   ├── 1562941454569.jpeg
│   ├── 1562941454570.jpeg
│   └── 1562941454571.png
└── index.html

# 檔案的MD5值
/usr/local/nginx/html/images# md5sum * | awk '{print "set "$1" "$2}'
set 6fad4c2466dc7f61fb055021ec65324d  1562941454569.jpeg
set 611877180883388de4752ded33a81165  1562941454570.jpeg
set 6636d52bfbe068177df5219edf4dd456  1562941454571.png

# 寫入KV到redis資料庫中
127.0.0.1:6379> set 6fad4c2466dc7f61fb055021ec65324d 1562941454569.jpeg
OK
127.0.0.1:6379> set 611877180883388de4752ded33a81165 1562941454570.jpeg
OK
127.0.0.1:6379> set 6636d52bfbe068177df5219edf4dd456 1562941454571.png
OK
  • Step 2.在demo.conf檔案中的server片段中增加 location 片段,其中進行如下設定:
$ vim conf.d/demo.conf
server {
......
 location = /api/v2/images/get {
   resolver 223.6.6.6;
   set $key $arg_md5sum;
   set $name "";
   access_by_lua_block {
    local redis = require "resty.redis"
    local client = redis:new()
    local REDIS_HOST = "192.168.1.22"
    local REDIS_PROT = 6379
    local REDIS_AUTH = "weiyigeek.top"
    client:set_timeouts(1000, 1000, 1000)
    local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
    if not ok then
      ngx.say("failed to connect: ", err)
      return
    end
    local res, err = client:auth(REDIS_AUTH)
    if not res then
      ngx.say("failed to authenticate: ", err)
      return
    end
    local res, err = client:get(ngx.var.key)
    if not res then
        ngx.say("failed to get key: ", err)
        return
    end
    if res == ngx.null then
        ngx.say("key not found.")
        return
    else
      -- # 關鍵點將redis中指定鍵的值賦予給nginx指定變數
      ngx.var.name = res
    end
    local ok, err = client:set_keepalive(10000, 100)
    if not ok then
      ngx.say("failed to set keepalive: ", err)
      return
    end
   }
   proxy_pass $scheme://$server_name/images/$name;
  }
......
}

在設定完成後我們過載 Nginx,然後利用瀏覽器進行存取如上URL,例如http://demo.weiyigeek.top/api/v2/images/get?md5sum=6636d52bfbe068177df5219edf4dd456,執行結果如下所示:


  • Step 3.如果我們想通過瀏覽器存取上述地址就直接彈出原始檔名稱進行下載的,我們則可以在 proxy_pass 片段後加上如下 header 頭: add_header Content-Disposition "attachment;filename=$name";
...
proxy_pass $scheme://$server_name/images/$name;
add_header Content-Disposition "attachment;filename=$name";
...

# 過載Nginx後利用CURL存取該URL
$ curl -I http://demo.weiyigeek.top/api/v2/images/get?md5sum=6636d52bfbe068177df5219edf4dd456
HTTP/1.1 200 OK
Server: nginx/1.22.0
Date: Tue, 02 Aug 2022 02:23:12 GMT
Content-Type: image/png
Content-Length: 32641
Connection: keep-alive
Last-Modified: Wed, 23 Mar 2022 00:48:26 GMT
ETag: "623a6e5a-7f81"
Accept-Ranges: bytes
Content-Disposition: attachment;filename=1562941454571.png


  • Step 4.當然,你也可使用rewrite_by_lua_block程式碼塊包含Lua可直接或者圖片路徑,然後使用ngx.redirect()方法進行跳轉。
$ vim conf.d/demo.conf
server {
......
 location = /api/v1/images/get {
  resolver 223.6.6.6;
  set $key $arg_md5sum;
  rewrite_by_lua_block {
    local redis = require "resty.redis"
    local client = redis:new()
    local REDIS_HOST = "192.168.1.22"
    local REDIS_PROT = 6379
    local REDIS_AUTH = "weiyigeek.top"
    client:set_timeouts(1000, 1000, 1000)
    local ok, err = client:connect(REDIS_HOST, REDIS_PROT)
    if not ok then
      ngx.say("failed to connect: ", err)
      return
    end
    local res, err = client:auth(REDIS_AUTH)
    if not res then
      ngx.say("failed to authenticate: ", err)
      return
    end
    local res, err = client:get(ngx.var.key)
    if not res then
        ngx.say("failed to get key: ", err)
        return
    end
    if res == ngx.null then
      ngx.say("key not found.")
    else
      -- # 關鍵點圖片格式化。
      return ngx.redirect(string.format("%s%s","/images/",res))
    end
    local ok, err = client:set_keepalive(10000, 100)
    if not ok then
      ngx.say("failed to set keepalive: ", err)
      return
    end
   }
   # 若沒有匹配搭配到進行跳轉進行跳轉則存取首頁
   proxy_pass $scheme://$server_name/index.html;
  }
......
}

好了,本章實踐就到此處了,更多的奇技淫巧盡在 [weiyigeek] 公眾號.


0x03 擴充套件補充

範例1.使用 ngx.location.capture() 請求內部介面

location = /auth {
  internal;
  retur 200 '{"status":"$auth_status"}'
}

# 此處根據業務的需求來寫正規表示式,一定要個 redis 裡的 KEY  對應上
location  ~/[0-9].*\.(gif|jpg|jpeg|png)$ {
  set $target '';
  access_by_lua '
  # 使用 nginx 的內部引數 ngx.var.uri 來獲取請求的 uri 地址,如 /000001.jpg
    local key = ngx.var.uri
  # 根據正則匹配到 KEY ,從 redis 資料庫裡獲取檔案 ID (路徑和檔名)
    local res = ngx.location.capture(
        "/Redis", { args = { key = key } }
    )
    if res.status ~= 200 then
        ngx.log(ngx.ERR, "Redis server returned bad status: ",res.status)
        ngx.exit(res.status)
    end
    if not res.body then
        ngx.log(ngx.ERR, "Redis returned empty body")
        ngx.exit(500)
    end
    local parser = require "Redis.parser"
    local filename, typ = parser.parse_reply(res.body)
    if typ ~= parser.BULK_REPLY or not server then
        ngx.log(ngx.ERR, "bad Redis response: ", res.body)
        ngx.exit(500)
    end
  
    ngx.var.target = filename
  ';
    proxy_pass http://10.20.172.196/$target;
}

0x0n 入坑出坑

問題1. 當編譯 Nginx 時報checking for LuaJIT 2.x ... not found, ./configure: error: unsupported LuaJIT version; ngx_http_lua_module requires LuaJIT 2.x. 錯誤時的解決辦法。

問題描述: tell nginx's build system where to find LuaJIT 2.1
解決辦法:

# 臨時生效
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1

# 永久生效
tee -a /etc/profile <<'EOF'
export LUAJIT_LIB=/usr/local/luajit/lib
export LUAJIT_INC=/usr/local/luajit/include/luajit-2.1
EOF
source /etc/profile

問題2.在使用luajit官方主分支LuaJIT-2.1.0-beta3提供LuaJIT安裝部署出現nginx: [alert] detected a LuaJIT version which is not OpenResty's;以及nginx: [alert] failed to load the 'resty.core' module警告。

錯誤資訊:

$ /usr/sbin/nginx 
nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see https://github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from https://openresty.org/en/download.html)
nginx: [alert] failed to load the 'resty.core' module (https://github.com/openresty/lua-resty-core); ensure you are using an OpenResty release from https://openresty.org/en/download.html (reason: module 'resty.core' not found:
  no field package.preload['resty.core']
  no file './resty/core.lua'
  no file '/usr/local/share/luajit-2.1.0-beta3/resty/core.lua'
  no file '/usr/local/share/lua/5.1/resty/core.lua'
  no file '/usr/local/share/lua/5.1/resty/core/init.lua'
  no file './resty/core.so'
  no file '/usr/local/lib/lua/5.1/resty/core.so'
  no file '/usr/local/lib/lua/5.1/loadall.so'
  no file './resty.so'
  no file '/usr/local/lib/lua/5.1/resty.so'
  no file '/usr/local/lib/lua/5.1/loadall.so') in /usr/local/nginx/nginx.conf:117

問題原因1: 提示LuaJIT的版本不匹配OpenResty's核心版本, 讓我不要用這個luajit版本,可以用openresty提供的luajit優化版本,或者乾脆直接用openresty,下面將安裝解除安裝luajit官網版本,下載openresty提供的luajit優化版本(即上面環境安裝已經實踐了,按照上面版本進行安裝一般不會存在該問題)。

# 你可能會進行 Lua 指令碼直譯器的安裝 LuaJIT 
http://luajit.org/download.html
wget -c https://luajit.org/download/LuaJIT-2.1.0-beta3.tar.gz
tar -zxf LuaJIT-2.1.0-beta3.tar.gz && cd LuaJIT-2.1.0-beta3
make && make install
ln -sf /usr/local/bin/luajit-2.1.0-beta3 /usr/local/bin/luajit
# 解除安裝LuaJIT官網主分支版本,然後重新安裝openresty提供的luajit優化版即可
make uninstall
make clean

問題原因2: 提示載入'resty.core'模組失敗,其解決辦法,按照https://github.com/openresty/lua-nginx-module/issues/1509上面所說, 安裝lua-resty-core和依賴檔案lua-resty-lrucache解決問題,即我前面實踐中已經進行此部分操作,若不會操作請上翻到 【安裝部署】標題進行檢視。

原文地址: https://blog.weiyigeek.top/2022/7-2-676.html

本文至此完畢,更多技術文章,盡情期待下一章節!


WeiyiGeek Blog 個人部落格 - 為了能到遠方,腳下的每一步都不能少 】

歡迎各位志同道合的朋友一起學習交流,如文章有誤請在下方留下您寶貴的經驗知識!

作者主頁: 【 https://weiyigeek.top
部落格地址: 【 https://blog.weiyigeek.top 】

專欄書寫不易,如果您覺得這個專欄還不錯的,請給這篇專欄 【點個贊、投個幣、收個藏、關個注,轉個發,留個言】(人間六大情),這將對我的肯定,謝謝!。

  • echo "【點個贊】,動動你那粗壯的拇指或者芊芊玉手,親!"

  • printf("%s", "【投個幣】,萬水千山總是情,投個硬幣行不行,親!")

  • fmt.Printf("【收個藏】,閱後即焚不吃灰,親!")

  • console.info("【轉個發】,讓更多的志同道合的朋友一起學習交流,親!")

  • System.out.println("【關個注】,後續瀏覽檢視不迷路喲,親!")

  • cout << "【留個言】,文章寫得好不好、有沒有錯誤,一定要留言喲,親! " << endl;

更多網路安全、系統運維、應用開發、物聯網實踐、網路工程、全棧文章,盡在 https://blog.weiyigeek.top 之中,謝謝各位看又支援!