企业级 Web 应用安全部署实战指南

1 阅读8分钟

企业级 Web 应用安全部署实战指南

基于 Docker + Nginx + Spring Boot 的生产环境安全部署最佳实践

背景

在现代 Web 应用开发中,安全部署与功能开发同等重要。本文将分享一套完整的企业级应用安全部署方案,涵盖 API 访问控制、Referer 校验、限流防护等核心安全措施的实施细节。

适用场景

  • SPA(单页应用)架构的前后端分离项目
  • 需要保护 API 不被外部直接调用的场景
  • 使用 Docker 容器化部署的生产环境

架构概述

技术栈

层级技术选型
前端Vue 3 + Vite
管理后台Vue 3 + Element Plus
后端Spring Boot 3.2 + MyBatis-Plus
数据库MySQL 8.0
缓存Redis 7
反向代理Nginx
容器化Docker Compose
SSL 证书云服务商免费证书

部署架构

                    ┌─────────────────────────────────────────┐
                    │              Internet                   │
                    └─────────────────┬───────────────────────┘
                                      │
                                      ▼
                    ┌─────────────────────────────────────────┐
                    │         Nginx (反向代理)                 │
                    │  - SSL 终止                             │
                    │  - Referer 校验                         │
                    │  - 安全响应头                           │
                    └─────────────────┬───────────────────────┘
                                      │
              ┌───────────────────────┼───────────────────────┐
              │                       │                       │
              ▼                       ▼                       ▼
    ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
    │   Frontend      │    │    Backend      │    │    Admin        │
    │   (Vue SPA)     │    │ (Spring Boot)   │    │   (Vue SPA)     │
    └─────────────────┘    └────────┬────────┘    └─────────────────┘
                                    │
                    ┌───────────────┼───────────────┐
                    │               │               │
                    ▼               ▼               ▼
            ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
            │   MySQL     │ │    Redis    │ │   uploads   │
            └─────────────┘ └─────────────┘ └─────────────┘

核心安全需求

业务安全需求

  1. API 访问控制:API 接口只能被官网和管理后台调用,禁止外部直接访问
  2. 敏感接口保护:管理后台接口必须登录后才能访问
  3. 防恶意攻击:防止恶意刷接口、暴力破解等攻击行为
  4. IP 访问限制:禁止通过 IP 地址直接访问服务器

安全威胁分析

威胁类型风险描述防护措施
API 滥用外部直接调用公开接口获取数据Referer 校验
恶意请求高频请求导致服务不可用Rate Limiting
数据泄露敏感接口未授权访问JWT 认证
中间人攻击HTTP 明文传输HTTPS 强制
点击劫持页面被嵌入恶意网站X-Frame-Options

解决方案

一、Nginx 安全配置详解

