Day 9-3 HTTPS 与安全加固
本节目标:为 Nginx 配置 HTTPS 并完成基础安全加固,让 Nginx 从“流量入口”升级为“安全入口”。
实验环境说明:本节沿用 Day 9-2 的实验环境——lb1 为 Nginx 接入层,backend1/backend2/backend3 为三台 Nginx 模拟的后端服务(监听 8080 端口)。静态资源由 LB 直接返回,动态请求(/api/)转发到后端。Day 9-2 中已在 LB 上准备了 /var/www/nginx/ 静态资源,本节直接复用。

| 目标 | 说明 | 为什么重要 |
|---|---|---|
| 配置 HTTPS | 使用 Certbot 或私有 CA 配置 SSL 证书 | 加密传输,防止密码/数据被窃听 |
| 强制跳转 HTTPS | 所有 HTTP 请求自动跳转到 HTTPS | 防止用户误用不安全的 HTTP |
| 配置安全头 | HSTS、X-Frame-Options 等响应头 | 防止浏览器侧攻击 |
| 配置防盗链 | 防止静态资源被其他网站盗用 | 节省带宽,保护内容 |
| 配置限流 | 限制请求速率,保护后端服务 | 防止恶意刷接口、CC 攻击 |
| 基础安全加固 | 隐藏版本号、限制请求方法 | 防止攻击者利用已知漏洞 |
一、HTTPS 配置
1.1 为什么需要 HTTPS?
| 问题 | 明文传输(HTTP) | 加密传输(HTTPS) |
|---|---|---|
| 密码提交 | 网络中可被截获 | 加密后无法读取 |
| Cookie 窃取 | 中间人可窃取 Session | 加密保护 |
| 页面内容篡改 | 运营商可插入广告 | 防篡改 |
| 用户信任 | 浏览器显示“不安全” | 显示小锁 |
1.2 确认 SSL 模块已安装
[root@lb1 ~]# nginx -V 2>&1 | grep with-http_ssl_module
with-http_ssl_module # 有输出说明已安装
1.3 SSL 证书获取方式
| 方式 | 说明 | 适用场景 |
|---|---|---|
| Certbot(Let's Encrypt) | 免费公网证书,自动续期 | 有公网域名的网站 |
| 私有 CA | 自己搭建证书颁发体系 | 内网环境、开发测试 |
1.4 方式一:Certbot 自动签发(公网环境)
# 安装 Certbot
[root@lb1 ~]# dnf install -y certbot python3-certbot-nginx
# 一键申请并配置(需要域名已解析到本机,80 端口可访问)
[root@lb1 ~]# certbot --nginx -d ruoyi.magedu.com
# 查看证书状态
[root@lb1 ~]# certbot certificates
# 测试自动续期(证书有效期 90 天)
[root@lb1 ~]# certbot renew --dry-run
说明:Certbot 默认使用 HTTP-01 挑战——在 /.well-known/acme-challenge/ 路径放置验证文件,Let's Encrypt 服务器通过 HTTP 访问验证域名所有权。--nginx 插件会自动处理验证文件的上传和清理,对用户完全透明。
1.5 方式二:私有 CA 签发(内网环境)
为什么需要私有 CA? 内网服务通常没有公网域名,无法通过 Let's Encrypt 验证域名所有权。私有 CA 让你在内部网络中自己签发证书。
# 1. 创建目录
[root@lb1 ~]# mkdir -p /opt/ca
[root@lb1 ~]# cd /opt/ca
# 2. 生成根 CA 私钥(4096 位,更安全)
[root@lb1 ~]# openssl genrsa -out ca.key 4096
[root@lb1 ~]# chmod 400 ca.key
| 参数 | 说明 |
|---|---|
| genrsa | 生成 RSA 私钥(OpenSSL 子命令) |
| -out ca.key | 输出私钥文件名 |
| 4096 | 密钥长度(位)。根 CA 建议 4096 |
| chmod 400 | 私钥只读,防止误删/修改 |
# 3. 生成根 CA 自签名证书(有效期 10 年)
[root@lb1 ~]# openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MageEdu/OU=IT/CN=MageEdu Root CA"
| 参数 | 说明 |
|---|---|
| req | 证书请求和生成工具 |
| -x509 | 生成自签名证书(根 CA 由自己签发) |
| -new | 生成新的证书请求 |
| -nodes | 不加密私钥(避免重启需输入密码) |
| -key ca.key | 指定用于签名的私钥 |
| -sha256 | 使用 SHA-256 哈希算法 |
| -days 3650 | 有效期:10 年 |
| -out ca.crt | 输出证书文件名 |
| -subj | 证书主题信息 |
# 4. 签发通配符服务器证书(覆盖 *.magedu.com)
[root@lb1 ~]# mkdir -p /opt/ca/certs/magedu.com
[root@lb1 ~]# cd /opt/ca/certs/magedu.com
# 4a. 生成服务器私钥(2048 位即可)
[root@lb1 ~]# openssl genrsa -out magedu.key 2048
[root@lb1 ~]# chmod 400 magedu.key
# 4b. 生成证书签名请求(CSR)
[root@lb1 ~]# openssl req -new -key magedu.key -sha256 \
-out magedu.csr \
-subj "/C=CN/ST=Beijing/L=Beijing/O=MageEdu/OU=IT/CN=*.magedu.com"
# 4c. 用根 CA 签发服务器证书(有效期 1 年)
[root@lb1 ~]# openssl x509 -req -in magedu.csr \
-CA /opt/ca/ca.crt -CAkey /opt/ca/ca.key -CAcreateserial \
-out magedu.crt -days 365 -sha256
# 5. 验证证书
[root@lb1 ~]# openssl verify -CAfile /opt/ca/ca.crt magedu.crt
magedu.crt: OK
原文签发命令只有 CN,没有加入 subjectAltName(SAN)。现代浏览器通常要求 SAN;
openssl verify返回 OK 仅说明链验证通过,不代表浏览器主机名验证也通过。通配符*.magedu.com也不覆盖根域名magedu.com。
1.6 客户端导入根证书
私有 CA 签发的证书,浏览器/系统默认不信任。需要手动导入根证书 ca.crt。Certbot(Let's Encrypt)证书已内置信任,无需导入。
各操作系统导入方法:
| 操作系统 | 操作步骤 |
|---|---|
| Windows | 双击 ca.crt → 安装证书 → 本地计算机 → 受信任的根证书颁发机构 |
| Linux(Rocky/CentOS) | cp ca.crt /etc/pki/ca-trust/source/anchors/ && update-ca-trust |
| Linux(Ubuntu/Debian) | cp ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates |
| macOS | 双击 ca.crt → 钥匙串访问 → 系统 → 始终信任 |
验证:导入后 curl https://ruoyi.magedu.com/ 应不再报 SSL 错误。
1.7 HTTPS 基础配置模板
# /etc/nginx/conf.d/ruoyi-https.conf
upstream ruoyi_backend {
server 192.168.100.11:8080;
server 192.168.100.12:8080;
server 192.168.100.13:8080;
}
server {
listen 443 ssl http2;
server_name ruoyi.magedu.com magedu.com;
# ===== 方式一:Certbot 证书(公网)=====
# ssl_certificate /etc/letsencrypt/live/ruoyi.magedu.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/ruoyi.magedu.com/privkey.pem;
# ===== 方式二:私有 CA 证书(内网)=====
ssl_certificate /opt/ca/certs/magedu.com/magedu.crt;
ssl_certificate_key /opt/ca/certs/magedu.com/magedu.key;
# ===== TLS 优化 =====
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers 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;
server_tokens off;
# ===== API 请求:转发到后端 =====
location /api/ {
proxy_pass http://ruoyi_backend;
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;
}
# ===== 其他请求:LB 直接返回静态文件 =====
location / {
root /var/www/nginx;
try_files $uri $uri/ /index.html;
}
}
# ===== HTTP 跳转 HTTPS =====
server {
listen 80;
server_name ruoyi.magedu.com magedu.com;
return 301 https://$host$request_uri;
}
1.8 验证 HTTPS
[root@lb1 ~]# nginx -t && systemctl reload nginx
# 测试 HTTPS 访问
[root@lb1 ~]# curl -k https://ruoyi.magedu.com/
LB Home Page
# 验证 HTTP 跳转
[root@lb1 ~]# curl -I http://ruoyi.magedu.com/ | grep Location
Location: https://ruoyi.magedu.com/
# 测试 API 代理(转发到后端)
[root@lb1 ~]# curl -k https://ruoyi.magedu.com/api/user
{"code":200,"data":"Backend 1"}
1.9 请求流向总览
| 请求 | 匹配规则 | 处理位置 | 说明 |
|---|---|---|---|
| GET /index.html | location / | LB 直接返回 | try_files 返回静态文件 |
| GET /images/photo.jpg | location / | LB 直接返回 | try_files 返回静态文件 |
| GET /api/user | location /api/ | LB → 后端 | 转发到后端 |
| GET / | location / | LB 直接返回 | 返回 /index.html |
二、安全加固
2.1 为什么需要这些安全加固?
| 安全问题 | 风险 | 加固措施 |
|---|---|---|
| 显示版本号 | 攻击者利用特定版本漏洞 | 隐藏版本号 |
| 危险 HTTP 方法 | PUT/DELETE 可被用于非法上传/删除 | 只允许 GET/HEAD/POST |
| 目录浏览 | 暴露网站文件结构 | 禁止 autoindex |
2.2 隐藏版本号
# /etc/nginx/nginx.conf
http {
server_tokens off;
}
验证:
[root@lb1 ~]# curl -I https://ruoyi.magedu.com/
Server: nginx # 不再显示版本号
2.3 限制 HTTP 危险方法
server {
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
}
验证:
[root@lb1 ~]# curl -X PUT -I https://ruoyi.magedu.com/
curl: (52) Empty reply from server
2.4 禁止目录浏览
# /etc/nginx/nginx.conf
http {
autoindex off;
}
三、防盗链配置
3.1 为什么需要防盗链?
盗链:其他网站直接引用你服务器的图片、视频等静态资源,消耗你的带宽和服务器资源。
防盗链核心逻辑:

3.2 valid_referers 参数详解
valid_referers none blocked server_names ruoyi.magedu.com magedu.com *.magedu.com;
| 参数 | 含义 | 匹配场景 |
|---|---|---|
| none | 允许没有 Referer 的请求 | 地址栏输入、书签打开 |
| blocked | 允许 Referer 被防火墙/代理移除的请求 | 企业防火墙、隐私插件 |
| server_names | 允许 Referer 匹配 server_name 列表 | 本站页面发起的请求 |
| ruoyi.magedu.com | 明确允许的域名 | 从该域名发起的请求 |
| *.magedu.com | 通配符匹配子域名 | 从任意 *.magedu.com 发起的请求 |
原文把 blocked 描述为 Referer 被“移除”;严格来说,它匹配的是存在但不以
http://或https://开头的 Referer 值。完全没有 Referer 对应 none。
3.3 配置防盗链
server {
# ... 原有 HTTPS 配置 ...
# ===== 防盗链:静态资源由 LB 直接返回 =====
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js)$ {
root /var/www/nginx;
valid_referers none blocked server_names ruoyi.magedu.com magedu.com *.magedu.com;
if ($invalid_referer) {
return 403;
}
expires 30d;
access_log off;
}
# ===== API 请求:转发到后端 =====
location /api/ {
proxy_pass http://ruoyi_backend;
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;
}
# ===== 其他请求:LB 直接返回静态文件 =====
location / {
root /var/www/nginx;
try_files $uri $uri/ /index.html;
}
}
3.4 验证防盗链
# 1. 本站访问 → 正常返回
[root@lb1 ~]# curl --header "Referer: https://ruoyi.magedu.com/" \
https://ruoyi.magedu.com/images/photo.jpg
Valid image
# 2. 盗链访问 → 403 Forbidden
[root@lb1 ~]# curl -H "Referer: http://evil.com/" \
https://ruoyi.magedu.com/images/photo.jpg
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>
# 3. 直接访问 → 正常返回(匹配 none)
[root@lb1 ~]# curl https://ruoyi.magedu.com/images/photo.jpg
Valid image
四、限流配置
4.1 为什么需要限流?
限流:限制客户端在单位时间内的请求数量,防止单个客户端耗尽服务器资源。
| 风险场景 | 后果 |
|---|---|
| 恶意刷接口 | CPU 飙升,正常用户无法访问 |
| 暴力破解密码 | 登录接口被大量尝试 |
| CC 攻击 | 大量并发请求耗尽连接池 |
限流算法:漏桶。

| 元素 | 说明 |
|---|---|
| 请求 | 客户端发起的 HTTP 请求,进入漏桶等待处理 |
| 漏桶缓冲区 | 临时存放请求,起到“削峰填谷”的作用 |
| 稳定速率流出 | 请求以固定速率(如 rate=10r/s)流出到后端,保护后端不被突发流量冲垮 |
| 超出容量 | 漏桶容量有限(burst=20),装不下的请求直接丢弃 |
| 返回 429 | 超出容量的请求返回 429 Too Many Requests,告诉客户端稍后再试 |
4.2 配置限流
以下保留原文配置:
# /etc/nginx/nginx.conf
http {
# 定义限流区域
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# 自定义 429 响应
error_page 429 = @rate_limit;
location @rate_limit {
return 429 '{"code":429,"message":"请求过于频繁"}';
add_header Content-Type application/json;
}
}
# /etc/nginx/conf.d/ruoyi-https.conf
server {
# 登录接口:每秒 1 次(防暴力破解)
location /api/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://ruoyi_backend/api/login;
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;
}
# 普通 API:每秒 10 次
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://ruoyi_backend;
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;
}
}
原文配置存在两个问题:
location不能直接放在 http 中,应放到对应 server 内;拒绝请求默认返回 503,要得到本节的 429 结果还需设置limit_req_status 429;。下方综合示例也保留了这些原文问题,实际使用前需修正。
4.3 验证限流
# 验证普通 API 限流(每秒 10 次)
for i in {1..20}; do
curl -s -o /dev/null -w "%{http_code}\n" https://ruoyi.magedu.com/api/user
done | sort | uniq -c
# 预期:约 10 个 200,10 个 429
# 验证登录接口限流(每秒 1 次,burst=3)
for i in {1..10}; do
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://ruoyi.magedu.com/api/login
done | sort | uniq -c
# 预期:4 个 200(1+3),6 个 429
这个命令在做什么?
这是一条限流验证命令,用于统计 20 次请求中有多少被正常处理(200)、多少被限流拒绝(429)。
for i in {1..20}; do
curl -s -o /dev/null -w "%{http_code}\n" https://ruoyi.magedu.com/api/user
done | sort | uniq -c
| 部分 | 含义 |
|---|---|
| for i in {1..20}; do ... done | 循环执行 20 次 |
| curl -s | 静默模式,不显示进度条和统计信息 |
| -o /dev/null | 丢弃响应内容(只看状态码) |
| -w "%{http_code}\n" | 输出 HTTP 状态码(如 200、429) |
| | sort | 将 20 个状态码排序 |
| | uniq -c | 统计每个状态码出现的次数 |
输出示例:
[root@lb1 ~]# for i in {1..20}; do
curl -s -o /dev/null -w "%{http_code}\n" https://ruoyi.magedu.com/api/user
done | sort | uniq -c
10 200
10 429
解读:20 次请求中,10 次成功(200),10 次被限流拒绝(429)。
上述为原文预期输出。实际数量取决于请求间隔、burst、先前的限流状态和处理耗时;带 burst=20 nodelay 的 20 次突发请求不能简单断定为 10 次成功、10 次拒绝。
4.4 详细剖析
每秒最多处理 1 个登录请求,瞬间最多允许 3 个请求排队(立即处理)。
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; # ① 定义限流规则
limit_req zone=login burst=3 nodelay; # ② 应用限流
| 部分 | 含义 |
|---|---|
| rate=1r/s | 后端每秒只能处理 1 个登录请求 |
| burst=3 | 漏桶容量为 3,瞬间最多容纳 3 个请求 |
| nodelay | 桶里的 3 个请求不排队,立即处理 |
工作流程(原文):
第 1 秒内涌入 5 个登录请求:
第 1 个 → 立即处理(消耗 1 个桶位)
第 2 个 → 立即处理(消耗 1 个桶位)
第 3 个 → 立即处理(消耗 1 个桶位)
第 4 个 → 桶已满(burst=3),返回 429
第 5 个 → 桶已满,返回 429
然后桶以 1r/s 的速度恢复:
1 秒后 → 恢复 1 个空位,可以再处理 1 个请求

