nginx

目录

nginx
config
nginx
conf.d
lua
filter_body.lua
json.log-format
json.stream-log-format
websocket-json.log-format
lua.conf
monitor.conf
nginx.conf
cut_nginx_log.sh
setup.sh

常用命令

cd /usr/local/nginx/sbin/
./nginx              # 启动
./nginx -s stop      # 停止
./nginx -s quit      # 安全退出
./nginx -s reload    # 重新加载配置文件
ps aux|grep nginx    # 查看 Nginx 进程

# 统计请求 IP 和个数
zcat *.log.gz | jq -r '.remote_addr' | sort | uniq -c | sort -nr

基本配置

sendfile on; # html 下的配置,可以直接把文件发给用户,不经过 nginx
listen 80; # 开放端口
server_name 127.0.0.1; # 域名或者 IP
index index.html index.htm; # 默认页面

# root 和 alias 的区别,如需要访问 /usr/local/nginx/html/images,访问的路径是域名加 /images:
# root 的处理结果是: root 路径 + location 路径
location /images {
    root /usr/local/nginx/html;
}
# alias 的处理结果是: 使用 alias 路径替换 location 路径
location /images {
    alias /usr/local/nginx/html/images;
}
include /etc/nginx/conf.d/*.conf; # 写在 http 下,可以读取目录下的多个配置文件,不需要改主文件

反向代理

最主要的是 proxy_pass 这个参数。

location / {
    proxy_pass http://127.0.0.1:5230/;
    rewrite ^/(.*)$ /$1 break;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Upgrade-Insecure-Requests 1;
    proxy_set_header X-Forwarded-Proto https;
}
location /api/ {
    proxy_pass http://127.0.0.1:3000/api/
}

在后端的基础上增加路径层数:

location / {
    rewrite  ^/proxy/(.*)$ /$1 break;
    proxy_pass http://127.0.0.1:3000/api/
}

动静分离

可以把静态文件部署在 Nginx 上。

location ~* \.(gif|jpg|jpeg)${
    root /html;
}
location ^~ /static/ {
    root /webroot/static/;
}
location ~* \.(html|gif|jpg|jpeg|png|css|js|ico)$ {
    root /webroot/res/;
}

重写(URLRewrite)

rewrite ^/[0-9]+.html$ /index.html?testParam=$1 break; # $1 表示第一个匹配的字符串

防盗链

写在 location 下:

valid_referers none 192.168.1.1 # 白名单 IP
if ($invalid_referer) {
    return 403; # 返回错误码
}
rewrite ^/ /403.png break;

Keepalived 高可用

global_defs {
   router_id LB_102 # 名字
}

vrrp_instance VI_102 { # 名字
    state MASTER  # MASTER 和 BACKUP 表示主备优先级
    interface ens33 # 网卡
    virtual_router_id 51 # ID 需一样
    priority 100 # 优先级
    advert_int 1
    authentication { # 组的账号和密码
        auth_type PASS
        auth_pass 1111
    }
    virtual_ipaddress {
        192.168.8.200 # 虚拟漂移的 IP
    }
}

SSL 证书配置

HTTP

server {
    listen 443 ssl;
    server_name localhost;  # 域名
    ssl_certificate xxx.pem; # 证书
    ssl_certificate_key xxx.key; # 密钥
    ssl_session_timeout 5m;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;

    location / {
        proxy_pass http://127.0.0.1:5230/;
    }
}

TCP

server {
	listen 443 ssl;
	ssl_certificate xxx.pem; #证书
	ssl_certificate_key xxx.key; #秘钥
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    proxy_pass 127.0.0.1:5230;
}

自签 SSL 证书

openssl genrsa -out server.key 2048
openssl req -x509 -new -nodes -key server.key -sha256 -days 3650 -out server.crt
Tip

由于 HTTP 协议默认的端口是 80,而 HTTPS 默认的端口是 443,如果想让 HTTP 的访问跳转到 HTTPS 的访问,可以做如下配置:

server {
	listen 80;
	server_name ; #域名
	return 301 https://$server_name$request_uri;	
}

下载目录

location /data {
    alias   /data/res;  # 定义网站根目录,目录可以是相对路径也可以是绝对路径
    autoindex on; # 开启目录浏览功能
    autoindex_exact_size off; # 关闭详细文件大小统计,让文件大小显示 MB、GB 单位,默认为 b
    autoindex_localtime on; # 开启以服务器本地时区显示文件修改日期
    charset  utf-8,gbk; # 定义站点的默认页
    auth_basic "Please input password"; # 密码访问
    auth_basic_user_file /data/web/passwd;
}

密码访问

centos
debian
yum install -y httpd-tools
htpasswd -c passwd user

美化下载目录

sendfile    on;
add_after_body /.autoindex.html;

在目录页放入文件,需要把 autoindex_exact_size 打开。

美化文件
autoindex.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Nginx File Server</title>
    <style>
        :root {
            --bg: #f6f8fa;
            --card: #ffffff;
            --text: #24292f;
            --muted: #6e7781;
            --border: #d0d7de;
            --hover: #f3f4f6;
            --accent: #0969da;
        }

        body.dark {
            --bg: #0d1117;
            --card: #161b22;
            --text: #c9d1d9;
            --muted: #8b949e;
            --border: #30363d;
            --hover: #21262d;
            --accent: #2f81f7;
        }

        body {
            margin: 0;
            background: var(--bg);
            color: var(--text);
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            display: flex;
            flex-direction: column;
            min-height: 100vh;
            transition: background 0.3s, color 0.3s;
        }

        body > hr {
            display: none;
        }

        .toolbar {
            position: sticky;
            top: 0;
            z-index: 10;
            background: var(--card);
            border-bottom: 1px solid var(--border);
            padding: 14px 18px;
            transition: background 0.3s, border-color 0.3s;
        }

        .toolbar-row {
            display: flex;
            gap: 10px;
            align-items: center;
        }

        .breadcrumb {
            font-size: 13px;
            margin-bottom: 10px;
        }

        .breadcrumb a {
            color: var(--accent);
            text-decoration: none;
        }

        .breadcrumb span {
            color: var(--muted);
            margin: 0 6px;
        }

        .breadcrumb .current {
            color: var(--muted);
        }

        input {
            height: 34px;
            padding: 0 12px;
            border-radius: 8px;
            border: 1px solid var(--border);
            background: var(--card);
            color: var(--text);
            width: 260px;
            transition: background 0.3s, border-color 0.3s, color 0.3s;
        }

        .download-btn {
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .download-btn:hover {
            text-decoration: underline;
        }

        .list {
            max-width: 1400px;
            width: 95%;
            margin: 16px auto;
            background: var(--card);
            border: 1px solid var(--border);
            border-radius: 12px;
            overflow: hidden;
            transition: background 0.3s, border-color 0.3s;
            flex: none;
        }

        .row {
            display: grid;
            grid-template-columns: 1fr 200px 120px;
            gap: 12px;
            align-items: center;
            padding: 10px 16px;
            border-bottom: 1px solid var(--border);
            transition: background 0.3s, border-color 0.3s;
        }

        .row:hover {
            background: var(--hover);
        }

        .row a {
            color: var(--text);
            text-decoration: none;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        a.folder::before {
            content: "📁 ";
        }

        a.file::before {
            content: "📄 ";
        }

        a.img::before {
            content: "🖼️ ";
        }

        a.code::before {
            content: "📜 ";
        }

        mark {
            background: #fde68a;
            border-radius: 4px;
            padding: 0 2px;
        }

        #theme-toggle {
            cursor: pointer;
            border: 1px solid var(--border);
            border-radius: 6px;
            background: var(--card);
            color: var(--text);
            padding: 4px 8px;
            transition: background 0.3s, color 0.3s, border-color 0.3s;
        }

        /* 页脚固定底部 */
        .footer {
            text-align: center;
            padding: 16px 0;
            font-size: 14px;
            color: var(--muted);
            background: var(--card);
            border-top: 1px solid var(--border);
            margin-top: auto; /* 关键: 把页脚推到底部 */
            transition: background 0.3s, border-color 0.3s, color 0.3s;
        }

        .footer-link {
            color: #3b82f6;
            text-decoration: none;
            margin-left: 4px;
        }

        .footer-link:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>