核心配置文件
# nginx.conf - 主配置文件
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    
    # 重要:设置 server_name 哈希桶大小
    server_names_hash_bucket_size 128;
    
    # 日志格式
    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;

    # 性能优化
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # Gzip 压缩
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json 
               application/javascript application/rss+xml;

    include /etc/nginx/conf.d/*.conf;
}
站点安全配置
# enterprise.conf - 站点配置

# ==========================================
# 1. 禁止 IP 直接访问
# ==========================================
server {
    listen 80 default_server;
    server_name _;
    return 444;  # 444 表示直接关闭连接,不返回任何响应
}

# ==========================================
# 2. HTTP 强制跳转 HTTPS
# ==========================================
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# ==========================================
# 3. 主服务 HTTPS 配置
# ==========================================
server {
    listen 443 ssl;
    server_name example.com www.example.com;
    
    # SSL 证书配置
    ssl_certificate /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    
    # 安全响应头
    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;
    
    # 请求体大小限制
    client_max_body_size 50M;
    
    # ==========================================
    # API 接口 - Referer 校验
    # ==========================================
    location /api/ {
        # 关键配置:使用正则表达式匹配域名
        valid_referers none blocked server_names ~\.example\.com;
        
        if ($invalid_referer) {
            return 403;
        }
        
        proxy_pass http://backend:18080;
        proxy_http_version 1.1;
        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;
    }
    
    # ==========================================
    # WebSocket 支持
    # ==========================================
    location /ws/ {
        valid_referers none blocked server_names ~\.example\.com;
        if ($invalid_referer) {
            return 403;
        }
        
        proxy_pass http://backend:18080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;
    }
    
    # ==========================================
    # 管理后台
    # ==========================================
    location /admin/ {
        alias /usr/share/nginx/admin/;
        try_files $uri $uri/ /admin/index.html;
    }
    
    # ==========================================
    # 官网前端
    # ==========================================
    location / {
        proxy_pass http://frontend;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
关键配置说明

⚠️ valid_referers 配置陷阱

错误写法

valid_referers none blocked server_names example.com www.example.com;

问题:直接写域名会导致与 server_name 参数冲突,报错:

conflicting parameter "example.com" in /etc/nginx/nginx.conf

正确写法

valid_referers none blocked server_names ~\.example\.com;

原因分析

  • server_names 已经包含了当前 server 块中定义的域名
  • 直接再写域名会被 Nginx 解析为重复定义
  • 使用正则表达式 ~\.example\.com 可以匹配所有子域名
valid_referers 参数说明
参数说明
none允许没有 Referer 头的请求(如直接输入 URL)
blocked允许 Referer 被防火墙修改的请求
server_names允许 Referer 为当前 server_name
~\.example\.com正则匹配,允许所有 example.com 子域名

二、后端安全防护实现

Rate Limiting(限流)

使用 Redis 实现分布式限流:

@Slf4j
@Component
@Order(2)
public class RateLimitFilter implements Filter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${rate.limit.enabled:true}")
    private boolean rateLimitEnabled;

    @Value("${rate.limit.requests-per-minute:60}")
    private int requestsPerMinute;

    @Value("${rate.limit.api-requests-per-minute:30}")
    private int apiRequestsPerMinute;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (!rateLimitEnabled) {
            chain.doFilter(request, response);
            return;
        }

        String clientIp = getClientIp(httpRequest);
        String path = httpRequest.getRequestURI();
        
        // 根据路径选择限流策略
        int limit = path.startsWith("/api/admin") ? apiRequestsPerMinute : requestsPerMinute;
        String key = "rate_limit:" + clientIp + ":" + path.startsWith("/api/admin") ? "admin" : "normal";

        try {
            Long currentCount = redisTemplate.opsForValue().increment(key);
            
            if (currentCount == 1) {
                redisTemplate.expire(key, 1, TimeUnit.MINUTES);
            }

            if (currentCount > limit) {
                log.warn("请求频率超限: ip={}, path={}, count={}", clientIp, path, currentCount);
                httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
                httpResponse.setContentType("application/json;charset=UTF-8");
                httpResponse.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
                return;
            }
        } catch (Exception e) {
            log.error("限流检查异常", e);
        }

        chain.doFilter(request, response);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
Security Audit(安全审计)

监控可疑行为并自动封禁:

@Slf4j
@Component
@Order(3)
public class SecurityAuditFilter implements Filter {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Value("${security.audit.enabled:true}")
    private boolean auditEnabled;

    @Value("${security.audit.suspicious-threshold:10}")
    private int suspiciousThreshold;

    private static final Set<String> SENSITIVE_PATHS = Set.of(
        "/api/auth/login",
        "/api/admin"
    );

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (!auditEnabled) {
            chain.doFilter(request, response);
            return;
        }

        String clientIp = getClientIp(httpRequest);
        String path = httpRequest.getRequestURI();

        // 检查是否被封禁
        String blockKey = "security:block:" + clientIp;
        if (Boolean.TRUE.equals(redisTemplate.hasKey(blockKey))) {
            log.warn("IP已被封禁: ip={}", clientIp);
            httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
            httpResponse.setContentType("application/json;charset=UTF-8");
            httpResponse.getWriter().write("{\"code\":403,\"message\":\"访问已被禁止\"}");
            return;
        }

        chain.doFilter(request, response);

        // 记录失败请求
        int status = httpResponse.getStatus();
        if (status >= 400 && isSensitivePath(path)) {
            String failKey = "security:fail:" + clientIp;
            Long failCount = redisTemplate.opsForValue().increment(failKey);
            redisTemplate.expire(failKey, 1, TimeUnit.HOURS);

            if (failCount >= suspiciousThreshold) {
                log.warn("检测到可疑行为,封禁IP: ip={}, failCount={}", clientIp, failCount);
                redisTemplate.opsForValue().set(blockKey, "1", 24, TimeUnit.HOURS);
            }
        }
    }

    private boolean isSensitivePath(String path) {
        return SENSITIVE_PATHS.stream().anyMatch(path::startsWith);
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}
配置文件
# application-prod.yml
rate:
  limit:
    enabled: true
    requests-per-minute: 60      # 普通接口限流
    api-requests-per-minute: 30   # 管理接口限流

security:
  audit:
    enabled: true
    suspicious-threshold: 10      # 失败次数阈值

三、Docker Compose 部署配置

# docker-compose.yml
services:
  nginx:
    image: nginx:alpine
    container_name: enterprise-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/enterprise.conf:/etc/nginx/conf.d/default.conf:ro
      - ./frontend/dist:/usr/share/nginx/html:ro
      - ./admin/dist:/usr/share/nginx/admin:ro
      - ./uploads:/var/www/uploads:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - backend
    networks:
      - enterprise-network

  backend:
    image: eclipse-temurin:17-jre-alpine
    container_name: enterprise-backend
    restart: unless-stopped
    working_dir: /app
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/enterprise_website
      - SPRING_DATASOURCE_USERNAME=enterprise
      - SPRING_DATASOURCE_PASSWORD=${DB_PASSWORD}
      - SPRING_DATA_REDIS_HOST=redis
      - JWT_SECRET=${JWT_SECRET}
    volumes:
      - ./backend/enterprise-backend.jar:/app/app.jar:ro
      - ./uploads:/app/uploads
    command: sh -c "java -Xms256m -Xmx512m -jar app.jar"
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - enterprise-network

  mysql:
    image: mysql:8.0
    container_name: enterprise-mysql
    restart: unless-stopped
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=enterprise_website
      - MYSQL_USER=enterprise
      - MYSQL_PASSWORD=${DB_PASSWORD}
    volumes:
      - mysql-data:/var/lib/mysql
    networks:
      - enterprise-network
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: enterprise-redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    networks:
      - enterprise-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  enterprise-network:
    driver: bridge

volumes:
  mysql-data:
  redis-data:

实践要点

安全验证测试

# 1. 测试外部 Referer(应返回 403)
curl -s -o /dev/null -w '%{http_code}' \
  -H 'Referer: https://evil.com/' \
  https://example.com/api/website/config --insecure
# 预期输出: 403

# 2. 测试官网 Referer(应返回 200)
curl -s -o /dev/null -w '%{http_code}' \
  -H 'Referer: https://example.com/' \
  https://example.com/api/website/config --insecure
# 预期输出: 200

# 3. 测试管理平台 Referer(应返回 200)
curl -s -o /dev/null -w '%{http_code}' \
  -H 'Referer: https://example.com/admin/' \
  https://example.com/api/website/config --insecure
# 预期输出: 200

# 4. 测试 IP 直接访问(应无响应)
curl -s -o /dev/null -w '%{http_code}' \
  http://192.168.1.100/ --connect-timeout 5
# 预期输出: 000 (连接被关闭)

# 5. 测试 HTTP 重定向
curl -s -o /dev/null -w '%{http_code}' \
  http://example.com/ -L --insecure
# 预期输出: 200 (自动跳转到 HTTPS)

测试结果

测试项预期结果实际结果状态
外部 Referer 访问 API403403
官网 Referer 访问 API200200
管理平台 Referer 访问 API200200
IP 直接访问444/无响应000
管理后台页面200200
官网首页200200

常见问题

Q1: Nginx 报错 "conflicting parameter"

问题

nginx: [emerg] conflicting parameter "example.com" in /etc/nginx/nginx.conf

原因valid_referers 中直接写域名与 server_name 冲突

解决方案:使用正则表达式

valid_referers none blocked server_names ~\.example\.com;

Q2: SPA 应用如何实现 API 签名验证?

分析:SPA 应用代码运行在用户浏览器中,无法安全存储签名密钥

解决方案

  1. 放弃客户端签名验证
  2. 使用 Referer 校验替代
  3. 后端实现 Rate Limiting
  4. 敏感接口强制 JWT 认证

Q3: 如何处理 Referer 为空的情况?

场景

  • 用户直接在地址栏输入 URL
  • 书签访问
  • 某些浏览器隐私模式

配置

valid_referers none blocked server_names ~\.example\.com;
  • none:允许无 Referer 的请求
  • blocked:允许 Referer 被代理/防火墙修改的请求

Q4: 如何防止 Referer 伪造?

说明:Referer 可以被伪造,但有一定门槛

多层防护策略

  1. Nginx Referer 校验:阻止简单的直接调用
  2. Rate Limiting:限制请求频率
  3. Security Audit:监控异常行为
  4. JWT 认证:敏感接口必须登录
  5. CORS 配置:限制跨域请求来源

安全最佳实践清单

网络层安全

  • 强制 HTTPS,禁用 HTTP
  • 禁止 IP 直接访问
  • 配置 HSTS 响应头
  • 使用 TLS 1.2+ 协议

应用层安全

  • API Referer 校验
  • Rate Limiting 限流
  • 敏感接口 JWT 认证
  • SQL 注入防护(使用参数化查询)
  • XSS 防护(输出编码)

容器安全

  • 使用最小化镜像(alpine)
  • 只读挂载配置文件
  • 环境变量管理敏感信息
  • 容器网络隔离

运维安全

  • 定期更新依赖版本
  • 日志监控与告警
  • 定期备份数据
  • 制定应急响应计划

总结

本文详细介绍了企业级 Web 应用的安全部署实践,核心要点:

  1. Nginx Referer 校验是保护 API 不被外部直接调用的有效手段,但需注意配置语法
  2. 多层防护策略比单一措施更可靠,应结合网络层、应用层、容器层的安全措施
  3. 安全与可用性的平衡:Referer 校验允许空值,避免影响正常用户体验
  4. 持续监控:通过 Security Audit 实时监控异常行为,自动封禁恶意 IP

安全是一个持续的过程,需要定期审查和更新防护策略,以应对不断演变的安全威胁。


参考资料


本文由「无边界科技」技术团队分享,专注软件开发与技术解决方案,转载请注明出处。官网:[wubianj.com](wubianj.com