目录

  1. 1. 前言
  2. 2. 环境搭建
  3. 3. 反向代理
  4. 4. 目录结构
  5. 5. 命令行操作
  6. 6. Nginx 内置变量
  7. 7. 配置学习
    1. 7.1. 静态资源服务
    2. 7.2. Location 匹配规则
    3. 7.3. 反向代理配置
    4. 7.4. 负载均衡
    5. 7.5. URL 重写与跳转
    6. 7.6. 配置 HTTPS(SSL/TLS)并强制跳转
    7. 7.7. gzip 压缩与浏览器缓存
      1. 7.7.1. expires vs proxy_cache
    8. 7.8. 防盗链
    9. 7.9. 安全措施
  8. 8. 不安全的配置
    1. 8.1. CRLF 注入
    2. 8.2. 目录穿越
    3. 8.3. add_header被覆盖
    4. 8.4. php cgi 配置错误

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Nginx

2025/11/25 DevOps Nginx
  |     |   总文章阅读量:

前言

Nginx 是一种轻量级的 Web 服务器、反向代理服务器及电子邮件(IMAP/POP3)代理服务器

参考:

https://github.com/dunwu/nginx-tutorial


环境搭建

准备一个 Nginx 容器作为网关,两个 Web 应用测试负载均衡

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    container_name: my-nginx
    ports:
      - "80:80"
    volumes:
      # 将本地的配置、静态文件和日志映射到容器内部
      - ./conf.d:/etc/nginx/conf.d:ro  # ro表示只读,保护配置文件
      - ./html:/usr/share/nginx/html:ro
      - ./logs:/var/log/nginx
    networks:
      - web-net
    depends_on:
      - app1
      - app2

  # 后端服务 1
  app1:
    image: traefik/whoami
    container_name: my-app1
    networks:
      - web-net

  # 后端服务 2
  app2:
    image: traefik/whoami
    container_name: my-app2
    networks:
      - web-net

networks:
  web-net:
    driver: bridge

default.conf

# 定义一个负载均衡组,包含两个后端容器
upstream backend_servers {
    # 这里的 app1 和 app2 是 docker-compose.yml 中定义的服务名
    # Docker 的内部 DNS 会自动将它们解析为容器的内网 IP
    server app1:80;
    server app2:80;
}