本段及流程图与上一节“1+3=4 个成功”不一致。空闲状态下,burst=3 通常允许首个请求及额外 3 个突发请求,nodelay 使它们立即处理;rate 是长期平均限制,并非任意一秒内最多放行 1 次。
暴力破解防护
| 配置 | 效果 |
|---|---|
| rate=1r/s | 攻击者每秒最多尝试 1 次密码 |
| burst=3 | 偶尔手误输错密码,可以快速重试 3 次 |
| nodelay | 这 3 次重试不用等待,立即响应 |
1 小时最多尝试:3600 + 3 = 3603 次(只有 3 次是突发优惠)。
限制登录 = 每秒 1 次,burst=3 给你 3 次快速重试机会,用完就拒绝。
五、综合配置示例
以下保留原文综合配置;除 4.2 已注明的问题外,原文将 server_tokens off; 放在全局主上下文,此指令应放在 http、server 或 location 中。
# /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
server_tokens off;
events {
worker_connections 10240;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ===== 限流定义 =====
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# ===== 自定义 429 响应 =====
error_page 429 = @rate_limit;
location @rate_limit {
return 429 '{"code":429,"message":"请求过于频繁"}';
add_header Content-Type application/json;
}
# ===== 日志格式 =====
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# ===== 禁止目录浏览 =====
autoindex off;
include /etc/nginx/conf.d/*.conf;
}
# /etc/nginx/conf.d/ruoyi-https.conf
upstream ruoyi_backend {
server 192.168.100.11:8080;
server 192.168.100.12:8080;
server 192.168.100.13:8080;
}
server {
listen 443 ssl http2;
server_name ruoyi.magedu.com magedu.com;
# ===== 私有 CA 证书 =====
ssl_certificate /opt/ca/certs/magedu.com/magedu.crt;
ssl_certificate_key /opt/ca/certs/magedu.com/magedu.key;
# ===== TLS 优化 =====
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers 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;
server_tokens off;
# ===== 防盗链:静态资源由 LB 直接返回 =====
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|css|js)$ {
root /var/www/nginx;
valid_referers none blocked server_names ruoyi.magedu.com magedu.com *.magedu.com;
if ($invalid_referer) {
return 403;
}
expires 30d;
access_log off;
}
# ===== 登录接口限流 =====
location /api/login {
limit_req zone=login burst=3 nodelay;
proxy_pass http://ruoyi_backend/api/login;
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;
}
# ===== API 请求限流 =====
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://ruoyi_backend;
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;
}
# ===== 其他请求:LB 直接返回 =====
location / {
root /var/www/nginx;
try_files $uri $uri/ /index.html;
}
}
# ===== HTTP 跳转 HTTPS =====
server {
listen 80;
server_name ruoyi.magedu.com magedu.com;
return 301 https://$host$request_uri;
}
面试题
私有 CA 签发的证书为什么浏览器不信任?如何解决?
因为私有 CA 的根证书不在浏览器内置的受信任根证书列表中。解决方法:将私有 CA 的根证书(ca.crt)导入到浏览器的“受信任的根证书颁发机构”存储中。
防盗链配置中 valid_referers 的 none 和 blocked 分别是什么意思?
- none:允许没有 Referer 的请求(如地址栏直接输入 URL)。
- blocked:允许 Referer 被防火墙或代理移除的请求。
blocked 的严格含义见 3.2 的原文勘误说明。
Nginx 限流中 burst 和 nodelay 分别起什么作用?
- burst:漏桶容量,允许瞬间排队的最大请求数。
- nodelay:burst 内的请求不排队等待,立即处理。不加 nodelay 时请求会排队等待,按 rate 速率慢慢处理。
评论