Day 9-3 HTTPS 与安全加固

Day 9-3 HTTPS 与安全加固 本节目标:为 Nginx 配置 HTTPS 并完成基础安全加固,让 Nginx 从“流量入口”升级为“安全入口”。 实验环境说明:本节沿用 Day 9-2 的实验环境——lb1 为 Nginx 接入层,backend1/backend2/backend3 为三

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 的 TCP 握手、证书检查、密钥交换与加密传输

目标 说明 为什么重要
配置 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 为什么需要防盗链?

盗链:其他网站直接引用你服务器的图片、视频等静态资源,消耗你的带宽和服务器资源。

防盗链核心逻辑:

盗链 Referer 不匹配返回 403,本站 Referer 匹配则返回图片

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 攻击 大量并发请求耗尽连接池

限流算法:漏桶。

漏桶缓冲、稳定流出及超出容量返回 429

元素 说明
请求 客户端发起的 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 个请求

原文中 5 个登录请求与 burst=3 的流程图

本段及流程图与上一节“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 速率慢慢处理。

评论