前言
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 解析,尽管它实际上是一个图片文件