写在前面
本文是 Nginx 学习笔记系列的第三篇,聚焦生产环境实战:HTTPS 证书配置、性能优化技巧(压缩、缓存、限流、跨域),以及 .NET 应用部署和常见问题排查。
一、HTTPS 配置
1.1 为什么需要 HTTPS
1
2
3
4
5
6
7
8
9
10
|
HTTP 的问题:
- 明文传输 — 数据可被窃听
- 不验证身份 — 可能被冒充(钓鱼)
- 数据可被篡改 — 运营商插广告、劫持
HTTPS = HTTP + TLS
- 加密传输 — 数据被加密,无法窃听
- 身份验证 — 证书验证服务器身份
- 完整性校验 — 数据被篡改可检测
- SEO 加分 — Google 优先收录 HTTPS
|
1.2 申请免费证书(Let’s Encrypt)
1
2
3
4
5
6
7
8
9
10
11
|
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 自动获取并配置证书(一条命令搞定)
sudo certbot --nginx -d example.com -d www.example.com
# 仅获取证书(手动配置)
sudo certbot certonly --nginx -d example.com
# 手动获取(DNS 验证,适合泛域名)
sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com
|
1
2
3
4
5
6
7
8
|
证书文件位置:
/etc/letsencrypt/live/example.com/fullchain.pem — 证书(含中间证书)
/etc/letsencrypt/live/example.com/privkey.pem — 私钥
证书有效期:90 天
自动续期:Certbot 安装时会自动创建定时任务
查看:systemctl list-timers | grep certbot
手动续期:sudo certbot renew --dry-run
|
1.3 自动续期配置
1
2
3
4
5
6
7
8
9
10
|
# 检查自动续期定时任务
systemctl status certbot.timer
# 手动测试续期(不会真的续)
sudo certbot renew --dry-run
# 续期后自动重载 Nginx(配置 hook)
# /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
#!/bin/bash
nginx -t && systemctl reload nginx
|
1.4 手动配置 HTTPS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
server {
# HTTP → HTTPS 重定向
listen 80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl http2; # 开启 HTTP/2
server_name www.example.com;
# 证书配置
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSL 参数
ssl_protocols TLSv1.2 TLSv1.3; # 只允许 TLS 1.2 和 1.3
ssl_ciphers HIGH:!aNULL:!MD5; # 高强度加密套件
ssl_prefer_server_ciphers on; # 优先使用服务器端的加密套件
# SSL 会话缓存
ssl_session_cache shared:SSL:10m; # 10MB 缓存,约 4万个会话
ssl_session_timeout 10m; # 会话超时 10 分钟
ssl_session_tickets off; # 禁用 Session Tickets(前向安全)
# OCSP Stapling(在线证书状态检查)
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS(告诉浏览器只能用 HTTPS 访问)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
|
1.5 SSL 安全等级测试
1
2
3
4
5
6
7
8
9
10
11
12
|
# 用 SSL Labs 测试(在线)
# https://www.ssllabs.com/ssltest/
# 用 openssl 测试连接
openssl s_client -connect example.com:443 -servername example.com
# 测试支持的协议
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# 查看证书信息
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -text -noout
|
1.6 自签名证书(开发环境)
1
2
3
4
5
6
|
# 生成自签名证书
openssl req -x509 -nodes -days 365 \
-newkey rsa:2048 \
-keyout selfsigned.key \
-out selfsigned.crt \
-subj "/CN=localhost"
|
1
2
3
4
5
6
7
|
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/nginx/ssl/selfsigned.crt;
ssl_certificate_key /etc/nginx/ssl/selfsigned.key;
}
|
二、性能优化
2.1 gzip 压缩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
http {
# 开启 gzip
gzip on;
gzip_vary on; # 添加 Vary: Accept-Encoding 头
gzip_proxied any; # 代理请求也压缩
gzip_comp_level 4; # 压缩级别 1-9(4 是最佳平衡点)
gzip_min_length 256; # 小于 256 字节不压缩
gzip_buffers 16 8k; # 压缩缓冲区
gzip_http_version 1.1; # 最低 HTTP 版本
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
application/atom+xml
image/svg+xml
font/opentype
font/ttf
font/woff2
application/wasm;
}
|
1
2
3
4
5
6
7
8
9
|
压缩效果对比:
未压缩 HTML(50KB)→ gzip 后(~8KB) 减少 84%
未压缩 CSS(100KB)→ gzip 后(~15KB) 减少 85%
未压缩 JS(200KB) → gzip 后(~50KB) 减少 75%
注意:
- 图片(jpg/png/gif)和视频已经压缩过,不要再 gzip
- gzip_comp_level 不是越高越好,6 以上 CPU 开销增大但压缩收益递减
- 推荐值 4-6
|
2.2 Brotli 压缩(比 gzip 更好)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 需要安装 ngx_brotli 模块
# Docker 可用 nginx:1.26-alpine 自行编译或使用 openresty
http {
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;
# 同时支持 gzip 和 brotli
# Nginx 会根据客户端 Accept-Encoding 自动选择
gzip on;
gzip_comp_level 4;
}
|
1
2
3
4
5
|
Brotli vs gzip:
Brotli 通常比 gzip 多压缩 15-25%
但压缩速度比 gzip 慢(CPU 开销更大)
大多数现代浏览器都支持
建议静态资源预压缩用 Brotli,动态内容用 gzip
|
2.3 浏览器缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
server {
root /var/www/html;
# 静态资源 — 长期缓存
location ~* \.(css|js)$ {
expires 1y; # 1 年
add_header Cache-Control "public, immutable";
# immutable:告诉浏览器不需要重新验证
}
# 图片 — 中期缓存
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif)$ {
expires 6M; # 6 个月
add_header Cache-Control "public";
}
# 字体 — 长期缓存
location ~* \.(woff|woff2|ttf|otf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML — 不缓存或短缓存
location ~* \.html$ {
add_header Cache-Control "no-cache";
# no-cache:每次都验证(304 Not Modified)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
缓存策略总结:
文件类型 策略 时间
──────────────────────────────────────────
HTML no-cache 每次验证
CSS/JS(带hash) immutable 1 年
图片 public 6 个月
字体 immutable 1 年
API 响应 no-store 不缓存
带 hash 的文件名(app.3a4b5c.js)→ 可以永久缓存
不带 hash 的文件名(app.js) → 不能长期缓存
|
2.4 限流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
http {
# 基于 IP 的请求速率限制
# binary_remote_addr:二进制格式的 IP,占用更少内存
# zone=api_limit:10m:共享内存区域,10MB(约 16万个 IP)
# rate=100r/s:每个 IP 每秒 100 个请求
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;
# 基于IP的并发连接数限制
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
server {
# 请求速率限制
location /api/ {
limit_req zone=api_limit burst=50 nodelay;
# burst=50:允许短时突发 50 个请求
# nodelay:突发请求不延迟处理
proxy_pass http://backend;
}
# 并发连接限制
location /download/ {
limit_conn conn_limit 10; # 每个 IP 最多 10 个并发连接
limit_rate 500k; # 每个连接限速 500KB/s
}
# 限流后的自定义响应
limit_req_status 429; # 返回 429 Too Many Requests
limit_conn_status 429;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
|
限流参数说明:
rate=10r/s — 每秒 10 个请求
rate=600r/m — 每分钟 600 个请求(更适合 API)
burst=N — 允许排队的请求数
nodelay — 突发请求立即处理(不延迟)
不加 burst:
超过 rate 立即返回 429
加 burst + nodelay:
允许瞬时突发,但总速率不超过 rate
|
2.5 跨域(CORS)配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
server {
# CORS 配置
location /api/ {
# 允许的源
add_header Access-Control-Allow-Origin "https://www.example.com" always;
# 允许的方法
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
# 允许的头部
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
# 允许携带凭证(Cookie)
add_header Access-Control-Allow-Credentials "true" always;
# 预检请求缓存时间
add_header Access-Control-Max-Age 3600 always;
# 处理 OPTIONS 预检请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "https://www.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
add_header Access-Control-Max-Age 3600 always;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
proxy_pass http://backend;
}
}
|
1
2
3
4
5
6
7
8
9
|
CORS 要点:
Access-Control-Allow-Origin:
- 只能设置一个源,不能设置多个
- 动态设置可以用 $http_origin 变量(但需维护白名单)
预检请求(OPTIONS):
- 浏览器自动发送
- 必须正确响应,否则实际请求不会发出
- 非简单请求(带自定义头、PUT/DELETE 等)会触发预检
|
2.6 动态 CORS(多域名白名单)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# 定义允许的域名
map $http_origin $cors_origin {
default "";
"https://www.example.com" "https://www.example.com";
"https://app.example.com" "https://app.example.com";
"https://admin.example.com" "https://admin.example.com";
"http://localhost:3000" "http://localhost:3000";
}
server {
location /api/ {
# 动态设置 CORS
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
if ($cors_origin = "") {
return 403; # 不在白名单的源直接拒绝
}
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Max-Age 3600 always;
return 204;
}
proxy_pass http://backend;
}
}
|
三、.NET 应用部署实战
3.1 部署架构
1
2
3
4
5
6
7
8
9
10
11
12
13
|
客户端 → Nginx(443/80)→ .NET 应用(5000)
Nginx 负责:
✓ HTTPS 终端(.NET 应用不需要处理 TLS)
✓ 静态文件服务(wwwroot)
✓ 压缩、缓存
✓ 限流、安全
✓ 负载均衡(多实例)
.NET 应用负责:
✓ API 处理
✓ 业务逻辑
✓ SSR(Blazor Server / Razor Pages)
|
3.2 发布 .NET 应用
1
2
3
4
5
6
7
8
|
# 发布应用
dotnet publish -c Release -o /app/publish
# 运行
cd /app/publish
./MyApp --urls http://0.0.0.0:5000
# 推荐用 systemd 管理
|
3.3 systemd 服务配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
# /etc/systemd/system/myapp.service
[Unit]
Description=My .NET Application
After=network.target
[Service]
Type=notify
WorkingDirectory=/app/publish
ExecStart=/app/publish/MyApp --urls http://0.0.0.0:5000
Restart=always
RestartSec=10
SyslogIdentifier=myapp
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://0.0.0.0:5000
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
[Install]
WantedBy=multi-user.target
|
1
2
3
4
5
6
7
8
9
10
|
# 启用并启动
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp
# 查看状态
sudo systemctl status myapp
# 查看日志
sudo journalctl -u myapp -f
|
3.4 Nginx 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
# /etc/nginx/conf.d/myapp.conf
# HTTP → HTTPS 重定向
server {
listen 80;
server_name api.example.com;
return 301 https://$host$request_uri;
}
# HTTPS
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL 证书
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# SSL 参数
include /etc/nginx/snippets/ssl-params.conf;
# 安全头部
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000" always;
# 请求体大小
client_max_body_size 50m;
# API 请求转发到 .NET
location /api/ {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# Swagger(仅开发/测试环境)
location /swagger/ {
proxy_pass http://127.0.0.1:5000/swagger/;
}
# 健康检查
location /health {
access_log off;
proxy_pass http://127.0.0.1:5000/health;
}
}
|
3.5 ASP.NET Core 配合 Nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 配置转发头(必须在使用路径基之前)
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build();
// 必须在其他中间件之前调用
app.UseForwardedHeaders();
// 其他中间件
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// appsettings.json
{
"Urls": "http://0.0.0.0:5000",
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:5000"
}
},
"Limits": {
"MaxRequestBodySize": 52428800
}
}
}
|
1
2
3
4
|
关键配置:
UseForwardedHeaders() — 让 ASP.NET Core 读取 X-Forwarded-* 头
KnownNetworks.Clear() — 信任所有代理(按需限制)
Kestrel 只监听 HTTP — HTTPS 由 Nginx 处理
|
3.6 Docker Compose 部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
version: '3.8'
services:
myapp:
image: myapp:latest
container_name: myapp
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://0.0.0.0:5000
expose:
- "5000"
restart: always
networks:
- internal
nginx:
image: nginx:1.26
container_name: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf:/etc/nginx
- ./nginx/ssl:/etc/nginx/ssl
- ./nginx/logs:/var/log/nginx
depends_on:
- myapp
restart: always
networks:
- internal
networks:
internal:
internal: true # 内部网络,不对外暴露
|
1
2
3
4
5
|
好处:
- myapp 只在内部网络,外部无法直接访问
- Nginx 是唯一的入口
- 升级 myapp 不影响 Nginx
- 可以轻松扩展 myapp 实例
|
四、常见问题排查
4.1 502 Bad Gateway
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
原因:Nginx 无法连接到后端服务
排查步骤:
1. 检查后端是否运行
systemctl status myapp
curl http://127.0.0.1:5000/health
2. 检查端口是否正确
ss -tlnp | grep 5000
3. 检查防火墙
sudo iptables -L -n
4. 查看 Nginx 错误日志
tail -f /var/log/nginx/error.log
# 常见错误:
# connect() failed (111: Connection refused)
# connect() failed (113: No route to host)
|
4.2 504 Gateway Timeout
1
2
3
4
5
6
7
8
|
原因:后端响应超时
解决:
# 增加超时时间
proxy_read_timeout 120s; # 默认 60s,慢接口可以增大
proxy_connect_timeout 10s;
# 或者优化后端性能
|
4.3 413 Request Entity Too Large
1
2
3
4
|
原因:请求体超过限制
解决:
client_max_body_size 50m; # 默认 1m,按需调大
|
4.4 配置不生效
1
2
3
4
5
6
7
8
9
10
11
12
|
# 检查配置语法
nginx -t
# 重新加载配置
nginx -s reload
# 检查是否加载了正确的配置文件
nginx -T | grep "configuration file"
# 确认 include 路径是否正确
# 检查是否有缓存
curl -H "Cache-Control: no-cache" http://localhost
|
4.5 权限问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
常见错误:403 Forbidden
排查:
1. 检查文件权限
ls -la /var/www/html/
# Nginx 用户(www-data)需要读取权限
2. 检查 SELinux(CentOS/RHEL)
getenforce
# 如果是 Enforcing,需要设置上下文
chcon -R -t httpd_sys_content_t /var/www/html/
3. 检查 user 指令
# nginx.conf 中的 user 是否有权限访问文件
user www-data;
|
4.6 性能排查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# 查看 Nginx 状态(需要 stub_status 模块)
location /nginx_status {
stub_status;
access_log off;
allow 127.0.0.1;
deny all;
}
# 访问状态
curl http://localhost/nginx_status
# Active connections: 5
# server accepts handled requests
# 10 10 100
# Reading: 0 Writing: 1 Waiting: 4
# 分析访问日志(慢请求)
awk '$NF > 1 {print $0}' /var/log/nginx/access.log | sort -kNF -rn | head -20
# 查看 Nginx 进程状态
ps aux | grep nginx
top -p $(pgrep nginx | tr '\n' ',')
|
五、生产环境检查清单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
# ✅ 一个完整的生产级配置模板
server {
listen 443 ssl http2;
server_name example.com;
# --- SSL ---
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_stapling on;
ssl_stapling_verify on;
# --- 安全头部 ---
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# --- 性能 ---
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_comp_level 4;
gzip_types text/plain text/css application/javascript application/json;
# --- 安全 ---
client_max_body_size 50m;
server_tokens off; # 隐藏 Nginx 版本号
# --- 代理 ---
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP → HTTPS
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
|
六、小结
本文学习了 Nginx 生产环境实战:
- HTTPS 配置(Let’s Encrypt、手动配置、安全参数)
- SSL 安全优化(HSTS、OCSP Stapling、安全头部)
- 性能优化(gzip、Brotli、浏览器缓存、限流)
- CORS 跨域配置(静态和动态白名单)
- .NET 应用部署(systemd、Docker Compose)
- ASP.NET Core 配合 Nginx(ForwardedHeaders)
- 常见问题排查(502、504、413、权限、性能)
- 生产环境配置检查清单
下一篇将深入 Nginx 的底层原理:进程模型、事件驱动机制、请求处理流程和内存管理。