<div class="toolbar">
    <div class="breadcrumb" id="breadcrumb"></div>
    <div class="toolbar-row">
        <input id="search" placeholder="🔍 搜索文件(支持多关键词)">
        <button id="theme-toggle">🌓</button>
    </div>
</div>

<script>
    // 删除 nginx autoindex 标题
    document.querySelectorAll('h1').forEach(h => h.remove());

    // 文件浏览器逻辑
    const MONTH = {
        Jan: '01',
        Feb: '02',
        Mar: '03',
        Apr: '04',
        May: '05',
        Jun: '06',
        Jul: '07',
        Aug: '08',
        Sep: '09',
        Oct: '10',
        Nov: '11',
        Dec: '12'
    };

    function getType(name) {
        if (name.endsWith('/')) return 'folder';
        const ext = name.split('.').pop().toLowerCase();
        if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'bmp'].includes(ext)) return 'img';
        if (['js', 'ts', 'go', 'py', 'sh', 'json', 'yaml', 'yml', 'md', 'html', 'css', 'conf', 'ini', 'log'].includes(ext)) return 'code';
        return 'file';
    }

    function formatSize(bytes) {
        const n = Number(bytes);
        if (!n || isNaN(n)) return '';
        if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(2) + ' G';
        if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(2) + ' M';
        return (n / 1024).toFixed(2) + ' K';
    }

    const rows = [];

    function transform() {
        const pre = document.querySelector('pre');
        if (!pre) return;
        const list = document.createElement('div');
        list.className = 'list';
        let row;
        pre.childNodes.forEach(node => {
            if (node.nodeType === 1) {
                row = document.createElement('div');
                row.className = 'row';
                const fileType = getType(node.text);
                const a = document.createElement('a');
                a.href = node.href;
                a.textContent = node.text;
                a.className = fileType;
                // 下载按钮 - 仅对非文件夹显示
                if (fileType !== 'folder') {
                    const btn = document.createElement('span');
                    btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style="color: var(--text); vertical-align: middle;"><path d="M12 16l-5-5 1.4-1.45 2.6 2.6V4h2v8.15l2.6-2.6L17 11l-5 5zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.588 1.413T18 20z"/></svg>';
                    const downloadLink = document.createElement('a');
                    downloadLink.href = node.href;
                    downloadLink.className = 'download-btn';
                    downloadLink.setAttribute('download', '');
                    downloadLink.style.textDecoration = 'none';
                    downloadLink.style.marginLeft = '6px';
                    downloadLink.style.display = 'inline-flex';
                    downloadLink.style.alignItems = 'center';
                    // 防止触发行点击
                    downloadLink.addEventListener('click', e => e.stopPropagation());
                    downloadLink.appendChild(btn);
                    a.appendChild(downloadLink);
                }
                row.appendChild(a);
                list.appendChild(row);
                row._name = node.text.toLowerCase();
                rows.push(row);
            }
            if (node.nodeType === 3 && row) {
                const text = node.nodeValue.replace(/\s+/g, ' ').trim();
                if (!text) return;
                const p = text.split(' ');
                if (p.length < 2) return;
                const [day, mon, year] = p[0].split('-');
                const time = p[1];
                const date = `${year}-${MONTH[mon] || mon}-${day} ${time}`;
                let size = '';
                if (p[2] && p[2] !== '-') {
                    size = /^\d+$/.test(p[2]) ? formatSize(p[2]) : p[2];
                }
                const d = document.createElement('div');
                d.textContent = date;
                const s = document.createElement('div');
                s.textContent = size;
                row.append(d, s);
            }
        });
        pre.remove();
        document.body.appendChild(list);
        renderBreadcrumb();
    }

    // 搜索功能
    document.getElementById('search').addEventListener('input', e => {
        const keys = e.target.value.toLowerCase().split(/\s+/).filter(Boolean);
        rows.forEach(r => {
            const hit = keys.every(k => r._name.includes(k));
            r.style.display = hit ? 'grid' : 'none';
            const a = r.querySelector('a');
            if (!keys.length) a.innerHTML = a.textContent;
            else a.innerHTML = a.textContent.replace(new RegExp(`(${keys.join('|')})`, 'ig'), '<mark>$1</mark>');
        });
    });

    // 面包屑
    function renderBreadcrumb() {
        const el = document.getElementById('breadcrumb');
        const parts = decodeURIComponent(location.pathname).split('/').filter(Boolean);
        let html = `<a href="/">🏠 root</a>`;
        let acc = '';
        parts.forEach((p, i) => {
            acc += '/' + p;
            html += '<span>/</span>';
            html += i === parts.length - 1 ? `<span class="current">${p}</span>` : `<a href="${acc}/">${p}</a>`;
        });
        el.innerHTML = html;
    }

    transform();

    // 三模式主题按钮
    const themeToggle = document.getElementById('theme-toggle');
    const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');

    let mode = localStorage.getItem('theme') || 'auto'; // dark | light | auto

    function updateButtonIcon(m) {
        if (m === 'dark') themeToggle.textContent = '🌙';
        else if (m === 'light') themeToggle.textContent = '☀️';
        else themeToggle.textContent = '🌓';
    }

    function applyTheme(m) {
        if (m === 'dark') document.body.classList.add('dark');
        else if (m === 'light') document.body.classList.remove('dark');
        else document.body.classList.toggle('dark', darkQuery.matches);
        updateButtonIcon(m);
    }

    // 初始化主题
    applyTheme(mode);

    // 系统主题变化(仅 auto 生效)
    darkQuery.addEventListener('change', e => {
        if (mode === 'auto') applyTheme('auto');
    });

    // 按钮循环切换模式: dark → light → auto → dark
    themeToggle.addEventListener('click', () => {
        if (mode === 'dark') mode = 'light';
        else if (mode === 'light') mode = 'auto';
        else mode = 'dark';
        applyTheme(mode);
        localStorage.setItem('theme', mode);
    });