server {
    listen 80;
    server_name localhost;

    # 1. 静态文件服务测试
    # 访问 http://localhost/ 时,读取 /usr/share/nginx/html 下的文件
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    # 2. 反向代理测试
    # 访问 http://localhost/api 时,请求会被转发给 app1 容器
    location /api {
        proxy_pass http://app1:80;
        
        # 将客户端的真实 IP 传递给后端
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # 3. 负载均衡测试
    # 访问 http://localhost/lb 时,请求会轮询转发给 app1 和 app2
    location /lb {
        proxy_pass http://backend_servers;
    }
}

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Nginx 学习环境</title>
</head>
<body>
    <h1>🎉 恭喜!Nginx 静态文件服务配置成功!</h1>
    <p>现在可以测试反向代理和负载均衡了:</p>
    <ul>
        <li><a href="/api" target="_blank">测试反向代理 (代理到单一后端)</a></li>
        <li><a href="/lb" target="_blank">测试负载均衡 (分发到多个后端)</a></li>
    </ul>
</body>
</html>


反向代理

反向代理(Reverse Proxy)方式是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。这个概念在内网渗透中也会经常遇到。


目录结构

  • /etc/nginx/nginx.conf:全局主配置文件
  • /etc/nginx/conf.d/ 或 /etc/nginx/sites-available/:子配置文件目录
  • /var/log/nginx/:日志目录(access.log 和 error.log)
  • /usr/share/nginx/html/:默认的静态网页存放目录

命令行操作

记几个常用的

nginx -s reload	# 重载配置
nginx -c filename	# 指定配置文件启动nginx

Nginx 内置变量

客户端与请求信息:

变量名 说明 示例值
$remote_addr 客户端的真实 IP 地址 192.168.1.100
$request_method HTTP 请求方法 GET、POST、OPTIONS
$scheme 请求使用的协议 http 或 https
$request_time Nginx 处理该请求花费的总时间(秒) 0.005

URL 与参数路径:

假设用户请求的完整网址是:http://www.test.com/api/user/info?id=123&name=tom

变量名 说明 示例值 (基于上方网址)
$host 请求的域名(或 IP) www.test.com
$request_uri 原始完整的 URI(包含问号后面的参数,在 Nginx 内部重写前的值) /api/user/info?id=123&name=tom
$uri 当前解析的 URI(不含参数。如果经过了 rewrite 重写,这里会变成重写后的值) /api/user/info
$args 或 $query_string 问号 ? 后面的所有参数字符串 id=123&name=tom
$arg_参数名 单独提取某个特定参数的值 $arg_id 的值是 123
$arg_name 的值是 tom

http 请求头:

原 HTTP Header 名称 对应的 Nginx 变量 常见用途
User-Agent $http_user_agent 获取浏览器信息/判断是不是手机端/拦截爬虫
Referer $http_referer 获取是从哪个网页跳过来的(防盗链)
Cookie $http_cookie 获取所有的 Cookie 字符串
Accept-Language $http_accept_language 判断访客语言(中/英),用于多语言跳转
X-Forwarded-For $http_x_forwarded_for 获取经过多层代理后的真实 IP 链

cookie 相关:

变量名 说明 示例
$cookie_session_id 获取名为 session_id 的 Cookie 值 abcdef123456

Upstream 后端相关:

变量名 说明 示例值
$upstream_addr 真正处理本次请求的后端服务器 IP 和端口 172.18.0.5:80
$upstream_status 后端服务器返回的 HTTP 状态码 200 或 502
$upstream_response_time 后端服务器处理请求花费的时间 1.503
$upstream_cache_status 缓存命中状态 HIT, MISS, EXPIRED

可以用下面这个配置查看部分变量的值:

server {
    listen 80;
    server_name localhost;

    location /test-vars {
        default_type application/json;
        
        return 200 '{
            "Client_IP": "$remote_addr",
            "Request_Method": "$request_method",
            "Host": "$host",
            "Original_URI": "$request_uri",
            "Parsed_URI": "$uri",
            "All_Args": "$args",
            "Specific_Arg_id": "$arg_id",
            "User_Agent": "$http_user_agent"
        }';
    }
}

配置学习

静态资源服务

学习 nginx 如何将浏览器请求的 URL 映射到硬盘上真实的文件夹

在 html/ 目录下创建 html/images 和 html/assets 目录,然后分别添加一个 1.jpg 与 1.txt

此时可以通过相对路径访问刚才添加的内容

接下来添加 default.conf 的配置

# 场景1:使用 root (追加模式)
location /images/ {
	root /usr/share/nginx/html;
}

# 场景2:使用 alias (替换模式)
location /static/ {
	alias /usr/share/nginx/html/assets/;
}

修改完后执行 nginx -s reload 重启 nginx 服务

此时的路由处理规则:

  • 访问 http://localhost/images/1.jpg (成功显示图片):root路径 + URL路径 = /usr/share/nginx/html + /images/logo.png
  • 访问 http://localhost/static/1.txt (成功显示文字):用 alias 路径替换掉 URL 匹配的部分 = 扔掉 /static/,换成 /usr/share/nginx/html/assets/ + test.txt

Location 匹配规则

匹配优先级:精确匹配 > 前缀匹配 > 正则匹配 > 通用匹配

修改 location 配置:

server {
    listen 80;
    server_name localhost;
    
    # 设置默认返回格式为纯文本,防止浏览器当作文件下载
    default_type text/plain;

    # 1. 通用匹配 (兜底)
    location / {
        return 200 "Matched: General /";
    }

    # 2. 正则匹配 (区分大小写,包含 'test' 字眼)
    location ~ /test {
        return 200 "Matched: Regex ~ /test";
    }

    # 3. 前缀匹配 (匹配以 /test 开头,且优先级高于正则)
    location ^~ /test/ {
        return 200 "Matched: Prefix ^~ /test/";
    }

    # 4. 精确匹配 (只匹配 /test_exact,差一个字母都不行)
    location = /test_exact {
        return 200 "Matched: Exact = /test_exact";
    }
}

