CC 咖啡猫的工作空间 Coding Space

网关与反向代理实践

实践导向的网关与反向代理开发手册。从 Nginx 到 Spring Cloud Gateway,覆盖常见生产场景的配置与代码。


〇、快速建立认知

一张图区分四个概念

        客户端(浏览器/App)
              │
              ▼
    ┌─────────────────┐
    │   反向代理/网关    │  ← 本章重点:请求的第一道入口
    │  (Nginx / Gateway) │
    └────────┬─────────┘
              │ 路由转发
    ┌─────────┼─────────┐
    ▼         ▼         ▼
 ServiceA  ServiceB  ServiceC
概念 一句话 典型工具
正向代理 帮你访问外网(翻墙/VPN) Squid、Shadowsocks
反向代理 帮你挡在服务前面,转发请求 Nginx、Apache httpd
API 网关 反向代理 + 鉴权/限流/路由/日志等能力 Spring Cloud Gateway、Kong、APISIX
负载均衡 把请求分到多台机器 Nginx upstream、Ribbon、Cloud LB

简单记:Nginx 是瑞士军刀——既可以做反向代理,也可以做负载均衡,加上 Lua/OpenResty 还能当网关。Spring Cloud Gateway 是 Java 生态的原生网关,与微服务体系天然集成。


一、Nginx 反向代理实践

1.1 核心配置文件结构

# /etc/nginx/nginx.conf

# 全局块
worker_processes auto;          # 工作进程数(auto = CPU 核心数)
events {
    worker_connections 1024;    # 每个进程最大连接数
}