</script>

<!-- 页脚固定底部 -->
<footer class="footer">
    <span>© 2026 <a href="https://github.com/buyfakett" target="_blank" class="footer-link">buyfakett</a>. All rights reserved.</span>
</footer>

</body>
</html>

隐藏版本号

# 不显示 openrestry 版本及信息
server_tokens off;
more_clear_headers 'Server';

伪造请求头

Tip

当我们需要更换域名时,可以使用这个配置(在不改变原来 Nginx 的情况下)。

当我们修改了任何一个 header 参数,其他 header 参数就不会生效,需要重新设置。

proxy_set_header Host xxx.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;

mirror 指令

nginx_http_mirror_module 模块特性

利用 mirror 模块,可以将线上实时流量拷贝至其他环境同时不影响源站请求的响应,因为 Nginx 会丢弃 mirror 的响应。

mirror 模块可用于以下几个场景:

  • 通过预生产环境测试来观察新系统对生产环境流量的处理能力
  • 复制请求日志以进行安全分析
  • 如用户行为日志,客户和自己都需要一份

将生产环境的流量拷贝到预上线环境或测试环境的好处:

  • 可以验证功能是否正常,以及服务的性能
  • 用真实有效的流量请求去验证,又不用造数据,不影响线上正常访问
  • 这跟灰度发布还不太一样,镜像流量不会影响真实流量
  • 可以用来排查线上问题
  • 重构,假如服务做了重构,这也是一种测试方式