反向代理配置

使用 proxy_pass 进行代理

server {
    listen 80;
    server_name localhost;

    # 测试一:proxy_pass 后面没有斜杠
    location /api1/ {
        proxy_pass http://app1:80;
    }

    # 测试二:proxy_pass 后面有斜杠
    location /api2/ {
        proxy_pass http://app1:80/;
    }

    # 测试三:传递请求头
    location /api3/ {
        proxy_pass http://app1:80;
        # 把客户端访问的域名传给后端
        proxy_set_header Host $host;
        # 使用$remote_addr获取客户端真实的IP,然后传给后端
        proxy_set_header X-Real-IP $remote_addr;
    }
}

访问 http://localhost/api1/user/123 时,此时请求的是

GET /api1/user/123

访问 http://localhost/api2/user/123 时,此时请求的是

GET /user/123

这就是 proxy_pass 中加斜杠的区别

如果我们访问内网时需要带上特定请求头,则使用 proxy_set_header 进行传递

同时测试可以发现

传递的 host 可以被篡改,但是 X-Real-Ip 则不会


负载均衡

使用 upstream 定义负载均衡组

最开始的环境中使用了负载均衡的配置,当我们访问 http://localhost/lb 时,不断 f5 刷新,会注意到 hostname 在 app1 和 app2 的容器 id 之间来回切换

# 定义一个负载均衡组,包含两个后端容器
upstream backend_servers {
    # 这里的 app1 和 app2 是 docker-compose.yml 中定义的服务名
    # Docker 的内部 DNS 会自动将它们解析为容器的内网 IP
    server app1:80;
    server app2:80;
}

server {
	listen 80;
	server_name localhost;
	
	# 访问 http://localhost/lb 时,请求会轮询转发给 app1 和 app2
	location /lb {
	    proxy_pass http://backend_servers;
	}
}

网站在实际运营过程中,代理往往不会仅指向一个服务器,大部分都是以集群的方式运行,这时需要使用负载均衡来分流。

# 1. 权重策略 (Weight)
upstream backend_weight {
    server app1:80 weight=3; # app1 处理 75% 的请求
    server app2:80 weight=1; # app2 处理 25% 的请求
    # server app3:80 backup; # 拓展:backup 作为备用机,平时不工作,全挂了才顶上
}

# 2. IP哈希策略 (IP Hash)
upstream backend_iphash {
    ip_hash;          # 根据客户端的 IP 算出一个哈希值,然后固定转发到同一台机器
    server app1:80;
    server app2:80;
}

server {
    listen 80;
    server_name localhost;

    location /weight {
        proxy_pass http://backend_weight;
    }
    
    location /iphash {
    proxy_pass http://backend_iphash;
	}
}

访问 http://localhost/weight 时,不断刷新页面,可以发现转发到 app1 与 app2 的比例大概在 3:1 左右,这就是负载均衡的权重作用,用于服务器性能不一致的场景

访问 http://localhost/iphash 时,不断刷新页面,发现 hostname 不变,此时 Nginx 在一直把你的请求派发给同一台后端,通常用于防止用户登录状态丢失


URL 重写与跳转

return 用于外部跳转

rewrite 用于内部转发

server {
    listen 80;
    server_name localhost;

    # 1. 直接返回与重定向 (Return)
    # 场景:旧页面下线了,引导用户去新页面
    location /old-page {
        return 301 /new-page; # 301 永久重定向,302 临时重定向
    }
    location /new-page {
        default_type text/plain;
        return 200 "This is the NEW page!";
    }

    # 2. 内部重写 (Rewrite)
    # 场景:为了 SEO 或隐藏后端真实的带参数的 URL
    # 用户访问 /user/123,Nginx 把它改成 /api?userid=123 传给后端
    location /user/ {
        # 正则捕获:将 /user/后面的数字存入变量 $1
        # break 标志:重写后立即发给后端,不再重新走 location 匹配
        rewrite ^/user/([0-9]+)$ /api?userid=$1 break;
        proxy_pass http://app1:80;
    }
}