# HTTP 块
http {
    include       mime.types;
    default_type  application/octet-stream;

    # 日志格式(JSON 格式便于 ELK 收集)
    log_format json escape=json '{'
        '"time":"$time_iso8601",'
        '"remote_addr":"$remote_addr",'
        '"request":"$request",'
        '"status":$status,'
        '"body_bytes":$body_bytes_sent,'
        '"request_time":$request_time,'
        '"upstream_addr":"$upstream_addr",'
        '"upstream_response_time":"$upstream_response_time"'
    '}';

    # 基础优化
    sendfile        on;         # 零拷贝传输文件
    keepalive_timeout 65;       # 长连接超时

    # 引入站点配置
    include /etc/nginx/conf.d/*.conf;
}

1.2 最简反向代理

# /etc/nginx/conf.d/app.conf
server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;   # 转给后端 Spring Boot
        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;
    }
}

每个 Header 的作用

Header 作用 后端怎么拿
Host 原始请求的域名 request.getServerName()
X-Real-IP 客户端真实 IP request.getHeader("X-Real-IP")
X-Forwarded-For 代理链(经过的每层代理追加) 取第一个 IP 是真实客户端
X-Forwarded-Proto 原始协议(http/https) 判断是否 HTTPS
// Java 后端获取真实 IP 的工具方法
public class IpUtils {
    public static String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        // X-Forwarded-For 可能是 "client, proxy1, proxy2",取第一个
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

1.3 负载均衡

# 定义上游服务器组
upstream backend {
    # 轮询(默认)
    server 192.168.1.10:8080 weight=3;   # 权重 3
    server 192.168.1.11:8080 weight=1;
    server 192.168.1.12:8080 backup;     # 备用,其他都挂了才启用

    # 其他策略(二选一):
    # ip_hash;              # 同一 IP 固定到同一台(会话保持)
    # least_conn;           # 最少连接数优先
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://backend;    # 填 upstream 的名字
        proxy_connect_timeout 3s;     # 连接后端超时
        proxy_read_timeout 30s;       # 读后端响应超时
        proxy_send_timeout 10s;       # 发请求给后端超时

        # 失败重试
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
    }
}

负载均衡策略对比

策略 原理 适合场景
轮询(默认) 挨个分,weight 大的多分 通用
ip_hash 按 IP hash 固定分配 需要会话保持
least_conn 发给连接数最少的 长连接/WebSocket
random 随机 简单场景

1.4 静态资源 + 动态请求分离

server {
    listen 80;
    server_name www.example.com;

    # 静态资源:Nginx 直接返回(不经过后端)
    location /static/ {
        root /data/www;
        expires 7d;                  # 浏览器缓存 7 天
        add_header Cache-Control "public, max-age=604800";
    }

    # 动态请求:转发后端
    location /api/ {
        proxy_pass http://backend;
    }
}

1.5 HTTPS 配置

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # 证书配置
    ssl_certificate     /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # 安全配置(关键!)
    ssl_protocols       TLSv1.2 TLSv1.3;          # 禁用 TLS 1.0/1.1
    ssl_ciphers         HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 10m;

    # HSTS(告诉浏览器永远用 HTTPS)
    add_header Strict-Transport-Security "max-age=63072000" always;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-Forwarded-Proto https;  # 告诉后端我走了 HTTPS
    }
}

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

1.6 常用 Nginx 生产片段

# 1. 限制请求频率(防 DDoS / 暴力破解)
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;

location /api/login {
    limit_req zone=login_limit burst=3 nodelay;
    proxy_pass http://backend;
}

# 2. 限制并发连接
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

location / {
    limit_conn conn_limit 10;       # 每个 IP 最多 10 个并发
    proxy_pass http://backend;
}

# 3. IP 白名单
location /admin {
    allow 10.0.0.0/8;              # 内网可以
    allow 203.0.113.0/24;           # 公司出口 IP
    deny all;                       # 其他全拒绝
    proxy_pass http://backend;
}

# 4. 跨域(前后端分离开发阶段用)
location /api/ {
    add_header Access-Control-Allow-Origin "https://www.example.com";
    add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS";
    add_header Access-Control-Allow-Headers "Content-Type,Authorization";
    add_header Access-Control-Allow-Credentials "true";

    if ($request_method = OPTIONS) {
        return 204;   # 预检请求直接返回
    }
    proxy_pass http://backend;
}

# 5. 设置请求体大小(上传文件)
client_max_body_size 10m;

二、Spring Cloud Gateway 实践

Nginx 处理网络层的事(HTTPS、静态资源、负载均衡),Spring Cloud Gateway 处理应用层的事(鉴权、限流、路由、日志、灰度)。

2.1 最简路由

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
# application.yml
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service        # lb = 从注册中心负载均衡
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1             # 去掉 /api,/api/users → /users

不需要写 Java 代码,只靠配置就完成了路由。

2.2 两种路由方式对比

方式 写法 适合
注册中心 uri: lb://user-service 微服务体系,服务自动发现
直连地址 uri: http://127.0.0.1:8080 简单场景、非微服务架构

2.3 核心概念:三个关键词

请求 → [Predicates(匹配)] → [Filters(加工)] → 后端服务

Predicates: 这个请求归我管吗?
Filters:    在转发前/后要做什么?
URI:        转发到哪?

2.4 自定义过滤器:统一鉴权

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    // 不需要鉴权的路径(白名单)
    private static final List<String> WHITELIST = List.of(
        "/api/auth/login",
        "/api/auth/register",
        "/api/public/"
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();

        // 1. 白名单直接放行
        if (WHITELIST.stream().anyMatch(path::startsWith)) {
            return chain.filter(exchange);
        }

        // 2. 提取 Token
        String token = exchange.getRequest()
            .getHeaders()
            .getFirst("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            return unauthorized(exchange, "缺少认证信息");
        }

        // 3. 校验 Token
        try {
            Claims claims = JwtUtils.parseToken(token.substring(7));
            // 4. 把用户信息传给下游(放在 Header 里)
            ServerHttpRequest request = exchange.getRequest().mutate()
                .header("X-User-Id", claims.get("userId", String.class))
                .header("X-User-Name", claims.get("username", String.class))
                .build();
            return chain.filter(exchange.mutate().request(request).build());
        } catch (Exception e) {
            return unauthorized(exchange, "Token 无效或已过期");
        }
    }

    private Mono<Void> unauthorized(ServerWebExchange exchange, String msg) {
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        byte[] body = ("{\"code\":401,\"msg\":\"" + msg + "\"}").getBytes();
        return exchange.getResponse()
            .writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(body)));
    }

    @Override
    public int getOrder() {
        return -100;  // 数字越小越先执行
    }
}

2.5 自定义过滤器:统一日志 + 耗时统计

@Component
public class RequestLogFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long start = System.currentTimeMillis();
        ServerHttpRequest request = exchange.getRequest();

        // 请求进来时记录
        log.info("[GATEWAY] --> {} {} from {}",
            request.getMethod(), request.getURI().getPath(),
            getClientIp(exchange));

        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            // 响应返回时记录
            long cost = System.currentTimeMillis() - start;
            HttpStatus status = exchange.getResponse().getStatusCode();
            log.info("[GATEWAY] <-- {} {}ms",
                status != null ? status.value() : "unknown", cost);
        }));
    }

    @Override
    public int getOrder() {
        return -200;  // 先于 AuthFilter 执行
    }
}

2.6 内置过滤器速览

Spring Cloud Gateway 提供大量开箱即用的过滤器,不需要自己写:

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            # 限流(Redis 令牌桶)
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10     # 每秒生成 10 个令牌
                redis-rate-limiter.burstCapacity: 20     # 最多积攒 20 个
            # 熔断(Resilience4j)
            - name: CircuitBreaker
              args:
                name: userServiceCB
                fallbackUri: forward:/fallback/user
            # 重试
            - name: Retry
              args:
                retries: 3
                statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
            # 请求体大小限制
            - name: RequestSize
              args:
                maxSize: 5MB
            # 重写路径
            - RewritePath=/api/users/(?<segment>.*), /$\{segment}
            # 添加响应头
            - AddResponseHeader=X-Response-Time, %{requestTime}

2.7 集成 Nacos 动态路由

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true           # 自动根据服务名创建路由
          lower-case-service-id: true

开启后,访问 http://gateway:8080/user-service/users 会自动路由到注册中心中名为 user-service 的服务。无需手动配置每个路由。

2.8 跨域配置(Java Config 方式)

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(List.of("https://*.example.com"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("Content-Type", "Authorization"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

三、Nginx + Gateway 组合部署(推荐架构)

生产环境推荐 Nginx 在前,Gateway 在后 的分层架构:

互联网
  │
  ▼
┌─────────────────────────┐
│  Nginx                   │  ← 网络层:HTTPS终结、静态资源、WAF、限流
│  (TLS/限流/静态资源)       │
└───────────┬──────────────┘
            │ HTTP(内网)
            ▼
┌─────────────────────────┐
│  Spring Cloud Gateway    │  ← 应用层:鉴权、路由、日志、灰度
│  (鉴权/路由/日志/灰度)     │
└───────────┬──────────────┘
            │
    ┌───────┼───────┐
    ▼       ▼       ▼
 ServiceA ServiceB ServiceC

分工原则

负责 工具
网络层 HTTPS、WAF、DDoS 防护、静态资源 Nginx / CDN
应用层 鉴权、路由、限流、日志、灰度 Spring Cloud Gateway
服务层 业务逻辑 Spring Boot

为什么不是只用一个?

  • Nginx 不擅长鉴权:每次鉴权都要调后端或写 Lua 脚本,开发和维护成本高
  • Gateway 不擅长网络层:Java 处理 TLS 性能不如 Nginx,静态资源更没必要经过 Java
  • Gateway 无状态、可水平扩展:放在 Nginx 后面可以随时加实例

四、生产注意事项

4.1 超时配置

# Nginx 侧
proxy_connect_timeout 3s;     # 连不上后端,3 秒放弃
proxy_read_timeout 30s;       # 后端 30 秒没返回,断开(上传/导出场景调大)
proxy_send_timeout 10s;       # 请求发不过去,10 秒放弃
# Gateway 侧
spring:
  cloud:
    gateway:
      httpclient:
        connect-timeout: 3000       # 连接超时(毫秒)
        response-timeout: 30s       # 响应超时

4.2 健康检查

upstream backend {
    server 192.168.1.10:8080 max_fails=3 fail_timeout=30s;  # 30秒内失败3次→摘除
    server 192.168.1.11:8080 max_fails=3 fail_timeout=30s;
}

Gateway 配合注册中心(Nacos/Eureka),服务下线自动摘除,不需要手动配置。

4.3 日志配置

# Nginx 侧(JSON 格式)
log_format json escape=json '{'
    '"time":"$time_iso8601",'
    '"ip":"$remote_addr",'
    '"method":"$request_method",'
    '"uri":"$request_uri",'
    '"status":$status,'
    '"rt":$request_time,'
    '"upstream":"$upstream_addr",'
    '"upstream_rt":"$upstream_response_time"'
'}';
access_log /var/log/nginx/access.log json;

关键字段说明

字段 含义 告警场景
$request_time 从收到请求到发完响应 >3s 告警
$upstream_response_time 后端处理时间 >1s 排查后端
$status 返回的 HTTP 状态码 5xx 比例 >1% 告警

4.4 日常运维命令

# 检查配置文件语法
nginx -t

# 热重载(不中断服务)
nginx -s reload

# 查看当前连接数
ss -antp | grep :80 | wc -l

# 查看 Nginx 错误日志
tail -f /var/log/nginx/error.log

# 压测网关
ab -n 10000 -c 100 http://api.example.com/api/users/

4.5 常见问题排查

现象 可能原因 排查方向
502 Bad Gateway 后端挂了或没启动 检查后端进程、端口、防火墙
504 Gateway Timeout 后端处理超时 调大 proxy_read_timeout,或优化慢接口
499 Client Closed 客户端等不及主动断开 前端调大超时,或优化接口性能
WebSocket 连不上 Nginx 没配置 Upgrade proxy_set_header Upgrade $http_upgrade
# WebSocket 代理配置
location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;    # WebSocket 长连接,超时设大
}

核心原则

网关是系统的门面——只管通用横切逻辑(鉴权、路由、限流、日志),不该包含业务逻辑。

网关要尽量轻:少写代码,多用配置。见配置就懂,不用翻源码。