Nginx 的流量镜像是只复制镜像发送到配置好的后端,但是后端响应返回到 Nginx 之后,Nginx 是自动丢弃掉的,这个特性就保证了镜像后端的任何处理不会影响到正常客户端的请求

流量镜像配置

mirror 中不支持配置 access_log,解决方法: mirror-location 跳转到 server,在 server 中配置 access_log。

基础配置
能获取日志配置
server {
    listen       80;
    server_name 192.168.1.1;

    location = /mirror1 {
        internal;
        #### address1 ####
        proxy_set_header Host mirror1.com;
        proxy_pass http://192.168.1.1:10001/api/service/list;
    }

    location = /mirror2 {
        internal;
        #### address2 ####
        proxy_set_header Host mirror2.com;
        proxy_pass http://192.168.1.1:10002/api/service/list;
    }

    location /api/service/list {
        access_log /data/logs/nginx/json_test_to_mirror.log json;
        mirror /mirror1;
        mirror /mirror2;
        proxy_pass http://192.168.1.1:8007;
    }

    location / {
        access_log /data/logs/nginx/json_test.log json;
        proxy_pass http://192.168.1.1:8007;
    }

}
server {
    # server没法设置为内部
    listen 172.168.1.58:10001;

    location / {
        internal;
        access_log /data/logs/nginx/json_testMirror1.log json;
        proxy_pass http://192.168.1.1:8008;
    }
 
}
server {
    # server没法设置为内部
    listen 192.168.1.1:10002;

    location / {
        internal;
        access_log /data/logs/nginx/json_testMirror2.log json;
        proxy_pass http://192.168.1.1:8009;
    }
 
}

常用案例

获取请求ip服务

get_ip.conf
server {
	listen 80;
	server_name xxx.top;

	location / {
		access_log /data/logs/nginx/json_ip.log json;
		proxy_set_header Host $http_host;
        proxy_set_header X-Real-Ip $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:9999;
	}
}

server {
	listen 9999;

	location / {
		access_log off;
		# HTTP 响应头中添加 Date
		add_header Date $date_gmt;
		default_type application/json;
		return 200 "{\"ip\":\"$http_X_Real_Ip\"}";
	}
}

编写逻辑

获取请求参数

使用 $http_ 变量获取 header

使用 $arg_ 变量获取 URL 参数

# 判断多个参数示例
set $flagts 0;
if ( $arg_aaa ~ "^aaa" ) {
    set $flagts "${flagts}1";
}
if ( $arg_bbb ~ "^bbb" ) {
    set $flagts "${flagts}1";
}
if ( $flagts = "011" ) {
    return 200;
}

同端口走不同逻辑

server {
    listen 80;
    server_name xxx.com;
    
    location / {
        return 404;
    }
}
server {
    listen 80 default_server;
    server_name _;
    
    location / {
        return "Hello, World!";
    }
}

AB机房切换

通过 map + include 实现 upstream 切换:

api.conf
location / {
    proxy_pass http://$upstream_api;
}
map.conf
include /etc/nginx/conf.d/mode.conf;

map $backend_mode $upstream_api {
    default api_a;
    b       api_b;
}
mode.conf
map "" $backend_mode {
    default b;
}
upstream.conf
upstream api_a {
    server 127.0.0.1:10000;
}

upstream api_b {
    server 127.0.0.1:10001;
}