配置 HTTPS(SSL/TLS)并强制跳转

生成证书

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/conf.d/cert.key -out /etc/nginx/conf.d/cert.crt -subj '/CN=localhost'
# HTTP 服务器:负责把所有 80 端口的请求,强制赶到 HTTPS
server {
    listen 80;
    server_name localhost;
    
    # 核心:将所有 http 请求 301 重定向到 https 对应的地址
    return 301 https://$host$request_uri;
}

# HTTPS 服务器
server {
    listen 443 ssl;
    server_name localhost;

    # 指定证书路径 (因为挂载原因,它们在容器内的 /etc/nginx/conf.d/ 下)
    ssl_certificate     /etc/nginx/conf.d/cert.crt;
    ssl_certificate_key /etc/nginx/conf.d/cert.key;

    # 一些常见的 SSL 安全优化配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        default_type text/plain;
        return 200 "Success! You are visiting over HTTPS 🔒";
    }
}

此时访问 http://localhost 就会自动跳转到 https://localhost


gzip 压缩与浏览器缓存

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;

    # ==========================
    # 1. 开启 Gzip 压缩
    # ==========================
    gzip on;
    gzip_min_length 1k;         # 小于1k的文件不压缩,压缩反而费CPU
    gzip_comp_level 4;          # 压缩级别(1-9),推荐 4-6
    # 指定需要压缩的文件类型 (图片视频本身已压缩,不要再用 gzip)
    gzip_types text/plain application/javascript text/css application/json text/xml;
    gzip_vary on;               # 在响应头中添加 Vary: Accept-Encoding

    # ==========================
    # 2. 静态资源缓存控制 (Cache)
    # ==========================
    # 匹配图片类型,让浏览器缓存 30 天
    location ~* \.(jpg|jpeg|png|gif|ico)$ {
        expires 30d;
        # 禁止记录访问日志,减少磁盘IO压力
        access_log off; 
    }

    # 匹配 css/js 类型,缓存 7 天
    location ~* \.(css|js)$ {
        expires 7d;
    }

    location / {
        index index.html;
    }
}

expires vs proxy_cache

expires 是让浏览器把文件缓存在用户的电脑上

proxy_cache 是让 nginx 把后端服务器生成的响应结果缓存在 nginx 服务器的硬盘/内存里

# ==========================================
# 1. 定义缓存区域 (必须放在 server 块外面)
# ==========================================
# levels=1:2    : 开启两级目录存储缓存文件,防止单目录文件过多导致性能下降
# keys_zone     : 内存区域名称为 my_cache,分配 10MB 内存存缓存的 Key
# max_size      : 硬盘缓存最大使用 1GB,满了会清理最旧的数据
# inactive      : 如果一个缓存数据 60 分钟没被访问,就自动清理
# use_temp_path : 建议设为 off,直接将缓存写入目标目录,避免临时文件拷贝
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;


server {
    listen 80;
    server_name localhost;

    # ==========================================
    # 2. 在具体的路由中开启缓存
    # ==========================================
    location /api/ {
        proxy_pass http://app1:80;

        # 开启缓存,并指定使用上面定义的 my_cache 这个区域
        proxy_cache my_cache;

        # ==========================================
        # 3. 配置缓存有效期 (proxy_cache_valid)
        # ==========================================
        # 状态码是 200 和 302 的响应,缓存 30 秒 (为了方便测试,我们设短一点)
        proxy_cache_valid 200 302 30s;
        # 状态码是 404 的响应,缓存 1 分钟 (防止恶意刷不存在的接口导致穿透)
        proxy_cache_valid 404 1m;

        # 缓存的 Key (默认是 URL,这里显式写出来)
        proxy_cache_key $host$request_uri;

        # ==========================================
        # 4. 调试技巧:给客户端返回缓存命中状态
        # ==========================================
        # 这行配置会在浏览器的响应头中增加一个 X-Cache-Status 字段
        # 它的值可能是: MISS(未命中), HIT(命中), EXPIRED(已过期), UPDATING(更新中)
        add_header X-Cache-Status $upstream_cache_status;
    }
}

