网关与反向代理实践
实践导向的网关与反向代理开发手册。从 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 长连接,超时设大
}
核心原则
网关是系统的门面——只管通用横切逻辑(鉴权、路由、限流、日志),不该包含业务逻辑。
网关要尽量轻:少写代码,多用配置。见配置就懂,不用翻源码。