防盗链

大多数知名网站上的图片都不能如 <img src="图片URL"> 这样直接引用

原因就是通过 referer 配置了防盗链

server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;

    location ~* \.(jpg|jpeg|png|gif)$ {
        # 核心指令:valid_referers
        # none: 允许直接在浏览器地址栏输入URL访问
        # blocked: 允许没有 Referer 头的请求
        # server_names: 允许来自本域名的请求
        # *.google.com: 允许谷歌等搜索引擎收录
        valid_referers none blocked server_names *.google.com;

        # 如果来源不合法 (invalid_referer),则执行下面的操作
        if ($invalid_referer) {
            # 做法1:直接返回 403 拒绝访问
            # return 403;
            
            # 做法2:返回一张防盗链提示图片
            rewrite ^/ /forbidden.png break; 
        }
    }
}

安全措施

# ==========================
# 定义限流内存区 (定义在 server 之外)
# ==========================
# 区域名为 my_limit,大小 10m,基于客户端 IP 限制,速率为 1个请求/秒
limit_req_zone $binary_remote_addr zone=my_limit:10m rate=1r/s;

server {
    listen 80;
    server_name localhost;

    # ==========================
    # 1. 隐藏版本号
    # ==========================
    # 关闭 Nginx 报错页或响应头中的版本号,防止黑客针对特定版本的漏洞攻击
    server_tokens off; 

    # ==========================
    # 2. IP 黑白名单 (访问控制)
    # ==========================
    location /admin/ {
        default_type text/plain;
        # 允许指定的内网 IP 或你自己的 IP 访问
        allow 127.0.0.1;
        allow 172.16.0.0/12; # Docker 内部网络范围
        # 拒绝其他所有人
        deny all;
        
        return 200 "Welcome Admin!";
    }

    # ==========================
    # 3. 限制请求速率
    # ==========================
    location /api/login {
        default_type text/plain;
        
        # 使用上面定义的 my_limit 区域
        # burst=5: 允许突发最多 5 个请求排队
        # nodelay: 超过排队数量的直接拒绝,不延迟处理
        limit_req zone=my_limit burst=5 nodelay;
        
        # 如果被限流,默认返回 503,这里为了明显改成返回 429 Too Many Requests
        limit_req_status 429;
        
        return 200 "Login Success!";
    }
}

不安全的配置

CRLF 注入

server {
	listen 8080;

	root /usr/share/nginx/html;

	index index.html;

	server_name _;

	location / {
		return 302 http://$host:$server_port$uri;
	}
}

$uri 此处是可控的,可以实现 CRLF 注入:http://your-ip:8080/%0d%0aSet-Cookie:%20a=1

目录穿越

location /files {
    alias /home/;
}

设置别名时,如果 location 的路由尾部没有 /,则会导致目录穿越:http://your-ip:8081/files../

此时 alias 仅替换 /files,那么访问文件的路由就变成了 /home/../

add_header被覆盖

add_header Content-Security-Policy "default-src 'self'";
add_header X-Frame-Options DENY;

location = /test1 {
    rewrite ^(.*)$ /xss.html break;
}

location = /test2 {
    add_header X-Content-Type-Options nosniff;
    rewrite ^(.*)$ /xss.html break;
}

此处的设计是在整站(父块中)添加了CSP头,但是 /test2 的 location 中又添加了 X-Content-Type-Options 头,会导致父块中的 add_header 被覆写


php cgi 配置错误

location ~ \.php$ {
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  /var/www/html$fastcgi_script_name;
    include        fastcgi_params;
}

当请求 file.jpg/.php 时,Nginx 会将其作为 PHP 文件处理并发送给 PHP-FPM 解析,尽管它实际上是一个图片文件