从零部署HTTPS网站完整指南-第一章

跨域(CORS)与 Cookie/Session 问题全面总结

目录


一、同源策略

1.1 什么是同源?

同源要求:协议 + 域名 + 端口 三者完全相同

URL A URL B 是否同源 原因
http://localhost:8080 http://localhost:8080/api ✅ 同源 路径不影响
http://localhost:8080 http://localhost:8081 ❌ 跨域 端口不同
http://localhost:8080 http://127.0.0.1:8080 ❌ 跨域 域名不同
http://example.com https://example.com ❌ 跨域 协议不同
http://a.example.com http://b.example.com ❌ 跨域 子域名不同

1.2 同源策略限制什么?

  • Ajax 请求:不能跨域请求数据
  • DOM 操作:不能操作跨域页面的 DOM
  • Cookie/LocalStorage:不能读取跨域的存储数据

二、跨域请求(CORS)

2.1 什么是 CORS?

CORS(Cross-Origin Resource Sharing)跨域资源共享,是一种允许服务器声明哪些源可以访问其资源的机制。

2.2 简单请求 vs 预检请求

简单请求(不触发预检)

满足以下所有条件

  • 方法:GETHEADPOST
  • 请求头:只包含以下头
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 没有自定义请求头
预检请求(需要 OPTIONS)

不满足简单请求条件的请求,比如:

  • 使用 PUTDELETE 方法
  • 使用 application/json 的 Content-Type
  • 携带自定义请求头(如 x-request-user-id

2.3 CORS 相关响应头

响应头 说明 示例
Access-Control-Allow-Origin 允许的源 *http://localhost:8080
Access-Control-Allow-Methods 允许的方法 GET, POST, PUT, DELETE
Access-Control-Allow-Headers 允许的请求头 *Content-Type, x-request-user-id
Access-Control-Allow-Credentials 是否允许携带凭证 true
Access-Control-Max-Age 预检结果缓存时间(秒) 3600
Access-Control-Expose-Headers 允许前端访问的响应头 x-custom-header

三、预检请求(OPTIONS)

3.1 预检请求流程

复制代码
浏览器                                    服务器
  |                                         |
  |  1. OPTIONS /api/data HTTP/1.1          |
  |  Origin: http://localhost:8080          |
  |  Access-Control-Request-Method: GET     |
  |  Access-Control-Request-Headers: x-request-user-id
  |---------------------------------------->|
  |                                         |
  |  2. HTTP/1.1 200 OK                     |
  |  Access-Control-Allow-Origin: *         |
  |  Access-Control-Allow-Methods: GET      |
  |  Access-Control-Allow-Headers: x-request-user-id
  |<----------------------------------------|
  |                                         |
  |  3. GET /api/data HTTP/1.1              |
  |  Origin: http://localhost:8080          |
  |  x-request-user-id: 123                 |
  |---------------------------------------->|
  |                                         |
  |  4. HTTP/1.1 200 OK                     |
  |  Access-Control-Allow-Origin: *         |
  |  { "data": ... }                        |
  |<----------------------------------------|

3.2 预检请求失败的原因

原因 说明
OPTIONS 返回非 2xx 服务器拒绝了预检请求
缺少 CORS 响应头 没有返回 Access-Control-Allow-*
请求头不被允许 Access-Control-Allow-Headers 没有包含自定义头
方法不被允许 Access-Control-Allow-Methods 没有包含请求方法

3.3 预检请求不携带自定义头

这是正常行为! OPTIONS 预检请求本身不会携带自定义请求头(如 x-request-user-id),它只是询问服务器"我可以发送带有这些头的请求吗?"


四、Spring Boot 中的 CORS 配置

4.1 三种配置方式及执行时机

方式 执行位置 优先级 推荐场景
@CrossOrigin 注解 Controller 层 最低 单个接口
addCorsMappings DispatcherServlet 内 无自定义 Filter
CorsFilter Filter 层 最高 有自定义 Filter/拦截器

4.2 执行顺序

复制代码
请求进入
    ↓
1. Filter(过滤器)        ← CorsFilter 应该在这里(最高优先级)
    ↓
2. DispatcherServlet
    ↓
3. HandlerMapping
    ↓
4. CORS 处理               ← addCorsMappings 在这里生效
    ↓
5. Interceptor(拦截器)   ← 身份验证通常在这里
    ↓
6. Controller

4.3 方式一:@CrossOrigin 注解

java 复制代码
@RestController
@CrossOrigin(origins = "*")
public class MyController {
    
    @GetMapping("/api/data")
    @CrossOrigin(origins = "http://localhost:8080")
    public String getData() {
        return "data";
    }
}

4.4 方式二:WebMvcConfigurer

java 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

⚠️ 注意: 如果有 Filter 在 CORS 处理之前抛出异常,此方式不生效!

4.5 方式三:CorsFilter(推荐)

java 复制代码
@Configuration
public class CorsConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)  // 最高优先级
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}

4.6 方式四:自定义 Filter(最灵活)

java 复制代码
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MyCorsFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        
        // ⭐ 先设置 CORS 响应头(无论后续是否异常)
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
        
        // OPTIONS 预检请求直接返回 200
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        chain.doFilter(req, res);
    }
}

五、Cookie 与 Session

Cookie 由 DomainPath 决定作用域,不区分端口

复制代码
localhost:8080 设置的 Cookie
         ↓
localhost:8081 也能访问(同一 Domain)
属性 说明 示例
Domain Cookie 的域 .example.com(包含子域名)
Path Cookie 的路径 /(所有路径)
Expires/Max-Age 过期时间 Max-Age=3600
HttpOnly 禁止 JS 访问 true(防 XSS)
Secure 仅 HTTPS true
SameSite 跨站限制 Strict/Lax/None

5.3 Session 原理

复制代码
1. 首次请求 → 服务器创建 Session → 返回 Set-Cookie: JSESSIONID=xxx
2. 后续请求 → 浏览器携带 Cookie: JSESSIONID=xxx → 服务器识别用户

6.1 为什么跨域默认不携带 Cookie?

浏览器出于安全考虑,跨域请求默认不携带 Cookie,防止 CSRF 攻击。

6.2 如何让跨域请求携带 Cookie?

前端配置
javascript 复制代码
// Axios
axios.defaults.withCredentials = true;

// 或单个请求
axios.get('http://localhost:8081/api/data', {
    withCredentials: true
});

// Fetch
fetch('http://localhost:8081/api/data', {
    credentials: 'include'
});

// jQuery
$.ajax({
    url: 'http://localhost:8081/api/data',
    xhrFields: {
        withCredentials: true
    }
});
后端配置
java 复制代码
@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    // ⚠️ 不能用 *,必须指定具体域名
    config.addAllowedOriginPattern("http://localhost:8080");
    config.addAllowedMethod("*");
    config.addAllowedHeader("*");
    // ⭐ 关键配置
    config.setAllowCredentials(true);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}

6.3 注意事项

配置项 说明
Access-Control-Allow-Credentials true 必须为 true
Access-Control-Allow-Origin 具体域名 不能为 *
前端 withCredentials true 必须开启

七、常见问题排查

7.1 拦截器导致的跨域问题

问题: 拦截器验证身份时,OPTIONS 请求没有携带自定义头,导致验证失败。

解决: 拦截器中放行 OPTIONS 请求

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    // ⭐ OPTIONS 请求直接放行
    if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
        return true;
    }
    
    // 其他请求验证身份
    String userId = request.getHeader("x-request-user-id");
    if (StringUtils.isBlank(userId)) {
        response.setStatus(401);
        return false;
    }
    return true;
}

7.2 Filter 抛异常导致的跨域问题

问题: Filter 抛异常后,CORS 响应头没有设置,浏览器报跨域错误。

原因:

  • Filter 在 DispatcherServlet 之前执行
  • 全局异常处理器(@ControllerAdvice)只能处理 Controller 层异常
  • Filter 异常由 Servlet 容器处理,不会设置 CORS 头

解决: CORS Filter 设置为最高优先级,在所有逻辑之前设置响应头

7.3 拦截器返回 false 时的响应

情况 响应状态码 响应体
只写 return false 200
setStatus(401) + return false 401
setStatus(401) + 写响应 + return false 401 自定义内容
抛异常 500(或全局处理器设置的) 异常信息

⚠️ 跨域情况下,如果没有 CORS 头,浏览器会隐藏真实错误,只显示跨域错误!

7.4 排查步骤

  1. 打开浏览器 F12 → Network
  2. 查看是否有 OPTIONS 请求
  3. 检查 OPTIONS 响应状态码(应该是 200/204)
  4. 检查响应头是否包含 Access-Control-Allow-*
  5. 后端添加日志,确认请求是否到达拦截器/Controller

八、扩展:跨站 Cookie(SameSite)

8.1 跨站 Ajax 请求是否只需解决跨域?

这取决于你是否需要携带 Cookie!

情况一:不需要 Cookie(纯数据请求)

只需要解决跨域(CORS)即可

复制代码
场景:从 a.com 请求 b.com 的公开接口,不需要登录态
     
解决方案:
1. 后端配置 CORS(Access-Control-Allow-Origin 等)
2. 前端正常发起 Ajax 请求

这种情况下,跨站和跨域的处理方式完全一样!
javascript 复制代码
// 前端代码
axios.get('http://b.com/api/public-data')
    .then(res => console.log(res.data));
情况二:需要携带 Cookie(需要身份认证)

仅解决跨域不够,还需要处理 SameSite 限制!

复制代码
场景:从 a.com 请求 b.com 的用户接口,需要携带 b.com 的登录 Cookie

问题链:
1. 跨域问题 → 需要 CORS 配置
2. 跨域 Cookie → 需要 withCredentials + allowCredentials  
3. 跨站 Cookie → 需要处理 SameSite(Chrome 80+ 默认 Lax)
问题层级 限制 解决方案
Layer 1: 跨域 浏览器同源策略 CORS 配置
Layer 2: 跨域 Cookie 默认不携带 withCredentials: true
Layer 3: 跨站 Cookie SameSite=Lax 阻止 SameSite=None; Secure
完整解决方案(跨站 + 需要 Cookie)

前端:

javascript 复制代码
axios.get('http://b.com/api/user-data', {
    withCredentials: true  // 必须
});

后端:

java 复制代码
// 1. CORS 配置
response.setHeader("Access-Control-Allow-Origin", "http://a.com"); // 不能用 *
response.setHeader("Access-Control-Allow-Credentials", "true");    // 必须

// 2. Cookie 配置(解决 SameSite)
Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setSecure(true);                    // 必须 HTTPS
cookie.setAttribute("SameSite", "None");   // 允许跨站
response.addCookie(cookie);
总结对比
场景 需要 CORS 需要 withCredentials 需要 SameSite=None
跨域 + 无 Cookie
跨域 + 需要 Cookie ❌(同站)
跨站 + 无 Cookie
跨站 + 需要 Cookie

💡 结论: 跨站 Ajax 如果只是获取公开数据,和跨域一样只需要 CORS;但如果需要携带 Cookie(如登录态),就必须额外处理 SameSite 问题,这是跨站特有的限制!

重要:跨站一定跨域!
复制代码
跨站(a.com → b.com):
  - eTLD+1 不同 → 跨站 ✅
  - 域名不同 → 跨域 ✅
  
结论:跨站 100% 是跨域的子集!

因此,跨站携带 Cookie 需要同时满足两个条件

复制代码
┌─────────────────────────────────────────────────────────┐
│  跨站携带 Cookie = CORS credentials + SameSite=None     │
│                                                         │
│  1. 解决跨域限制(因为跨站必然跨域)                       │
│     ├─ 前端:withCredentials: true                      │
│     └─ 后端:Access-Control-Allow-Credentials: true     │
│                                                         │
│  2. 解决跨站限制(SameSite 默认 Lax)                     │
│     └─ Cookie:SameSite=None; Secure                    │
└─────────────────────────────────────────────────────────┘

缺一不可!只配置 SameSite=None 不配置 credentials,Cookie 照样发不出去!


8.2 跨域但不跨站时,为什么还要设置 credentials?

典型场景
复制代码
http://localhost:8080  →  http://localhost:8081
        前端                      后端
        
判断:
- 跨域? ✅ 是(端口不同:8080 vs 8081)
- 跨站? ❌ 否(都是 localhost,同站)
为什么同站还需要 credentials?

因为"跨域不携带 Cookie"是 CORS 规范的要求,与 SameSite 无关!

复制代码
浏览器有两套独立的安全机制:

┌─────────────────────────────────────────────────────────────┐
│  1. CORS(跨域资源共享)                                      │
│     ├─ 作用:控制跨域请求是否被允许                            │
│     ├─ 判断依据:协议 + 域名 + 端口(同源策略)                 │
│     └─ 默认行为:跨域请求不携带 Cookie                         │
│        → 需要 withCredentials + Access-Control-Allow-Credentials│
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  2. SameSite Cookie                                          │
│     ├─ 作用:控制 Cookie 是否在跨站请求中发送                   │
│     ├─ 判断依据:eTLD+1(有效顶级域名+1)                      │
│     └─ 默认行为:Lax 模式下 Ajax 跨站不携带 Cookie              │
│        → 需要 SameSite=None; Secure                          │
└─────────────────────────────────────────────────────────────┘
原理解释

即使是同站 ,只要跨域,浏览器的 CORS 机制就会生效:

javascript 复制代码
// 前端 http://localhost:8080
axios.get('http://localhost:8081/api/user', {
    // 不设置 withCredentials
});

// 结果:请求发出,但不会携带 localhost:8081 的 Cookie!
// 原因:CORS 规范默认禁止跨域请求携带凭证
credentials 的真正作用
配置 含义
前端 withCredentials: true 告诉浏览器:我想在这个跨域请求中携带 Cookie
后端 Access-Control-Allow-Credentials: true 告诉浏览器:我允许这个跨域请求携带 Cookie

两者必须同时配置,缺一不可!

复制代码
请求流程:

1. 前端设置 withCredentials: true
       ↓
2. 浏览器检查:这是跨域请求,需要先发 OPTIONS 预检
       ↓
3. OPTIONS 响应包含 Access-Control-Allow-Credentials: true
       ↓
4. 浏览器确认:后端允许携带凭证
       ↓
5. 发送实际请求,携带 Cookie
为什么这样设计?

防止 CSRF(跨站请求伪造)攻击!

复制代码
假设没有 credentials 限制:

1. 用户登录了 bank.com,浏览器存储了 Cookie
2. 用户访问恶意网站 evil.com
3. evil.com 的 JS 发起请求到 bank.com/transfer?to=hacker&amount=10000
4. 如果自动携带 Cookie,银行以为是用户操作,转账成功!

有了 credentials 限制:

- 默认不携带 Cookie → 恶意请求没有登录态 → 攻击失败
- 只有 bank.com 明确设置 Allow-Credentials: true 才会携带
- bank.com 可以控制哪些域名可以携带(不能用 * )
完整配置示例

跨域但不跨站(如 localhost 不同端口):

java 复制代码
// 后端配置
@Bean
public CorsFilter corsFilter() {
    CorsConfiguration config = new CorsConfiguration();
    // 跨域但不跨站时,可以用通配符(因为不涉及 SameSite)
    // 但如果要携带 Cookie,必须指定具体域名!
    config.addAllowedOrigin("http://localhost:8080");
    config.addAllowedMethod("*");
    config.addAllowedHeader("*");
    config.setAllowCredentials(true);  // ⭐ 允许携带凭证
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}
javascript 复制代码
// 前端配置
axios.get('http://localhost:8081/api/user', {
    withCredentials: true  // ⭐ 携带凭证
});
关键限制

credentials: true 时:

限制 说明
Access-Control-Allow-Origin 不能为 * 必须指定具体域名
响应的 Cookie 才会被浏览器接受 Set-Cookie 需要 credentials 才生效
复制代码
// ❌ 错误配置
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
// 浏览器会拒绝,报错!

// ✅ 正确配置  
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Allow-Credentials: true
总结
问题 解决方案 触发条件
跨域请求被阻止 CORS 配置 协议/域名/端口不同
跨域不携带 Cookie credentials: true 协议/域名/端口不同
跨站不携带 Cookie SameSite=None; Secure eTLD+1 不同

💡 记住: credentials 是 CORS 规范的一部分,解决的是跨域 携带 Cookie 的问题;SameSite 是 Cookie 属性,解决的是跨站携带 Cookie 的问题。两者独立生效!


8.4 什么是跨站?

跨域 ≠ 跨站

概念 判断依据 示例
跨域(Cross-Origin) 协议+域名+端口 a.com:80 vs a.com:8080 = 跨域
跨站(Cross-Site) eTLD+1(有效顶级域名+1) a.example.com vs b.example.com = 同站
复制代码
同站示例:
  a.example.com → b.example.com  ✅ 同站
  
跨站示例:
  a.com → b.com                  ❌ 跨站
  example.com → example.org      ❌ 跨站

8.5 SameSite 属性

Chrome 80+ 默认 SameSite=Lax,影响跨站 Cookie 发送。

说明 跨站请求携带 Cookie
Strict 完全禁止跨站 ❌ 都不携带
Lax(默认) 部分允许 ✅ 顶级导航 GET 请求 / ❌ Ajax、iframe
None 允许跨站 ✅ 都携带(需配合 Secure)

8.6 SameSite=Lax 的影响

复制代码
场景:从 a.com 页面请求 b.com 的接口

Lax 模式下:
  - 点击链接跳转到 b.com  → ✅ 携带 Cookie
  - Ajax 请求 b.com        → ❌ 不携带 Cookie
  - iframe 加载 b.com      → ❌ 不携带 Cookie
  - form POST 到 b.com     → ❌ 不携带 Cookie

⚠️ 重要前提:跨站一定跨域,所以 CORS 的 credentials 配置也是必须的!

跨站携带 Cookie 需要同时配置

  1. CORS:withCredentials: true + Access-Control-Allow-Credentials: true
  2. Cookie:SameSite=None; Secure

只配置其中一个都不行!

"Cookie 发不出去"其实包含两个方向

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│                        Cookie 的两个方向                             │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  方向一:前端 → 后端(请求时携带 Cookie)                             │
│  ═══════════════════════════════════════                            │
│  场景:用户已登录 b.com,现在从 a.com 请求 b.com 的接口               │
│  期望:请求自动带上 b.com 的登录 Cookie                              │
│                                                                     │
│  Request:                                                           │
│    GET /api/user HTTP/1.1                                          │
│    Host: b.com                                                      │
│    Cookie: JSESSIONID=abc123  ← 这个能不能发出去?                   │
│                                                                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  方向二:后端 → 前端(响应时设置 Cookie)                             │
│  ═══════════════════════════════════════                            │
│  场景:用户从 a.com 请求 b.com 的登录接口                            │
│  期望:b.com 返回的 Set-Cookie 能被浏览器保存                        │
│                                                                     │
│  Response:                                                          │
│    HTTP/1.1 200 OK                                                  │
│    Set-Cookie: JSESSIONID=abc123  ← 这个能不能被浏览器保存?          │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘
两个方向的限制对比
方向 限制 不配置的后果
请求时携带 Cookie CORS credentials + SameSite 请求不带 Cookie,后端收到匿名请求
响应时设置 Cookie CORS credentials + SameSite 浏览器拒绝保存 Cookie
详细说明

方向一:前端发送 Cookie(请求)

javascript 复制代码
// 前端 a.com 请求 b.com
axios.get('http://b.com/api/user', {
    withCredentials: true
});

受到的限制:

  1. CORS 限制 :如果 withCredentials: false(默认),浏览器不会在跨域请求中携带 Cookie
  2. SameSite 限制 :即使 withCredentials: true,如果 Cookie 是 SameSite=Lax,跨站 Ajax 也不会携带

方向二:后端响应 Cookie(Set-Cookie)

复制代码
// b.com 响应
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Set-Cookie: JSESSIONID=abc123; SameSite=None; Secure

受到的限制:

  1. CORS 限制 :如果没有 Access-Control-Allow-Credentials: true,浏览器会忽略 Set-Cookie 响应头
  2. SameSite 限制 :如果没有 SameSite=None; Secure,浏览器会拒绝保存这个跨站 Cookie
完整流程图
复制代码
用户首次登录 b.com(跨站请求):

a.com 页面                    浏览器                         b.com 服务器
    |                           |                               |
    | 1. 发起登录请求            |                               |
    |   withCredentials:true    |                               |
    |-------------------------->|                               |
    |                           | 2. 发送请求(无 Cookie)       |
    |                           |------------------------------>|
    |                           |                               |
    |                           | 3. 响应 Set-Cookie            |
    |                           |   + Allow-Credentials:true    |
    |                           |   + SameSite=None; Secure     |
    |                           |<------------------------------|
    |                           |                               |
    |                           | 4. 浏览器检查:               |
    |                           |   ✓ credentials=true          |
    |                           |   ✓ SameSite=None             |
    |                           |   → 保存 Cookie ✅             |
    |                           |                               |

用户后续请求 b.com:

a.com 页面                    浏览器                         b.com 服务器
    |                           |                               |
    | 1. 发起 API 请求           |                               |
    |   withCredentials:true    |                               |
    |-------------------------->|                               |
    |                           | 2. 浏览器检查:               |
    |                           |   ✓ credentials=true          |
    |                           |   ✓ Cookie 的 SameSite=None   |
    |                           |   → 携带 Cookie ✅             |
    |                           |------------------------------>|
    |                           |        Cookie: JSESSIONID=xxx |
    |                           |                               |
总结
配置 影响请求(发送 Cookie) 影响响应(保存 Cookie)
withCredentials: true ✅ 必须 ✅ 必须
Access-Control-Allow-Credentials: true ✅ 必须 ✅ 必须
SameSite=None; Secure ✅ 跨站必须 ✅ 跨站必须

💡 结论: CORS 的 credentials 和 Cookie 的 SameSite 配置,对发送保存 Cookie 都有影响,两个方向都需要正确配置!

方案一:设置 SameSite=None + Secure + Credentials(完整方案)

后端完整配置:

java 复制代码
@Configuration
public class CorsConfig {
    
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("http://a.com");  // 指定允许的源
        config.addAllowedMethod("*");
        config.addAllowedHeader("*");
        config.setAllowCredentials(true);  // ⭐ 必须!解决跨域 Cookie 限制
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
    
    // ⭐ 必须!解决跨站 Cookie 限制(SameSite)
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setSameSite("None");
        serializer.setUseSecureCookie(true);  // 必须 HTTPS
        return serializer;
    }
}

前端配置:

javascript 复制代码
axios.get('http://b.com/api/user', {
    withCredentials: true  // ⭐ 必须!
});

或者手动设置 Cookie:

java 复制代码
Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setSecure(true);                    // ⭐ 必须 HTTPS
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setAttribute("SameSite", "None");   // ⭐ 允许跨站(Servlet 4.0+)
response.addCookie(cookie);

⚠️ 注意: SameSite=None 必须配合 Secure=true,即必须使用 HTTPS!

javascript 复制代码
// 前端存储 Token
localStorage.setItem('token', 'xxx');

// 请求时携带
axios.get('http://b.com/api/data', {
    headers: {
        'Authorization': 'Bearer ' + localStorage.getItem('token')
    }
});
方案三:代理转发(推荐)
复制代码
前端 a.com → 请求 a.com/api(同站)→ 后端代理转发到 b.com

Nginx 配置:

nginx 复制代码
location /api/ {
    proxy_pass http://b.com/;
}
方案四:使用同一顶级域名
复制代码
将服务部署在同一顶级域名下:
  前端:www.example.com
  后端:api.example.com
  
Cookie 设置:
  Domain=.example.com(注意前面的点)
  
这样两个子域名共享 Cookie,且是同站。

8.8 各浏览器 SameSite 默认值

浏览器 版本 默认值
Chrome 80+ Lax
Firefox 69+ Lax
Edge 86+ Lax
Safari 无默认 无(但有 ITP 限制)

九、Nginx 解决跨域与跨站问题

9.0 Nginx 配置文件位置与快速入门

📁 配置前必读:找到你的 Nginx 配置文件

操作系统/安装方式 主配置文件 站点配置目录
Linux (apt/yum) /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
Linux (编译安装) /usr/local/nginx/conf/nginx.conf /usr/local/nginx/conf/vhost/*.conf
Windows C:\nginx\conf\nginx.conf C:\nginx\conf\vhost\*.conf
macOS (brew) /usr/local/etc/nginx/nginx.conf /usr/local/etc/nginx/servers/*.conf
Docker /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf

推荐做法:

bash 复制代码
# 1. 在 conf.d 目录下创建独立配置文件(不修改主配置)
sudo vim /etc/nginx/conf.d/my-app.conf

# 2. 检查配置语法
sudo nginx -t

# 3. 重新加载配置(不中断服务)
sudo nginx -s reload

⚡ 快速配置指南(5 分钟上手)

选择你的场景,复制配置,修改标记【改】的地方即可!

场景选择流程图
复制代码
开始
  │
  ├─ 开发环境?
  │    ├─ 需要 Cookie?─→ 是 ─→ 【场景二】CORS + Cookie
  │    │                  └─ 否 ─→ 【场景一】CORS 无 Cookie
  │    │
  └─ 生产环境?(推荐)
       ├─ 需要 Cookie?─→ 是 ─→ 【场景四】反向代理 + Cookie ⭐最常用
       │                  └─ 否 ─→ 【场景三】反向代理无 Cookie
       │
       └─ 需要 HTTPS?─→ 是 ─→ 【9.14 完整配置】HTTPS + 负载均衡
各场景需要修改的配置速查
场景 需修改项 示例值
场景一/二 listen 8081
(CORS 方式) server_name localhost
proxy_pass http://127.0.0.1:9000
Allow-Origin(场景二) http://localhost:8080
场景三/四 listen 80
(反向代理) server_name localhost
root /usr/share/nginx/html
proxy_pass http://127.0.0.1:9000
rewrite(可选) 根据后端路径调整
完整配置 server_name example.com
(HTTPS) ssl_certificate /path/to/cert.crt
ssl_certificate_key /path/to/key.key
root /var/www/html
upstream server 127.0.0.1:9000
最简配置模板(3 行搞定)

开发环境-反向代理(最简单):

nginx 复制代码
# /etc/nginx/conf.d/dev.conf
# 只需改 3 处:listen、root、proxy_pass
server {
    listen 80;                                    # 【改】端口
    server_name localhost;
    
    location / {
        root /usr/share/nginx/html;               # 【改】前端路径
        try_files $uri $uri/ /index.html;
    }
    
    location /api/ {
        rewrite ^/api/(.*)$ /$1 break;
        proxy_pass http://127.0.0.1:9000;         # 【改】后端地址
        proxy_set_header Host $host;
        proxy_set_header Cookie $http_cookie;
        proxy_pass_header Set-Cookie;
    }
}

使用方法:

bash 复制代码
# 1. 复制上面配置到文件
sudo vim /etc/nginx/conf.d/dev.conf

# 2. 修改 3 处【改】标记的内容

# 3. 测试并重载
sudo nginx -t && sudo nginx -s reload

# 4. 访问 http://localhost 即可

9.1 Nginx 解决跨域的核心思路

⚠️ 重要提示:同源请求不需要任何 CORS 配置!

如果前端和后端是同源(协议+域名+端口都相同),则:

  • ❌ 不需要 Access-Control-Allow-Origin
  • ❌ 不需要 Access-Control-Allow-Headers
  • ❌ 不需要 Access-Control-Allow-Methods
  • ❌ 不需要 Access-Control-Allow-Credentials
  • ❌ 不需要处理 OPTIONS 预检请求
  • ✅ 所有请求头自动转发
  • ✅ Cookie 自动携带

因此,使用反向代理(场景三/四)消除跨域后,配置会简单很多!

原理:将跨域请求变成同源请求

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  方式一:Nginx 添加 CORS 响应头(后端代理)                       │
│  ════════════════════════════════════════                       │
│  前端 a.com:8080 → 后端 b.com:8081                              │
│                                                                 │
│  Nginx 代理 b.com:8081,添加 CORS 响应头                         │
│  适用:后端服务不方便修改代码                                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  方式二:Nginx 反向代理(推荐)                                   │
│  ════════════════════════════════════                           │
│  前端 a.com:8080 → a.com:8080/api → Nginx → b.com:8081          │
│                                                                 │
│  前端请求同源地址,Nginx 转发到后端                               │
│  适用:生产环境,彻底消除跨域                                     │
└─────────────────────────────────────────────────────────────────┘

9.2 场景一:Nginx 添加 CORS 响应头(不需要 Cookie)

场景: 前端 http://localhost:8080 请求后端 http://localhost:8081,不需要携带 Cookie

📁 配置文件: /etc/nginx/conf.d/cors-no-cookie.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  场景:跨域请求 + 不需要 Cookie                                    ║
# ║  用途:开发环境调试、公开 API、无需登录的接口                        ║
# ║  前端地址:http://localhost:8080                                   ║
# ║  后端地址:http://localhost:8081 (Nginx) → http://127.0.0.1:9000   ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# 🔧 需要修改的配置项(共 3 处):
# ┌─────────────────┬─────────────────────────────────────────────────┐
# │ 配置项           │ 说明                                            │
# ├─────────────────┼─────────────────────────────────────────────────┤
# │ listen          │ Nginx 对外暴露的端口                             │
# │ server_name     │ 访问 Nginx 的域名                                │
# │ proxy_pass      │ 后端服务的实际地址                               │
# └─────────────────┴─────────────────────────────────────────────────┘

server {
    # ══════════════════════════════════════════════════════════════
    # 【改】listen - Nginx 监听端口
    # ══════════════════════════════════════════════════════════════
    # 作用:前端通过这个端口访问 Nginx
    # 
    # 📝 为什么要改:
    #    端口号要根据你的实际部署情况设置,不能和其他服务冲突
    #
    # 🔄 举一反三:
    #    listen 80;        # HTTP 默认端口,生产环境常用
    #    listen 443 ssl;   # HTTPS 默认端口,需要配置 SSL 证书
    #    listen 8081;      # 自定义端口,开发环境常用
    #    listen 8080;      # 另一个常用开发端口
    # ──────────────────────────────────────────────────────────────
    listen 8081;
    
    # ══════════════════════════════════════════════════════════════
    # 【改】server_name - 服务器域名
    # ══════════════════════════════════════════════════════════════
    # 作用:指定这个 server 块响应哪个域名的请求
    #
    # 📝 为什么要改:
    #    要匹配前端实际请求的域名,否则 Nginx 可能不会处理请求
    #
    # 🔄 举一反三:
    #    server_name localhost;           # 本地开发
    #    server_name 192.168.1.100;       # 局域网 IP 访问
    #    server_name api.example.com;     # 生产环境域名
    #    server_name *.example.com;       # 泛域名匹配
    #    server_name _;                   # 匹配所有域名(默认服务器)
    # ──────────────────────────────────────────────────────────────
    server_name localhost;
    
    # ══════════════════════════════════════════════════════════════
    # location / - 匹配所有请求路径
    # ══════════════════════════════════════════════════════════════
    # 作用:定义如何处理以 / 开头的请求(即所有请求)
    #
    # 其他 location 写法:
    #    location /api/ { }      # 只匹配 /api/ 开头的请求
    #    location ~ \.php$ { }   # 正则匹配 .php 结尾的请求
    #    location = /health { }  # 精确匹配 /health
    # ──────────────────────────────────────────────────────────────
    location / {
    
        # ══════════════════════════════════════════════════════════
        # CORS 响应头配置(跨域的核心)
        # ══════════════════════════════════════════════════════════
        
        # ----------------------------------------------------------
        # Access-Control-Allow-Origin - 允许哪些源访问
        # ----------------------------------------------------------
        # 作用:告诉浏览器允许哪些域名的页面访问这个接口
        # 值 '*':允许所有源(仅在不需要 Cookie 时可用)
        # 'always':无论响应状态码是什么都添加这个头
        #
        # 🔄 举一反三:
        #    '*'                        # 允许所有(不能和 credentials 一起用)
        #    'http://localhost:8080'    # 只允许这一个源
        #    'http://localhost:3000'    # Vue/React 开发服务器
        #    'https://www.example.com'  # 生产环境前端域名
        # ----------------------------------------------------------
        add_header 'Access-Control-Allow-Origin' '*' always;
        
        # ----------------------------------------------------------
        # Access-Control-Allow-Methods - 允许的 HTTP 方法
        # ----------------------------------------------------------
        # 作用:告诉浏览器允许使用哪些 HTTP 方法
        # 常用方法:GET(查询) POST(创建) PUT(更新) DELETE(删除) OPTIONS(预检)
        # ----------------------------------------------------------
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        
        # ----------------------------------------------------------
        # Access-Control-Allow-Headers - 允许的请求头
        # ----------------------------------------------------------
        # 作用:告诉浏览器允许前端携带哪些请求头
        # 
        # 📝 为什么要配置:
        #    浏览器会检查前端发送的请求头是否在允许列表中
        #    如果前端用了自定义头(如 x-request-user-id),必须加到这里
        #
        # 🔄 举一反三(根据你的前端请求头添加):
        #    'Content-Type'         # 必须,POST 请求需要
        #    'Authorization'        # JWT Token 认证
        #    'X-Requested-With'     # Ajax 请求标识
        #    'x-request-user-id'    # 自定义用户 ID 头
        #    'x-token'              # 自定义 Token 头
        #    '*'                    # 允许所有头(Nginx 1.7.5+)
        # ----------------------------------------------------------
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, x-request-user-id' always;
        
        # ----------------------------------------------------------
        # Access-Control-Max-Age - 预检请求缓存时间
        # ----------------------------------------------------------
        # 作用:浏览器缓存预检结果的时间(秒),减少 OPTIONS 请求次数
        # 3600 = 1 小时内相同请求不再发送 OPTIONS 预检
        # ----------------------------------------------------------
        add_header 'Access-Control-Max-Age' '3600' always;
        
        # ══════════════════════════════════════════════════════════
        # OPTIONS 预检请求处理
        # ══════════════════════════════════════════════════════════
        # 作用:浏览器发送跨域请求前会先发 OPTIONS 预检请求
        #       这里直接返回 204(无内容)表示允许
        # 204 vs 200:204 不返回响应体,更轻量
        # ──────────────────────────────────────────────────────────
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # ══════════════════════════════════════════════════════════
        # 【改】proxy_pass - 代理到后端服务
        # ══════════════════════════════════════════════════════════
        # 作用:将请求转发到实际的后端服务器
        #
        # 📝 为什么要改:
        #    这是你后端服务的实际地址,必须根据部署情况修改
        #
        # 🔄 举一反三:
        #    http://127.0.0.1:9000       # 本机后端服务
        #    http://127.0.0.1:8080       # Spring Boot 默认端口
        #    http://192.168.1.100:9000   # 局域网其他机器
        #    http://backend:9000         # Docker 容器名(Docker 网络)
        #    http://10.0.0.5:8080        # 内网服务器
        # ──────────────────────────────────────────────────────────
        proxy_pass http://127.0.0.1:9000;
        
        # ══════════════════════════════════════════════════════════
        # 请求头转发配置
        # ══════════════════════════════════════════════════════════
        
        # ----------------------------------------------------------
        # Host - 原始请求的主机头
        # ----------------------------------------------------------
        # 作用:告诉后端原始请求的域名
        # $host:不包含端口的域名(如 localhost)
        # 如果后端需要知道端口,用 $http_host(如 localhost:8081)
        # ----------------------------------------------------------
        proxy_set_header Host $host;
        
        # ----------------------------------------------------------
        # X-Real-IP - 客户端真实 IP
        # ----------------------------------------------------------
        # 作用:将客户端 IP 传给后端,否则后端只能看到 Nginx 的 IP
        # $remote_addr:发起请求的客户端 IP
        # 后端获取:request.getHeader("X-Real-IP")
        # ----------------------------------------------------------
        proxy_set_header X-Real-IP $remote_addr;
        
        # ----------------------------------------------------------
        # X-Forwarded-For - IP 链路追踪
        # ----------------------------------------------------------
        # 作用:记录请求经过的所有代理 IP,用于多级代理场景
        # 格式:客户端IP, 代理1IP, 代理2IP, ...
        # $proxy_add_x_forwarded_for:自动追加当前代理 IP
        # ----------------------------------------------------------
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # ----------------------------------------------------------
        # X-Forwarded-Proto - 原始请求协议
        # ----------------------------------------------------------
        # 作用:告诉后端原始请求是 HTTP 还是 HTTPS
        # $scheme:当前请求的协议(http 或 https)
        # 用途:后端生成重定向 URL 时需要知道原始协议
        # ----------------------------------------------------------
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

前端请求示例:

javascript 复制代码
// 直接请求后端地址(跨域),不带 Cookie
axios.get('http://localhost:8081/api/data')
    .then(res => console.log(res.data));

9.3 场景二:Nginx 添加 CORS 响应头(需要 Cookie)

场景: 前端 http://localhost:8080 请求后端 http://localhost:8081,需要携带 Cookie

📁 配置文件: /etc/nginx/conf.d/cors-with-cookie.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  场景:跨域请求 + 需要 Cookie(登录态)                            ║
# ║  用途:需要身份认证的跨域 API 请求                                 ║
# ║  前端地址:http://localhost:8080                                   ║
# ║  后端地址:http://localhost:8081 (Nginx) → http://127.0.0.1:9000   ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# ⚠️ 与「不需要 Cookie」的关键区别:
# ┌─────────────────────────────────┬─────────────────────────────────┐
# │ 不需要 Cookie                    │ 需要 Cookie                     │
# ├─────────────────────────────────┼─────────────────────────────────┤
# │ Allow-Origin 可以用 *            │ Allow-Origin 必须指定具体域名    │
# │ 不需要 Allow-Credentials         │ 必须设置 Allow-Credentials: true │
# │ 前端不需要 withCredentials       │ 前端必须设置 withCredentials     │
# │ 不需要转发 Cookie                │ 必须转发 Cookie 和 Set-Cookie    │
# └─────────────────────────────────┴─────────────────────────────────┘
#
# 🔧 需要修改的配置项(共 4 处):
# ┌──────────────────────────────┬────────────────────────────────────┐
# │ 配置项                        │ 说明                               │
# ├──────────────────────────────┼────────────────────────────────────┤
# │ listen                       │ Nginx 对外暴露的端口                │
# │ server_name                  │ 访问 Nginx 的域名                   │
# │ Access-Control-Allow-Origin  │ 前端页面的域名(不能用 *)          │
# │ proxy_pass                   │ 后端服务的实际地址                  │
# └──────────────────────────────┴────────────────────────────────────┘

server {
    # ══════════════════════════════════════════════════════════════
    # 【改】listen - Nginx 监听端口
    # ══════════════════════════════════════════════════════════════
    # 参考场景一的说明
    # ──────────────────────────────────────────────────────────────
    listen 8081;
    
    # ══════════════════════════════════════════════════════════════
    # 【改】server_name - 服务器域名
    # ══════════════════════════════════════════════════════════════
    # 参考场景一的说明
    # ──────────────────────────────────────────────────────────────
    server_name localhost;
    
    location / {
        # ══════════════════════════════════════════════════════════
        # 【改】Access-Control-Allow-Origin - 允许的源(关键!)
        # ══════════════════════════════════════════════════════════
        # 作用:告诉浏览器允许哪个域名的页面访问这个接口
        #
        # 📝 为什么要改:
        #    必须改成你前端页面的实际地址(协议+域名+端口)
        #
        # ⚠️ 重要限制:
        #    需要 Cookie 时,这里不能用 *,必须指定具体域名!
        #    否则浏览器会报错:
        #    "The value of 'Access-Control-Allow-Origin' must not be '*' 
        #     when credentials mode is 'include'"
        #
        # 🔄 举一反三(改成你的前端地址):
        #    'http://localhost:8080'      # Vue CLI 默认开发端口
        #    'http://localhost:3000'      # React CRA 默认端口
        #    'http://localhost:5173'      # Vite 默认端口
        #    'http://192.168.1.100:8080'  # 局域网访问
        #    'https://www.example.com'    # 生产环境前端域名
        #    'https://admin.example.com'  # 管理后台域名
        # ──────────────────────────────────────────────────────────
        add_header 'Access-Control-Allow-Origin' 'http://localhost:8080' always;
        
        # ══════════════════════════════════════════════════════════
        # Access-Control-Allow-Credentials - 允许携带凭证(必须!)
        # ══════════════════════════════════════════════════════════
        # 作用:告诉浏览器允许跨域请求携带 Cookie
        # 值必须是 'true'(字符串,不是布尔值)
        #
        # 📝 为什么必须配置:
        #    如果不配置这个,即使前端设置了 withCredentials: true
        #    浏览器也不会发送 Cookie,后端收到的是匿名请求
        # ──────────────────────────────────────────────────────────
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        
        # ----------------------------------------------------------
        # 其他 CORS 头(与场景一相同)
        # ----------------------------------------------------------
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With, x-request-user-id' always;
        add_header 'Access-Control-Max-Age' '3600' always;
        
        # ----------------------------------------------------------
        # OPTIONS 预检请求处理
        # ----------------------------------------------------------
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # ══════════════════════════════════════════════════════════
        # 【改】proxy_pass - 代理到后端服务
        # ══════════════════════════════════════════════════════════
        # 参考场景一的说明
        # ──────────────────────────────────────────────────────────
        proxy_pass http://127.0.0.1:9000;
        
        # ----------------------------------------------------------
        # 请求头转发(与场景一相同)
        # ----------------------------------------------------------
        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;
        
        # ══════════════════════════════════════════════════════════
        # Cookie 转发配置(关键!需要 Cookie 时必须配置)
        # ══════════════════════════════════════════════════════════
        
        # ----------------------------------------------------------
        # proxy_set_header Cookie - 转发请求中的 Cookie
        # ----------------------------------------------------------
        # 作用:将前端请求中的 Cookie 转发给后端
        # $http_cookie:获取请求头中的 Cookie 字段
        #
        # 📝 为什么必须配置:
        #    虽然 Nginx 默认会转发大部分请求头,但显式配置更可靠
        #    确保后端能收到 JSESSIONID 等 Cookie
        # ----------------------------------------------------------
        proxy_set_header Cookie $http_cookie;
        
        # ----------------------------------------------------------
        # proxy_pass_header Set-Cookie - 透传响应中的 Set-Cookie
        # ----------------------------------------------------------
        # 作用:将后端响应的 Set-Cookie 头传给前端
        #
        # 📝 为什么必须配置:
        #    后端登录成功后会返回 Set-Cookie: JSESSIONID=xxx
        #    这个配置确保浏览器能收到并保存这个 Cookie
        # ----------------------------------------------------------
        proxy_pass_header Set-Cookie;
    }
}

前端请求示例:

javascript 复制代码
// ⚠️ 前端必须设置 withCredentials: true,否则不会携带 Cookie!
axios.get('http://localhost:8081/api/user', {
    withCredentials: true  // ⭐ 关键配置
});

// 或者全局设置
axios.defaults.withCredentials = true;

9.4 场景三:Nginx 反向代理消除跨域(推荐-不需要 Cookie)

场景: 前端和后端都通过 Nginx 同一端口访问,彻底消除跨域

复制代码
┌─────────────────────────────────────────────────────────────────────┐
│  请求流程(同源,无跨域问题!)                                        │
│                                                                      │
│  浏览器 ──→ http://localhost/api/user ──→ Nginx ──→ 后端 127.0.0.1:9000
│         │                              │        │
│         │  同源请求                     │        │  /api/user → /user
│         │  无需 CORS                    │        │  路径重写
│         └──────────────────────────────┘        └──────────────────────
└─────────────────────────────────────────────────────────────────────┘

📁 配置文件: /etc/nginx/conf.d/reverse-proxy-no-cookie.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  🌟 场景:反向代理消除跨域(不需要 Cookie)                         ║
# ║  用途:生产环境、前后端同域部署、公开 API                           ║
# ║  优势:彻底消除跨域问题,无需任何 CORS 配置!                        ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# ✨ 同源的好处(对比场景一/二):
# ┌─────────────────────────────────────────────────────────────────┐
# │ ❌ 不需要 Access-Control-Allow-Origin                           │
# │ ❌ 不需要 Access-Control-Allow-Headers                          │
# │ ❌ 不需要 Access-Control-Allow-Methods                          │
# │ ❌ 不需要 Access-Control-Allow-Credentials                      │
# │ ❌ 不需要处理 OPTIONS 预检请求                                   │
# │ ✅ 自定义请求头自动转发(只需 proxy_set_header)                  │
# │ ✅ 配置更简单,性能更好                                          │
# └─────────────────────────────────────────────────────────────────┘
#
# 🎯 核心思想:
#    将前端和后端都通过同一个 Nginx 端口访问
#    前端请求 /api/xxx 时,Nginx 内部转发到后端
#    对浏览器来说,这是同源请求,不存在跨域!
#
# 🔧 需要修改的配置项(共 5 处):
# ┌─────────────────┬──────────────────────────────────────────────────┐
# │ 配置项           │ 说明                                             │
# ├─────────────────┼──────────────────────────────────────────────────┤
# │ listen          │ Nginx 统一入口端口                                │
# │ server_name     │ 访问域名                                          │
# │ root            │ 前端打包文件路径                                   │
# │ rewrite         │ API 路径重写规则(可选)                           │
# │ proxy_pass      │ 后端服务地址                                       │
# └─────────────────┴──────────────────────────────────────────────────┘

server {
    # ══════════════════════════════════════════════════════════════
    # 【改】listen - 统一入口端口
    # ══════════════════════════════════════════════════════════════
    # 作用:前端页面和 API 请求都通过这个端口访问
    #
    # 📝 为什么要改:
    #    选择一个不冲突的端口作为统一入口
    #    生产环境通常用 80(HTTP) 或 443(HTTPS)
    #
    # 🔄 举一反三:
    #    listen 80;              # HTTP 默认端口(生产环境推荐)
    #    listen 443 ssl;         # HTTPS(需要 SSL 证书)
    #    listen 8080;            # 开发环境自定义端口
    #    listen 3000;            # 与前端开发端口一致
    # ──────────────────────────────────────────────────────────────
    listen 80;
    
    # ══════════════════════════════════════════════════════════════
    # 【改】server_name - 域名
    # ══════════════════════════════════════════════════════════════
    # 🔄 举一反三:
    #    localhost                # 本地开发
    #    www.example.com          # 生产环境
    #    admin.example.com        # 管理后台
    # ──────────────────────────────────────────────────────────────
    server_name localhost;
    
    # ══════════════════════════════════════════════════════════════
    # 前端静态资源配置
    # ══════════════════════════════════════════════════════════════
    location / {
        # ──────────────────────────────────────────────────────────
        # 【改】root - 前端打包文件路径
        # ──────────────────────────────────────────────────────────
        # 作用:指定前端静态文件的存放目录
        #
        # 📝 为什么要改:
        #    要指向你前端 build/打包后的文件目录
        #
        # 🔄 举一反三(常见路径):
        #    /usr/share/nginx/html              # Nginx 默认目录
        #    /var/www/html                      # Linux 常用目录
        #    /var/www/my-app/dist               # Vue 打包输出
        #    /var/www/my-app/build              # React 打包输出
        #    /home/user/projects/my-app/dist    # 用户目录
        #    D:/projects/my-app/dist            # Windows 路径
        #    C:/nginx/html                      # Windows Nginx 默认
        # ──────────────────────────────────────────────────────────
        root /usr/share/nginx/html;
        
        # ----------------------------------------------------------
        # index - 默认首页文件
        # ----------------------------------------------------------
        index index.html;
        
        # ----------------------------------------------------------
        # try_files - SPA 路由支持(重要!)
        # ----------------------------------------------------------
        # 作用:Vue/React 单页应用必须配置,否则刷新页面会 404
        # 
        # 原理解释:
        #   $uri        - 先尝试访问请求的文件(如 /about)
        #   $uri/       - 再尝试访问同名目录
        #   /index.html - 都不存在则返回 index.html(交给前端路由)
        #
        # 📝 为什么必须配置:
        #    SPA 的路由是前端控制的(如 /user/123)
        #    服务器上实际没有这个文件,会返回 404
        #    这个配置让所有找不到的路径都返回 index.html
        #    然后由前端 JS 路由处理
        # ----------------------------------------------------------
        try_files $uri $uri/ /index.html;
    }
    
    # ══════════════════════════════════════════════════════════════
    # API 反向代理配置
    # ══════════════════════════════════════════════════════════════
    # 作用:所有 /api/ 开头的请求都转发到后端
    # ──────────────────────────────────────────────────────────────
    location /api/ {
        # ──────────────────────────────────────────────────────────
        # 【改/删】rewrite - 路径重写
        # ──────────────────────────────────────────────────────────
        # 作用:将 /api/xxx 转换为后端实际接收的路径
        #
        # 📝 如何选择(三种情况):
        #
        # 情况 1:后端接口是 /xxx(没有 /api 前缀)
        #    前端请求:/api/user → 后端收到:/user
        #    配置:rewrite ^/api/(.*)$ /$1 break;
        #
        # 情况 2:后端接口也是 /api/xxx(有 /api 前缀)
        #    前端请求:/api/user → 后端收到:/api/user
        #    配置:删除 rewrite 这行!
        #
        # 情况 3:后端接口是 /v1/xxx(其他前缀)
        #    前端请求:/api/user → 后端收到:/v1/user
        #    配置:rewrite ^/api/(.*)$ /v1/$1 break;
        #
        # 正则解释:
        #    ^/api/(.*)$ - 匹配 /api/ 开头的所有内容,() 捕获后面部分
        #    /$1         - $1 是捕获的内容,前面加 / 变成 /xxx
        #    break       - 重写后停止匹配其他规则
        # ──────────────────────────────────────────────────────────
        rewrite ^/api/(.*)$ /$1 break;
        
        # ──────────────────────────────────────────────────────────
        # 【改】proxy_pass - 后端服务地址
        # ──────────────────────────────────────────────────────────
        # 📝 为什么要改:
        #    这是你后端服务的实际地址
        #
        # 🔄 举一反三:
        #    http://127.0.0.1:9000       # 本机后端
        #    http://127.0.0.1:8080       # Spring Boot 默认端口
        #    http://localhost:9000       # 等同于 127.0.0.1
        #    http://192.168.1.100:8080   # 局域网其他机器
        #    http://backend-service:8080 # Docker/K8s 服务名
        #    http://10.0.0.5:8080        # 内网服务器 IP
        # ──────────────────────────────────────────────────────────
        proxy_pass http://127.0.0.1:9000;
        
        # ----------------------------------------------------------
        # 请求头转发配置
        # ----------------------------------------------------------
        proxy_set_header Host $host;                                    # 原始域名
        proxy_set_header X-Real-IP $remote_addr;                        # 客户端 IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    # IP 链路
        proxy_set_header X-Forwarded-Proto $scheme;                     # 原始协议
        
        # ----------------------------------------------------------
        # 自定义请求头转发
        # ----------------------------------------------------------
        # 作用:将前端传的自定义头转发给后端
        #
        # 📝 为什么需要:
        #    如果前端用了自定义请求头(如 x-request-user-id)
        #    需要显式转发,后端才能收到
        #
        # 变量命名规则:
        #    请求头 X-Request-User-Id → 变量 $http_x_request_user_id
        #    规则:$http_ + 小写 + 横线变下划线
        # ----------------------------------------------------------
        proxy_set_header x-request-user-id $http_x_request_user_id;     # 自定义用户 ID
        proxy_set_header Authorization $http_authorization;             # JWT Token
    }
}

前端请求示例:

javascript 复制代码
// 同源请求,无需任何跨域配置!
// 直接请求 /api/xxx,Nginx 会转发到后端
axios.get('/api/user')
    .then(res => console.log(res.data));

// 注意:不要写完整域名!
// ❌ 错误:axios.get('http://localhost/api/user')
// ✅ 正确:axios.get('/api/user')

9.5 场景四:Nginx 反向代理 + Cookie(推荐-最常用)

场景: 同源代理,需要携带 Cookie(登录态)

📁 配置文件: /etc/nginx/conf.d/reverse-proxy-with-cookie.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  🌟 场景:反向代理 + Cookie(最常用的生产配置!)                   ║
# ║  用途:需要登录态的生产环境                                        ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# ✨ 此配置的优势:
# ┌─────────────────────────────────────────────────────────────────┐
# │ 1. 彻底消除跨域问题(同源请求)                                    │
# │ 2. Cookie 自动携带,无需前端设置 withCredentials                  │
# │ 3. Set-Cookie 自动保存,无需处理 SameSite                         │
# │ 4. 无需配置任何 CORS 响应头                                       │
# │ 5. 隐藏后端真实地址,更安全                                        │
# └─────────────────────────────────────────────────────────────────┘
#
# 🔧 需要修改的配置项(共 5 处):
# ┌─────────────────────┬────────────────────────────────────────────┐
# │ 配置项               │ 说明                                       │
# ├─────────────────────┼────────────────────────────────────────────┤
# │ listen              │ Nginx 统一入口端口                          │
# │ server_name         │ 访问域名                                    │
# │ root                │ 前端打包文件路径                             │
# │ proxy_pass          │ 后端服务地址                                 │
# │ proxy_cookie_path   │ Cookie 路径映射(通常不需要改)              │
# └─────────────────────┴────────────────────────────────────────────┘

server {
    # ══════════════════════════════════════════════════════════════
    # 【改】listen - 统一入口端口
    # ══════════════════════════════════════════════════════════════
    # 生产环境推荐 80 或 443
    # ──────────────────────────────────────────────────────────────
    listen 80;
    
    # ══════════════════════════════════════════════════════════════
    # 【改】server_name - 域名
    # ══════════════════════════════════════════════════════════════
    # 生产环境改为实际域名,如 www.example.com
    # ──────────────────────────────────────────────────────────────
    server_name localhost;
    
    # ══════════════════════════════════════════════════════════════
    # 前端静态资源配置
    # ══════════════════════════════════════════════════════════════
    #
    # 📦 前端部署流程:
    # ┌─────────────────────────────────────────────────────────────┐
    # │ 1. 前端打包:npm run build                                   │
    # │ 2. 生成目录:dist/ 或 build/                                 │
    # │ 3. 上传到服务器:scp -r dist/* user@server:/usr/share/nginx/html/
    # │ 4. 重载 Nginx:nginx -s reload                              │
    # └─────────────────────────────────────────────────────────────┘
    #
    # 📝 前端配置的后端地址应该怎么写?
    # ┌─────────────────────────────────────────────────────────────┐
    # │ ⚠️ 重要:使用 Nginx 反向代理后,前端请求地址要用相对路径!     │
    # │                                                             │
    # │ ❌ 错误写法(绝对路径,会跨域):                              │
    # │    axios.get('http://192.168.1.100:9000/user')              │
    # │    axios.get('http://localhost:9000/api/user')              │
    # │                                                             │
    # │ ✅ 正确写法(相对路径,走 Nginx 代理):                       │
    # │    axios.get('/api/user')                                   │
    # │    axios.post('/api/login', data)                           │
    # └─────────────────────────────────────────────────────────────┘
    #
    # 🔄 前端配置示例(Vue/React/Angular):
    #
    # ┌─ Vue (vue.config.js 或 vite.config.js) ─────────────────────┐
    # │                                                             │
    # │ // vite.config.js                                           │
    # │ export default {                                            │
    # │   server: {                                                 │
    # │     proxy: {                                                │
    # │       '/api': {                                             │
    # │         target: 'http://localhost:9000',  // 开发时后端地址  │
    # │         changeOrigin: true,                                 │
    # │         rewrite: (path) => path.replace(/^\/api/, '')       │
    # │       }                                                     │
    # │     }                                                       │
    # │   }                                                         │
    # │ }                                                           │
    # │                                                             │
    # │ // 请求代码(开发和生产都用相对路径)                          │
    # │ axios.get('/api/user')                                      │
    # │                                                             │
    # └─────────────────────────────────────────────────────────────┘
    #
    # ┌─ React (setupProxy.js 或 package.json) ─────────────────────┐
    # │                                                             │
    # │ // src/setupProxy.js (开发环境代理)                          │
    # │ const { createProxyMiddleware } = require('http-proxy-middleware');
    # │ module.exports = function(app) {                            │
    # │   app.use('/api', createProxyMiddleware({                   │
    # │     target: 'http://localhost:9000',                        │
    # │     changeOrigin: true,                                     │
    # │     pathRewrite: { '^/api': '' }                            │
    # │   }));                                                      │
    # │ };                                                          │
    # │                                                             │
    # │ // 请求代码                                                  │
    # │ fetch('/api/user').then(res => res.json())                  │
    # │                                                             │
    # └─────────────────────────────────────────────────────────────┘
    #
    # ┌─ axios 封装示例 ────────────────────────────────────────────┐
    # │                                                             │
    # │ // src/utils/request.js                                     │
    # │ import axios from 'axios';                                  │
    # │                                                             │
    # │ const request = axios.create({                              │
    # │   baseURL: '/api',  // ⭐ 关键:使用相对路径                  │
    # │   timeout: 10000                                            │
    # │ });                                                         │
    # │                                                             │
    # │ // 使用                                                      │
    # │ request.get('/user')     // 实际请求 /api/user              │
    # │ request.post('/login')   // 实际请求 /api/login             │
    # │                                                             │
    # └─────────────────────────────────────────────────────────────┘
    #
    location / {
        # ──────────────────────────────────────────────────────────
        # 【改】root - 前端打包文件路径
        # ──────────────────────────────────────────────────────────
        # 作用:指定前端静态文件的存放目录
        #
        # 📝 前端文件应该放到这个目录下:
        #    假设 root 配置为 /usr/share/nginx/html
        #    那么目录结构应该是:
        #
        #    /usr/share/nginx/html/
        #    ├── index.html          ← 入口文件(必须)
        #    ├── favicon.ico         ← 网站图标
        #    └── assets/             ← 静态资源目录
        #        ├── js/
        #        │   ├── app.js
        #        │   └── chunk-xxx.js
        #        ├── css/
        #        │   └── app.css
        #        └── img/
        #            └── logo.png
        #
        # 🔄 举一反三(常见路径):
        #    /usr/share/nginx/html         # Nginx 默认目录(推荐)
        #    /var/www/html                 # Linux 传统 Web 目录
        #    /var/www/my-project/dist      # 项目专用目录
        #    /home/deploy/frontend         # 部署用户目录
        #    D:/nginx/html                 # Windows 示例
        # ──────────────────────────────────────────────────────────
        root /usr/share/nginx/html;
        
        # 默认首页文件
        index index.html;
        
        # ──────────────────────────────────────────────────────────
        # try_files - SPA 单页应用路由支持(重要!)
        # ──────────────────────────────────────────────────────────
        # 语法:try_files <尝试1> <尝试2> ... <最终回退>;
        #
        # 📝 详细解释 try_files $uri $uri/ /index.html:
        #
        # 假设用户访问 http://example.com/user/123
        #
        # ┌─────────────────────────────────────────────────────────┐
        # │ 第一步:尝试 $uri                                        │
        # │   Nginx 检查文件:/usr/share/nginx/html/user/123        │
        # │   结果:文件不存在 ❌                                     │
        # │   继续下一步...                                          │
        # ├─────────────────────────────────────────────────────────┤
        # │ 第二步:尝试 $uri/                                       │
        # │   Nginx 检查目录:/usr/share/nginx/html/user/123/       │
        # │   如果存在,返回目录下的 index.html                       │
        # │   结果:目录不存在 ❌                                     │
        # │   继续下一步...                                          │
        # ├─────────────────────────────────────────────────────────┤
        # │ 第三步:回退到 /index.html                               │
        # │   Nginx 返回:/usr/share/nginx/html/index.html          │
        # │   结果:返回首页 ✅                                       │
        # │   前端 JS 路由接管,根据 URL 显示对应页面                  │
        # └─────────────────────────────────────────────────────────┘
        #
        # 📝 为什么 SPA 必须配置这个?
        #
        # ┌─ 问题场景 ──────────────────────────────────────────────┐
        # │                                                         │
        # │ SPA 的路由是前端 JS 控制的(Vue Router/React Router)    │
        # │                                                         │
        # │ 用户直接访问 http://example.com/user/123 时:            │
        # │                                                         │
        # │ 没有 try_files:                                        │
        # │   → Nginx 找不到 /user/123 文件                         │
        # │   → 返回 404 错误 ❌                                     │
        # │                                                         │
        # │ 有 try_files:                                          │
        # │   → Nginx 返回 index.html                               │
        # │   → 前端 JS 加载                                        │
        # │   → Vue/React Router 解析 URL                          │
        # │   → 显示 /user/123 对应的组件 ✅                        │
        # │                                                         │
        # └─────────────────────────────────────────────────────────┘
        #
        # 🔄 其他 try_files 写法:
        #    try_files $uri $uri/ /index.html;     # 标准 SPA(最常用)
        #    try_files $uri $uri/ =404;            # 非 SPA,找不到返回 404
        #    try_files $uri /index.php?$args;      # PHP 框架常用
        # ──────────────────────────────────────────────────────────
        try_files $uri $uri/ /index.html;
        
        # ----------------------------------------------------------
        # 静态资源缓存配置(可选,提升性能)
        # ----------------------------------------------------------
        # 作用:让浏览器缓存静态文件,减少重复请求
        #
        # ~* 是不区分大小写的正则匹配
        # \. 匹配点号
        # (js|css|...) 匹配这些后缀的文件
        # $ 表示以这些后缀结尾
        #
        # expires 7d:告诉浏览器缓存 7 天
        # immutable:告诉浏览器这个文件不会变(文件名带 hash 时用)
        # ----------------------------------------------------------
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
            expires 7d;                                              # 缓存 7 天
            add_header Cache-Control "public, immutable";            # 可被缓存且不变
        }
    }
    
    # ══════════════════════════════════════════════════════════════
    # API 反向代理配置(带 Cookie)
    # ══════════════════════════════════════════════════════════════
    #
    # 📝 location 匹配优先级说明:
    # ┌─────────────────────────────────────────────────────────────┐
    # │ Nginx 会按以下优先级匹配 location:                          │
    # │                                                             │
    # │ 1. = /path      精确匹配(最高优先级)                       │
    # │ 2. ^~ /path     前缀匹配(匹配后停止正则搜索)                │
    # │ 3. ~ 或 ~*      正则匹配(~区分大小写,~*不区分)             │
    # │ 4. /path        普通前缀匹配(按最长匹配原则)                │
    # │                                                             │
    # │ 本配置中 location / 和 location /api/ 都是普通前缀匹配       │
    # │ 按最长匹配原则:/api/user 会匹配 /api/(更长)而不是 /       │
    # └─────────────────────────────────────────────────────────────┘
    #
    # 🔄 匹配示例:
    # ┌───────────────────┬──────────────────┬──────────────────────┐
    # │ 请求路径           │ 匹配的 location   │ 原因                 │
    # ├───────────────────┼──────────────────┼──────────────────────┤
    # │ /index.html       │ location /       │ 只匹配 /             │
    # │ /assets/app.js    │ location /       │ 只匹配 /             │
    # │ /api/user         │ location /api/   │ /api/ 更长,优先匹配  │
    # │ /api/login        │ location /api/   │ /api/ 更长,优先匹配  │
    # │ /api              │ location /       │ 没有尾部 /,匹配 /    │
    # │ /api/             │ location /api/   │ 完整匹配 /api/       │
    # │ /apixxx           │ location /       │ 不是 /api/ 前缀      │
    # └───────────────────┴──────────────────┴──────────────────────┘
    #
    # ⚠️ 注意:location /api/ 末尾的 / 很重要!
    #    /api/  → 只匹配 /api/xxx(推荐,更精确)
    #    /api   → 会匹配 /api 和 /apixxx(可能误匹配)
    #
    # ══════════════════════════════════════════════════════════════
    # ⚠️ 重要提醒:前端路由不要以 /api 开头!
    # ══════════════════════════════════════════════════════════════
    #
    # 📝 原因:
    #    如果前端路由用了 /api 开头,会被 Nginx 转发到后端
    #    而不是返回 index.html 让前端路由处理
    #
    # ❌ 错误的前端路由设计:
    #    /api/users          → 被 Nginx 转发到后端(找不到页面)
    #    /api/dashboard      → 被 Nginx 转发到后端(找不到页面)
    #    /api-management     → 被 Nginx 转发到后端(如果用 /api 没有 /)
    #
    # ✅ 正确的前端路由设计:
    #    /users              → 返回 index.html,前端路由处理
    #    /dashboard          → 返回 index.html,前端路由处理
    #    /user/123           → 返回 index.html,前端路由处理
    #    /admin/api-list     → 返回 index.html,前端路由处理(api 在中间没问题)
    #
    # 🔄 常见的路径规划:
    # ┌───────────────────┬───────────────────────────────────────┐
    # │ 前端路由(页面)    │ /login, /home, /user/:id, /dashboard │
    # │ 后端 API          │ /api/login, /api/user, /api/data     │
    # │ 静态资源          │ /assets/*, /images/*, /js/*, /css/*  │
    # └───────────────────┴───────────────────────────────────────┘
    #
    location /api/ {
        # ──────────────────────────────────────────────────────────
        # 【改/删】rewrite - 路径重写
        # ──────────────────────────────────────────────────────────
        # 参考场景三的详细说明
        # ──────────────────────────────────────────────────────────
        rewrite ^/api/(.*)$ /$1 break;
        
        # ──────────────────────────────────────────────────────────
        # 【改】proxy_pass - 后端服务地址
        # ──────────────────────────────────────────────────────────
        # 参考场景三的详细说明
        # ──────────────────────────────────────────────────────────
        proxy_pass http://127.0.0.1:9000;
        
        # ----------------------------------------------------------
        # 请求头转发配置
        # ----------------------------------------------------------
        proxy_set_header Host $host;                                    # 原始域名
        proxy_set_header X-Real-IP $remote_addr;                        # 客户端 IP
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    # IP 链路
        proxy_set_header X-Forwarded-Proto $scheme;                     # 原始协议
        
        # ══════════════════════════════════════════════════════════
        # Cookie 相关配置(关键!)
        # ══════════════════════════════════════════════════════════
        
        # ----------------------------------------------------------
        # 转发请求中的 Cookie 到后端
        # ----------------------------------------------------------
        # 作用:将浏览器发来的 Cookie(如 JSESSIONID)转发给后端
        # ----------------------------------------------------------
        proxy_set_header Cookie $http_cookie;
        
        # ----------------------------------------------------------
        # 透传后端响应的 Set-Cookie 到浏览器
        # ----------------------------------------------------------
        # 作用:后端登录成功返回 Set-Cookie,透传给浏览器保存
        # ----------------------------------------------------------
        proxy_pass_header Set-Cookie;
        
        # ══════════════════════════════════════════════════════════
        # 【改】proxy_cookie_path - Cookie 路径映射
        # ══════════════════════════════════════════════════════════
        # 作用:修改后端返回的 Cookie 的 path 属性
        #
        # 📝 为什么可能需要改:
        #    后端设置的 Cookie path 可能和前端访问路径不一致
        #    比如后端设置 path=/,但前端只访问 /api/
        #    需要映射才能让 Cookie 在正确的路径下生效
        #
        # 语法:proxy_cookie_path <后端路径> <前端路径>;
        #
        # 🔄 举一反三:
        #    proxy_cookie_path / /;                    # 通常够用,不改路径
        #    proxy_cookie_path /user-service /api;    # 微服务:后端 /user-service → 前端 /api
        #    proxy_cookie_path /backend /;            # 后端 /backend → 前端根路径
        #    proxy_cookie_path ~^/(.+)$ /$1;          # 正则匹配,保持不变
        # ──────────────────────────────────────────────────────────
        proxy_cookie_path / /;
        
        # ══════════════════════════════════════════════════════════
        # 【改】proxy_cookie_domain - Cookie 域名映射(通常不需要)
        # ══════════════════════════════════════════════════════════
        # 作用:修改后端返回的 Cookie 的 domain 属性
        #
        # 📝 什么时候需要:
        #    后端设置的 Cookie domain 和前端访问的域名不同时
        #    比如后端 domain=backend.internal,前端是 www.example.com
        #
        # 语法:proxy_cookie_domain <后端域名> <前端域名>;
        #
        # 🔄 举一反三:
        #    proxy_cookie_domain backend.local www.example.com;
        #    proxy_cookie_domain ~\.backend\.com$ .example.com;  # 正则
        #    proxy_cookie_domain off;                            # 关闭(不修改)
        #
        # ⚠️ 注意:大多数情况下不需要配置这个
        #    因为同源代理时,后端通常不设置 domain,浏览器自动用当前域名
        # ──────────────────────────────────────────────────────────
        # proxy_cookie_domain backend.internal www.example.com;
        
        # ----------------------------------------------------------
        # 自定义请求头转发
        # ----------------------------------------------------------
        proxy_set_header Authorization $http_authorization;             # JWT Token
        proxy_set_header x-request-user-id $http_x_request_user_id;     # 用户 ID
    }
}

前端请求示例:

javascript 复制代码
// ✨ 同源请求,一切自动!
// 无需 withCredentials,Cookie 自动携带!
// 无需处理跨域,因为根本没有跨域!

// 登录请求
axios.post('/api/login', { 
    username: 'admin', 
    password: '123456' 
}).then(res => {
    console.log('登录成功');
    // Set-Cookie 自动保存到浏览器,无需处理
});

// 获取用户信息(Cookie 自动携带)
axios.get('/api/user')
    .then(res => console.log(res.data));

9.6 路径替换详解

📌 选择指南:根据你的后端 API 路径选择合适的配置

方式一:rewrite 指令(推荐)
nginx 复制代码
# ============================================================
# 场景:前端请求 /api/xxx,后端接口是 /xxx
# 示例:/api/user/123 → /user/123
# ============================================================
location /api/ {
    rewrite ^/api/(.*)$ /$1 break;
    
    # 【改】后端地址
    proxy_pass http://127.0.0.1:9000;
}
方式二:proxy_pass 带路径
nginx 复制代码
# ============================================================
# 场景:前端请求 /api/xxx,后端接口是 /v1/xxx
# 示例:/api/user/123 → /v1/user/123
# ============================================================
location /api/ {
    # 【改】后端地址 + 路径前缀
    # ⚠️ 注意:末尾的 / 必须有!
    proxy_pass http://127.0.0.1:9000/v1/;
}
方式三:保留原路径
nginx 复制代码
# ============================================================
# 场景:前端请求 /api/xxx,后端接口也是 /api/xxx
# 示例:/api/user/123 → /api/user/123(不变)
# ============================================================
location /api/ {
    # 【改】后端地址
    # ⚠️ 注意:末尾没有 /
    proxy_pass http://127.0.0.1:9000;
}
路径替换速查表
你的情况 配置方式 前端请求 后端收到
后端也是 /api/ 开头 proxy_pass http://backend; /api/user /api/user
后端是 / 开头 proxy_pass http://backend/; /api/user /user
后端是 /v2/ 开头 proxy_pass http://backend/v2/; /api/user /v2/user
复杂路径转换 rewrite + proxy_pass /api/user 自定义

9.7 从零开始部署 HTTPS(域名 + 证书 + Nginx 完整流程)

📌 从购买域名到 HTTPS 上线的完整流程,按步骤操作即可!


🎯 架构说明:SSL 终止(SSL Termination)
复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                                                                         │
│  用户浏览器  ──HTTPS──▶  Nginx  ──HTTP──▶  后端服务                      │
│              (加密)      (解密)    (内网明文)  (不需要配置 SSL)           │
│                                                                         │
│  ✅ 优点:                                                               │
│     1. 后端服务不需要配置 SSL 证书                                       │
│     2. 证书只需要在 Nginx 上管理,方便维护                                │
│     3. 减轻后端服务器的加密计算负担                                       │
│     4. 内网传输用 HTTP,性能更好                                         │
│                                                                         │
│  📝 回答你的问题:                                                        │
│     是的!只需要给 Nginx 配置 HTTPS,后端服务继续用 HTTP!                │
│     Nginx 的 proxy_pass http://127.0.0.1:8080 就是用 HTTP 连接后端      │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

步骤零:购买域名(完整流程)
1. 选择域名注册商
注册商 网址 特点 .com 首年价格
阿里云(万网) https://wanwang.aliyun.com 国内首选,中文界面 ¥55-69
腾讯云 https://dnspod.cloud.tencent.com 国内备选 ¥55-65
Cloudflare https://www.cloudflare.com 国外首选,无加价续费 $8.57
Namecheap https://www.namecheap.com 国外老牌 $5.98(首年)
GoDaddy https://www.godaddy.com 全球最大 $11.99
2. 域名购买流程(以阿里云为例)
复制代码
╔══════════════════════════════════════════════════════════════════════════╗
║  阿里云域名购买流程                                                        ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  第一步:注册/登录阿里云账号                                               ║
║  ─────────────────────────────────────────────────────────────────       ║
║  访问:https://www.aliyun.com                                            ║
║  点击右上角「登录」或「免费注册」                                           ║
║  完成实名认证(购买域名必须)                                               ║
║                                                                          ║
║  第二步:搜索域名                                                          ║
║  ─────────────────────────────────────────────────────────────────       ║
║  访问:https://wanwang.aliyun.com                                        ║
║  输入想要的域名(如 mycompany)                                            ║
║  系统会显示各种后缀的可用情况:                                             ║
║    mycompany.com     ¥55/年  ✓可注册                                     ║
║    mycompany.cn      ¥29/年  ✓可注册                                     ║
║    mycompany.net     ¥69/年  ✓可注册                                     ║
║                                                                          ║
║  💡 域名选择建议:                                                         ║
║     - .com 首选(国际通用,用户习惯)                                       ║
║     - .cn 国内业务可选(需要备案)                                          ║
║     - 避免太长或难记的域名                                                  ║
║     - 避免容易拼错的域名                                                    ║
║                                                                          ║
║  第三步:加入购物车并支付                                                   ║
║  ─────────────────────────────────────────────────────────────────       ║
║  选择购买年限(建议至少 1 年)                                              ║
║  填写域名持有者信息(建议用企业名称)                                        ║
║  完成支付                                                                 ║
║                                                                          ║
║  第四步:域名实名认证                                                       ║
║  ─────────────────────────────────────────────────────────────────       ║
║  购买后需要完成实名认证才能正常使用                                          ║
║  上传身份证/营业执照                                                       ║
║  等待审核(通常 1-3 个工作日)                                              ║
║                                                                          ║
╚══════════════════════════════════════════════════════════════════════════╝
3. 域名解析配置(指向你的服务器)
复制代码
╔══════════════════════════════════════════════════════════════════════════╗
║  域名解析配置流程                                                          ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  第一步:进入域名管理控制台                                                 ║
║  ─────────────────────────────────────────────────────────────────       ║
║  阿里云:https://dc.console.aliyun.com/next/index#/domain/list           ║
║  腾讯云:https://console.cloud.tencent.com/domain                        ║
║  找到你的域名 → 点击「解析」                                               ║
║                                                                          ║
║  第二步:添加解析记录                                                       ║
║  ─────────────────────────────────────────────────────────────────       ║
║                                                                          ║
║  📝 记录 1:主域名(example.com)                                          ║
║  ┌──────────┬─────────────────────────────────────────┐                  ║
║  │ 记录类型  │ A                                       │                  ║
║  │ 主机记录  │ @(代表主域名)                          │                  ║
║  │ 记录值    │ 【改】你的服务器公网 IP,如 47.96.123.45 │                  ║
║  │ TTL      │ 10 分钟(默认)                          │                  ║
║  └──────────┴─────────────────────────────────────────┘                  ║
║                                                                          ║
║  📝 记录 2:www 子域名(www.example.com)                                  ║
║  ┌──────────┬─────────────────────────────────────────┐                  ║
║  │ 记录类型  │ A                                       │                  ║
║  │ 主机记录  │ www                                     │                  ║
║  │ 记录值    │ 【改】你的服务器公网 IP,如 47.96.123.45 │                  ║
║  │ TTL      │ 10 分钟(默认)                          │                  ║
║  └──────────┴─────────────────────────────────────────┘                  ║
║                                                                          ║
║  📝 可选记录 3:API 子域名(api.example.com)                               ║
║  ┌──────────┬─────────────────────────────────────────┐                  ║
║  │ 记录类型  │ A                                       │                  ║
║  │ 主机记录  │ api                                     │                  ║
║  │ 记录值    │ 【改】你的服务器公网 IP                   │                  ║
║  │ TTL      │ 10 分钟                                  │                  ║
║  └──────────┴─────────────────────────────────────────┘                  ║
║                                                                          ║
║  第三步:等待解析生效                                                       ║
║  ─────────────────────────────────────────────────────────────────       ║
║  通常需要 5-10 分钟,最长可能需要 48 小时(DNS 缓存)                        ║
║                                                                          ║
║  第四步:验证解析是否生效                                                   ║
║  ─────────────────────────────────────────────────────────────────       ║
║                                                                          ║
║  # 方法 1:ping 域名                                                      ║
║  ping example.com                                                        ║
║  # 如果返回你的服务器 IP,说明解析成功                                       ║
║                                                                          ║
║  # 方法 2:nslookup 查询                                                  ║
║  nslookup example.com                                                    ║
║                                                                          ║
║  # 方法 3:在线工具                                                        ║
║  https://tool.chinaz.com/dns/                                            ║
║                                                                          ║
╚══════════════════════════════════════════════════════════════════════════╝
4. 备案说明(国内服务器必须)
复制代码
╔══════════════════════════════════════════════════════════════════════════╗
║  ICP 备案说明                                                             ║
╠══════════════════════════════════════════════════════════════════════════╣
║                                                                          ║
║  ⚠️ 什么情况需要备案?                                                     ║
║  ─────────────────────────────────────────────────────────────────       ║
║  服务器在中国大陆 → 必须备案(否则无法访问)                                 ║
║  服务器在香港/国外 → 不需要备案                                             ║
║                                                                          ║
║  📝 备案流程(以阿里云为例,约 15-20 个工作日):                            ║
║  ─────────────────────────────────────────────────────────────────       ║
║  1. 登录阿里云备案系统:https://beian.aliyun.com                           ║
║  2. 填写主体信息(公司/个人)                                               ║
║  3. 填写网站信息                                                          ║
║  4. 上传证件照片                                                          ║
║  5. 阿里云初审(1-2 天)                                                   ║
║  6. 工信部审核(10-20 天)                                                 ║
║  7. 备案成功,获得备案号                                                   ║
║                                                                          ║
║  💡 快速上线方案(不想等备案):                                            ║
║  ─────────────────────────────────────────────────────────────────       ║
║  使用香港/海外服务器,无需备案,即买即用                                     ║
║  - 阿里云香港                                                             ║
║  - 腾讯云香港                                                             ║
║  - AWS 东京/新加坡                                                        ║
║  - Vultr 东京                                                            ║
║                                                                          ║
╚══════════════════════════════════════════════════════════════════════════╝

完整部署流程图
复制代码
┌─────────────────────────────────────────────────────────────────────────┐
│                     从零到 HTTPS 上线完整流程                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ① 购买域名                        ⏱️ 10 分钟                            │
│     └─ 阿里云/腾讯云/Cloudflare                                          │
│                 ↓                                                       │
│  ② 购买服务器                      ⏱️ 5 分钟                             │
│     └─ 选择香港服务器(免备案)或国内服务器(需备案)                        │
│                 ↓                                                       │
│  ③ 域名解析                        ⏱️ 5-10 分钟生效                       │
│     └─ 添加 A 记录,指向服务器 IP                                         │
│                 ↓                                                       │
│  ④ 安装 Nginx                      ⏱️ 2 分钟                             │
│     └─ apt install nginx                                                │
│                 ↓                                                       │
│  ⑤ 申请 SSL 证书                   ⏱️ 2 分钟                             │
│     └─ certbot --nginx -d example.com                                   │
│                 ↓                                                       │
│  ⑥ 配置 Nginx                      ⏱️ 5 分钟                             │
│     └─ 复制模板,修改 6 处配置                                            │
│                 ↓                                                       │
│  ⑦ 部署前端 + 启动后端             ⏱️ 根据项目                            │
│     └─ 上传前端文件,启动后端服务                                          │
│                 ↓                                                       │
│  ✅ 完成!访问 https://example.com                                        │
│                                                                         │
│  总耗时:约 30 分钟(不含备案)                                            │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

步骤一:获取 SSL 证书
方式 A:Let's Encrypt 免费证书(推荐)
bash 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  Let's Encrypt 免费证书申请流程                                   ║
# ║  有效期:90 天(自动续期)                                         ║
# ║  费用:完全免费                                                    ║
# ╚══════════════════════════════════════════════════════════════════╝

# 1. 安装 Certbot(证书申请工具)
# ──────────────────────────────────────────────────────────────────

# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx -y

# CentOS/RHEL
sudo yum install epel-release -y
sudo yum install certbot python3-certbot-nginx -y

# 2. 申请证书(自动配置 Nginx)
# ──────────────────────────────────────────────────────────────────
# 【改】把 example.com 和 www.example.com 改成你的域名
# ⚠️ 前提:域名必须已经解析到这台服务器!

sudo certbot --nginx -d example.com -d www.example.com

# 按提示操作:
# - 输入邮箱(用于证书到期提醒)
# - 同意服务条款(输入 Y)
# - 是否分享邮箱(输入 N)
# - 是否重定向 HTTP 到 HTTPS(输入 2,推荐)

# 3. 证书文件位置
# ──────────────────────────────────────────────────────────────────
# 申请成功后,证书存放在:
#
# /etc/letsencrypt/live/example.com/
# ├── fullchain.pem    ← 证书文件(配置用这个)
# ├── privkey.pem      ← 私钥文件(配置用这个)
# ├── cert.pem         ← 证书(不含中间证书)
# └── chain.pem        ← 中间证书

# 4. 自动续期测试
# ──────────────────────────────────────────────────────────────────
# Certbot 会自动添加定时任务续期,测试一下:
sudo certbot renew --dry-run

# 5. 手动续期(如果需要)
# ──────────────────────────────────────────────────────────────────
sudo certbot renew
方式 B:阿里云/腾讯云免费证书
bash 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  云服务商免费证书申请流程                                          ║
# ║  有效期:1 年(到期需手动续期)                                     ║
# ║  费用:免费(每个账号有额度限制)                                   ║
# ╚══════════════════════════════════════════════════════════════════╝

# 1. 申请证书
# ──────────────────────────────────────────────────────────────────
# 阿里云:https://yundun.console.aliyun.com/?p=cas
# 腾讯云:https://console.cloud.tencent.com/ssl
#
# 步骤:
# - 选择「免费证书」→「申请」
# - 填写域名(如 example.com)
# - 选择验证方式(推荐 DNS 验证)
# - 按提示添加 DNS 解析记录
# - 等待验证通过(几分钟到几小时)
# - 下载证书(选择 Nginx 格式)

# 2. 上传证书到服务器
# ──────────────────────────────────────────────────────────────────
# 下载的证书包通常包含:
# - example.com.pem   或 fullchain.crt  ← 证书文件
# - example.com.key   或 private.key    ← 私钥文件

# 创建证书目录
sudo mkdir -p /etc/nginx/ssl

# 上传证书(本地执行)
# 【改】替换为你的文件名和服务器地址
scp example.com.pem root@your-server:/etc/nginx/ssl/
scp example.com.key root@your-server:/etc/nginx/ssl/

# 3. 设置权限(服务器上执行)
# ──────────────────────────────────────────────────────────────────
sudo chmod 644 /etc/nginx/ssl/example.com.pem
sudo chmod 600 /etc/nginx/ssl/example.com.key
方式 C:自签名证书(仅限测试)
bash 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  自签名证书(仅限本地测试,浏览器会显示不安全警告)                  ║
# ╚══════════════════════════════════════════════════════════════════╝

# 创建证书目录
sudo mkdir -p /etc/nginx/ssl

# 生成自签名证书(有效期 365 天)
# 【改】把 localhost 改成你的域名或 IP
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
    -keyout /etc/nginx/ssl/server.key \
    -out /etc/nginx/ssl/server.crt \
    -subj "/C=CN/ST=Beijing/L=Beijing/O=Test/CN=localhost"

# 文件说明:
# /etc/nginx/ssl/server.crt  ← 证书文件
# /etc/nginx/ssl/server.key  ← 私钥文件

步骤二:Nginx HTTPS 配置

📁 配置文件: /etc/nginx/conf.d/https.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  HTTPS 完整配置模板(复制后修改 6 处即可使用)                      ║
# ╚══════════════════════════════════════════════════════════════════╝
#
# 🔧 需要修改的配置项(共 6 处):
# ┌─────┬─────────────────────┬────────────────────────────────────┐
# │ 序号 │ 配置项               │ 示例值                             │
# ├─────┼─────────────────────┼────────────────────────────────────┤
# │  1  │ server_name         │ example.com www.example.com        │
# │  2  │ ssl_certificate     │ /etc/nginx/ssl/example.com.pem     │
# │  3  │ ssl_certificate_key │ /etc/nginx/ssl/example.com.key     │
# │  4  │ root                │ /var/www/html                      │
# │  5  │ proxy_pass          │ http://127.0.0.1:8080              │
# │  6  │ rewrite(可选)      │ 根据后端路径调整                    │
# └─────┴─────────────────────┴────────────────────────────────────┘

# ══════════════════════════════════════════════════════════════════
# HTTP → HTTPS 自动重定向
# ══════════════════════════════════════════════════════════════════
# 作用:用户访问 http://example.com 自动跳转到 https://example.com
server {
    listen 80;                                    # 监听 HTTP 80 端口
    
    # 【改-1】你的域名(多个用空格分隔)
    server_name example.com www.example.com;
    
    # 301 永久重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

# ══════════════════════════════════════════════════════════════════
# HTTPS 主配置
# ══════════════════════════════════════════════════════════════════
server {
    listen 443 ssl http2;                         # 监听 HTTPS 443 端口,启用 HTTP/2
    
    # 【改-1】你的域名(与上面保持一致)
    server_name example.com www.example.com;
    
    # ──────────────────────────────────────────────────────────────
    # 【改-2】SSL 证书文件路径
    # ──────────────────────────────────────────────────────────────
    # Let's Encrypt 证书:
    #   ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    #   ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    #
    # 阿里云/腾讯云证书:
    #   ssl_certificate /etc/nginx/ssl/example.com.pem;
    #   ssl_certificate_key /etc/nginx/ssl/example.com.key;
    #
    # 自签名证书:
    #   ssl_certificate /etc/nginx/ssl/server.crt;
    #   ssl_certificate_key /etc/nginx/ssl/server.key;
    # ──────────────────────────────────────────────────────────────
    ssl_certificate /etc/nginx/ssl/example.com.pem;           # 【改-2】证书路径
    ssl_certificate_key /etc/nginx/ssl/example.com.key;       # 【改-3】私钥路径
    
    # ──────────────────────────────────────────────────────────────
    # SSL 安全配置(通常不需要改)
    # ──────────────────────────────────────────────────────────────
    ssl_session_timeout 1d;                       # SSL 会话超时
    ssl_session_cache shared:SSL:50m;             # SSL 会话缓存
    ssl_session_tickets off;                      # 禁用会话票据(更安全)
    
    # 只允许安全的 TLS 版本(TLS 1.2 和 1.3)
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # 加密套件配置(推荐配置)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    
    # ──────────────────────────────────────────────────────────────
    # 安全响应头(防止常见攻击)
    # ──────────────────────────────────────────────────────────────
    add_header X-Frame-Options "SAMEORIGIN" always;             # 防点击劫持
    add_header X-Content-Type-Options "nosniff" always;         # 防 MIME 嗅探
    add_header X-XSS-Protection "1; mode=block" always;         # 防 XSS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;  # HSTS
    
    # 请求体大小限制
    client_max_body_size 100M;
    
    # ══════════════════════════════════════════════════════════════
    # 【改-4】前端静态资源配置
    # ══════════════════════════════════════════════════════════════
    location / {
        root /var/www/html;                       # 【改-4】前端文件路径
        index index.html;
        try_files $uri $uri/ /index.html;         # SPA 路由支持
        
        # 静态资源缓存
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
            expires 30d;
            add_header Cache-Control "public, immutable";
        }
    }
    
    # ══════════════════════════════════════════════════════════════
    # 【改-5/6】API 反向代理配置
    # ══════════════════════════════════════════════════════════════
    location /api/ {
        # 【改-6】路径重写(如果后端没有 /api 前缀)
        rewrite ^/api/(.*)$ /$1 break;
        # 如果后端也是 /api/ 开头,删除上面这行
        
        # 【改-5】后端服务地址
        proxy_pass http://127.0.0.1:8080;
        
        # 请求头转发
        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;             # 告诉后端是 HTTPS
        
        # Cookie 转发
        proxy_set_header Cookie $http_cookie;
        proxy_pass_header Set-Cookie;
        proxy_cookie_path / /api/;
        
        # 自定义请求头
        proxy_set_header Authorization $http_authorization;
        proxy_set_header x-request-user-id $http_x_request_user_id;
        
        # 超时配置
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
    
    # 健康检查(可选)
    location /health {
        access_log off;
        return 200 "OK";
        add_header Content-Type text/plain;
    }
}

步骤三:应用配置
bash 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  配置完成后的操作步骤                                              ║
# ╚══════════════════════════════════════════════════════════════════╝

# 1. 检查配置语法
sudo nginx -t

# 输出应该是:
# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# nginx: configuration file /etc/nginx/nginx.conf test is successful

# 2. 如果语法检查失败,查看错误详情
sudo nginx -t 2>&1

# 常见错误:
# - "cannot load certificate" → 证书路径错误或文件不存在
# - "SSL_CTX_use_PrivateKey_file failed" → 私钥文件格式错误
# - "unknown directive" → 配置语法错误

# 3. 重新加载配置(不中断服务)
sudo nginx -s reload

# 或者重启 Nginx(会短暂中断)
sudo systemctl restart nginx

# 4. 检查 HTTPS 是否生效
curl -I https://example.com

# 5. 检查证书信息
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates

# 输出示例:
# notBefore=Dec  8 00:00:00 2025 GMT
# notAfter=Mar  8 23:59:59 2026 GMT

步骤四:防火墙配置
bash 复制代码
# ╔══════════════════════════════════════════════════════════════════╗
# ║  开放 HTTPS 端口(如果有防火墙)                                   ║
# ╚══════════════════════════════════════════════════════════════════╝

# Ubuntu/Debian (UFW)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload

# CentOS/RHEL (firewalld)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload

# 云服务器安全组
# 阿里云/腾讯云/AWS 等需要在控制台安全组中放行:
# - 入方向:TCP 80 端口
# - 入方向:TCP 443 端口

HTTPS 配置速查表
步骤 命令/操作 说明
1 certbot --nginx -d example.com 申请免费证书
2 编辑 /etc/nginx/conf.d/https.conf 配置 Nginx
3 nginx -t 检查配置语法
4 nginx -s reload 重载配置
5 curl -I https://example.com 测试 HTTPS
6 certbot renew --dry-run 测试证书续期

9.8 Nginx 解决跨站问题(不同域名)

场景: 前端 https://a.com 请求后端 https://b.com(跨站 + 需要 Cookie)

方案一:Nginx 添加 CORS + 后端配置 SameSite(较复杂)

📁 配置文件: /etc/nginx/conf.d/cross-site-cors.conf

nginx 复制代码
# ============================================================
# 场景:跨站请求 + 需要 Cookie(不推荐,配置复杂)
# 限制:必须 HTTPS,后端需配合设置 SameSite=None
# 
# 🔧 需要修改的地方:
#    1. server_name 后端域名
#    2. ssl_certificate SSL 证书路径
#    3. Access-Control-Allow-Origin 前端域名
#    4. proxy_pass 后端地址
# ============================================================

server {
    listen 443 ssl;
    
    # 【改】后端域名
    server_name b.com;
    
    # 【改】SSL 证书路径
    ssl_certificate /etc/nginx/ssl/b.com.crt;
    ssl_certificate_key /etc/nginx/ssl/b.com.key;
    
    location / {
        # 【改】前端域名(必须具体,不能用 *)
        add_header 'Access-Control-Allow-Origin' 'https://a.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, x-request-user-id' always;
        add_header 'Access-Control-Max-Age' '3600' always;
        
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # 【改】后端地址
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Cookie $http_cookie;
        proxy_pass_header Set-Cookie;
        
        # ⚠️ 后端必须设置 Cookie: SameSite=None; Secure
        # Nginx 原生无法修改 Set-Cookie 的 SameSite 属性
    }
}

⚠️ 此方案需要后端配合设置 SameSite=None; Secure,否则 Cookie 无法跨站发送!

方案二:统一域名反向代理(强烈推荐)

📁 配置文件: /etc/nginx/conf.d/unified-domain.conf

nginx 复制代码
# ============================================================
# 场景:将前端和后端统一到同一域名,彻底消除跨站
# 优势:无需处理 SameSite,Cookie 自动携带
# 
# 🔧 需要修改的地方:
#    1. server_name 统一域名
#    2. ssl_certificate SSL 证书路径
#    3. 前端服务器地址
#    4. 后端服务器地址
# ============================================================

server {
    listen 443 ssl http2;
    
    # 【改】统一域名(前端后端都用这个域名访问)
    server_name www.example.com;
    
    # 【改】SSL 证书路径
    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    
    # ==================== 前端 ====================
    location / {
        # 【改】前端服务器地址(原 a.com 的服务)
        # 方式1:代理到前端开发服务器
        proxy_pass http://127.0.0.1:3000;
        # 方式2:直接托管静态文件
        # root /var/www/frontend/dist;
        # try_files $uri $uri/ /index.html;
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    # ==================== 后端 API ====================
    location /api/ {
        # 【改】路径重写(根据后端实际路径)
        rewrite ^/api/(.*)$ /$1 break;
        
        # 【改】后端服务器地址(原 b.com 的服务)
        proxy_pass http://127.0.0.1:9000;
        
        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;
        
        # Cookie 转发
        proxy_set_header Cookie $http_cookie;
        proxy_pass_header Set-Cookie;
        
        # 【改】Cookie 路径映射
        proxy_cookie_path / /api/;
    }
}

前端代码无需任何修改:

javascript 复制代码
// 同站同源,一切自动!
axios.get('/api/user')
    .then(res => console.log(res.data));

📌 仅当无法统一域名时使用此方案

📁 配置文件: /etc/nginx/conf.d/cross-site-lua.conf

nginx 复制代码
# ============================================================
# 场景:跨站 + Cookie,使用 Lua 修改 Set-Cookie
# 前提:必须安装 OpenResty 或 nginx-lua-module
# 
# 🔧 需要修改的地方:
#    1. server_name 后端域名
#    2. ssl_certificate SSL 证书路径
#    3. Access-Control-Allow-Origin 前端域名
#    4. proxy_pass 后端地址
# 
# 📦 安装 OpenResty:
#    Ubuntu: apt install openresty
#    CentOS: yum install openresty
# ============================================================

server {
    listen 443 ssl;
    
    # 【改】后端域名
    server_name api.example.com;
    
    # 【改】SSL 证书
    ssl_certificate /etc/nginx/ssl/api.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/api.example.com.key;
    
    location / {
        # 【改】前端域名
        add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, x-request-user-id' always;
        add_header 'Access-Control-Max-Age' '3600' always;
        
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # 【改】后端地址
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Cookie $http_cookie;
        
        # ⭐ Lua 脚本:自动给 Set-Cookie 添加 SameSite=None; Secure
        header_filter_by_lua_block {
            local cookies = ngx.header["Set-Cookie"]
            if cookies then
                if type(cookies) == "table" then
                    -- 多个 Set-Cookie
                    for i, cookie in ipairs(cookies) do
                        if not string.find(cookie, "SameSite") then
                            cookies[i] = cookie .. "; SameSite=None; Secure"
                        end
                    end
                else
                    -- 单个 Set-Cookie
                    if not string.find(cookies, "SameSite") then
                        ngx.header["Set-Cookie"] = cookies .. "; SameSite=None; Secure"
                    end
                end
            end
        }
    }
}

9.9 Nginx 配置总结对照表

场景 跨域/跨站 需要 Cookie 推荐方案 关键配置
开发环境调试 跨域 CORS 头 add_header Access-Control-Allow-Origin *
开发环境调试 跨域 CORS 头 Allow-Origin 具体域 + Allow-Credentials true
生产环境 跨域 反向代理 proxy_pass + rewrite
生产环境 跨域 反向代理 proxy_pass + proxy_cookie_*
跨站 跨站 CORS 头 与跨域相同
跨站 跨站 统一域名 将前后端代理到同一域名下

9.10 常用 Nginx 指令说明

指令 说明 示例
proxy_pass 代理目标地址 proxy_pass http://backend:9000;
rewrite URL 重写 rewrite ^/api/(.*)$ /$1 break;
add_header 添加响应头 add_header 'X-Custom' 'value' always;
proxy_set_header 设置转发请求头 proxy_set_header Host $host;
proxy_pass_header 透传响应头 proxy_pass_header Set-Cookie;
proxy_cookie_path 修改 Cookie 路径 proxy_cookie_path /old /new;
proxy_cookie_domain 修改 Cookie 域名 proxy_cookie_domain .old.com .new.com;

9.11 最佳实践建议

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  开发环境                                                        │
│  ════════                                                        │
│  方案:前端 devServer 代理 或 Nginx CORS 头                       │
│  原因:方便调试,灵活切换后端                                      │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  生产环境(推荐)                                                 │
│  ════════════════                                                │
│  方案:Nginx 反向代理,前后端同域名                                │
│  原因:                                                          │
│    1. 彻底消除跨域/跨站问题                                       │
│    2. 无需配置 CORS                                              │
│    3. Cookie 自动携带(同源)                                     │
│    4. 无 SameSite 限制                                           │
│    5. 更安全(隐藏后端真实地址)                                   │
└─────────────────────────────────────────────────────────────────┘

生产环境推荐架构:

复制代码
                    Nginx (443)
                        │
         ┌──────────────┼──────────────┐
         │              │              │
         ▼              ▼              ▼
    /静态资源       /api/*         /ws/*
         │              │              │
         ▼              ▼              ▼
   前端服务器       后端服务器      WebSocket
   (3000)          (9000)         (9001)

9.12 Nginx 请求头转发机制

Nginx 默认行为

Nginx 不会自动转发所有请求头! 有些请求头会被过滤或修改。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  默认转发的请求头                                                 │
│  ════════════════                                                │
│  大部分普通请求头会自动转发,如:                                   │
│  - Content-Type                                                  │
│  - Accept                                                        │
│  - User-Agent                                                    │
│  - Authorization                                                 │
│  - 自定义头(如 x-request-user-id)                               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  默认会被修改的请求头                                              │
│  ════════════════════                                            │
│  - Host:默认改为 proxy_pass 的地址(如 127.0.0.1:9000)          │
│  - Connection:默认改为 close                                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  默认不转发的请求头(值为空的头)                                   │
│  ══════════════════════════════                                  │
│  如果请求头的值为空字符串,默认不转发                               │
└─────────────────────────────────────────────────────────────────┘
配置请求头转发
nginx 复制代码
location /api/ {
    proxy_pass http://backend:9000;
    
    # ==================== 必须手动设置的请求头 ====================
    
    # 保持原始 Host(重要!后端可能需要)
    proxy_set_header Host $host;
    # 或者保持原始请求的 Host
    # proxy_set_header Host $http_host;
    
    # 客户端真实 IP(后端获取真实 IP 用)
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
    # 原始协议(http/https)
    proxy_set_header X-Forwarded-Proto $scheme;
    
    # 原始端口
    proxy_set_header X-Forwarded-Port $server_port;
    
    # ==================== Cookie 转发 ====================
    proxy_set_header Cookie $http_cookie;
    
    # ==================== 自定义请求头转发 ====================
    # 默认会转发,但如果担心被过滤,可以显式设置
    proxy_set_header x-request-user-id $http_x_request_user_id;
    proxy_set_header Authorization $http_authorization;
    
    # ==================== 保持长连接 ====================
    proxy_set_header Connection "";
    proxy_http_version 1.1;
}
请求头变量对照表
请求头 Nginx 变量 说明
Host $host / $http_host $host 不含端口,$http_host 含端口
Cookie $http_cookie 所有 Cookie
Authorization $http_authorization 认证头
X-Request-User-Id $http_x_request_user_id 自定义头(下划线转小写)
Content-Type $content_type 内容类型
Content-Length $content_length 内容长度
任意头 Xxx-Yyy $http_xxx_yyy 通用规则:小写 + 下划线

9.13 Nginx 响应头转发机制

Nginx 默认行为

Nginx 也不会自动转发所有响应头! 某些响应头会被隐藏。

复制代码
┌─────────────────────────────────────────────────────────────────┐
│  默认转发的响应头                                                 │
│  ════════════════                                                │
│  大部分响应头会自动转发,如:                                       │
│  - Content-Type                                                  │
│  - Content-Length                                                │
│  - Cache-Control                                                 │
│  - 自定义响应头                                                   │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  默认隐藏的响应头(不转发给前端)                                   │
│  ══════════════════════════════                                  │
│  以下响应头默认被 Nginx 隐藏:                                     │
│  - Date                                                          │
│  - Server                                                        │
│  - X-Pad                                                         │
│  - X-Accel-*(Nginx 内部使用)                                    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  可能被忽略的响应头                                                │
│  ════════════════                                                │
│  - Set-Cookie:默认转发,但某些情况可能需要显式配置                  │
│  - 某些 hop-by-hop 头:Connection, Keep-Alive 等                 │
└─────────────────────────────────────────────────────────────────┘
配置响应头转发
nginx 复制代码
location /api/ {
    proxy_pass http://backend:9000;
    
    # ==================== 显式透传响应头 ====================
    
    # 透传 Set-Cookie(重要!登录相关)
    proxy_pass_header Set-Cookie;
    
    # 透传 Server 头(默认隐藏)
    proxy_pass_header Server;
    
    # 透传自定义响应头
    proxy_pass_header X-Custom-Header;
    
    # ==================== 隐藏某些响应头 ====================
    
    # 隐藏后端的 Server 头(安全考虑)
    proxy_hide_header X-Powered-By;
    proxy_hide_header Server;
    
    # ==================== 修改响应头 ====================
    
    # 添加自定义响应头
    add_header X-Proxy-By 'Nginx' always;
    
    # ⚠️ 注意:add_header 只在响应码为 2xx/3xx 时生效
    # 使用 always 参数可让所有响应码都生效
    
    # ==================== Cookie 相关 ====================
    
    # 修改 Cookie 路径
    proxy_cookie_path /internal-api /api;
    
    # 修改 Cookie 域名
    proxy_cookie_domain backend.internal example.com;
    
    # 修改 Cookie 的其他属性(需要 Nginx 1.19.3+)
    proxy_cookie_flags ~ secure httponly samesite=none;
}
响应头配置指令对照表
指令 作用 示例
proxy_pass_header 透传指定响应头 proxy_pass_header Set-Cookie;
proxy_hide_header 隐藏指定响应头 proxy_hide_header X-Powered-By;
add_header 添加响应头 add_header X-Frame-Options DENY;
proxy_cookie_path 修改 Cookie 路径 proxy_cookie_path /old /new;
proxy_cookie_domain 修改 Cookie 域名 proxy_cookie_domain .old.com .new.com;
proxy_cookie_flags 修改 Cookie 属性 proxy_cookie_flags ~ secure samesite=none;

9.14 完整 Nginx 配置示例(生产环境-可直接使用)

📁 配置文件: /etc/nginx/nginx.conf(主配置)或 /etc/nginx/conf.d/production.conf

nginx 复制代码
# ╔══════════════════════════════════════════════════════════════════════════╗
# ║  🌟 生产环境完整配置(HTTPS + 前后端分离 + Cookie + WebSocket)           ║
# ╠══════════════════════════════════════════════════════════════════════════╣
# ║  适用场景:企业级生产环境部署                                              ║
# ║  包含功能:HTTPS、负载均衡、API代理、WebSocket、文件上传、静态资源缓存      ║
# ╚══════════════════════════════════════════════════════════════════════════╝
#
# 🔧 需要修改的配置项清单(共 10 处):
# ┌─────┬─────────────────────────┬────────────────────────────────────────┐
# │ 序号 │ 配置项                   │ 说明                                   │
# ├─────┼─────────────────────────┼────────────────────────────────────────┤
# │  1  │ server_name             │ 你的域名                               │
# │  2  │ ssl_certificate         │ SSL 证书路径                           │
# │  3  │ ssl_certificate_key     │ SSL 私钥路径                           │
# │  4  │ root                    │ 前端静态文件路径                        │
# │  5  │ upstream backend        │ 后端服务器地址和端口                     │
# │  6  │ upstream websocket      │ WebSocket 服务器地址(如不需要可删除)   │
# │  7  │ rewrite                 │ API 路径重写规则                        │
# │  8  │ proxy_cookie_path       │ Cookie 路径映射                         │
# │  9  │ client_max_body_size    │ 上传文件大小限制                        │
# │ 10  │ 自定义请求头             │ 根据项目需要添加                        │
# └─────┴─────────────────────────┴────────────────────────────────────────┘

# ══════════════════════════════════════════════════════════════════════════
# 全局配置(通常不需要修改)
# ══════════════════════════════════════════════════════════════════════════
user nginx;                              # Nginx 运行用户
worker_processes auto;                   # 工作进程数,auto 自动检测 CPU 核数
error_log /var/log/nginx/error.log warn; # 错误日志路径和级别
pid /var/run/nginx.pid;                  # PID 文件路径

events {
    worker_connections 10240;            # 单个进程最大连接数
    use epoll;                           # Linux 高性能事件模型
}

http {
    include /etc/nginx/mime.types;       # 文件类型映射
    default_type application/octet-stream;
    
    # 日志格式(包含响应时间,便于排查性能问题)
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for" '
                    'rt=$request_time uct="$upstream_connect_time" '
                    'uht="$upstream_header_time" urt="$upstream_response_time"';
    
    access_log /var/log/nginx/access.log main;
    
    # 性能优化配置
    sendfile on;                         # 高效文件传输
    tcp_nopush on;                       # 减少网络报文数量
    tcp_nodelay on;                      # 实时传输
    keepalive_timeout 65;                # 长连接超时
    
    # Gzip 压缩配置(减少传输大小)
    gzip on;                             # 开启压缩
    gzip_vary on;                        # 根据请求头判断是否压缩
    gzip_min_length 1024;                # 最小压缩大小
    gzip_types text/plain text/css text/xml text/javascript 
               application/json application/javascript application/xml;
    
    # ══════════════════════════════════════════════════════════════════════
    # 【改】upstream - 后端服务器配置(负载均衡)
    # ══════════════════════════════════════════════════════════════════════
    # 作用:定义后端服务器池,支持负载均衡和故障转移
    #
    # 🔄 举一反三(根据你的后端部署情况修改):
    #
    # 单机部署(最简单):
    #    upstream backend_servers {
    #        server 127.0.0.1:8080;
    #    }
    #
    # 多实例负载均衡:
    #    upstream backend_servers {
    #        server 192.168.1.10:8080 weight=3;  # 权重 3
    #        server 192.168.1.11:8080 weight=2;  # 权重 2
    #        server 192.168.1.12:8080 weight=1;  # 权重 1
    #    }
    #
    # Docker 部署(使用容器名):
    #    upstream backend_servers {
    #        server backend-app:8080;
    #    }
    # ──────────────────────────────────────────────────────────────────────
    upstream backend_servers {
        # 【改】后端服务器地址
        server 127.0.0.1:9000 weight=1 max_fails=3 fail_timeout=30s;      # 主服务器
        server 127.0.0.1:9001 weight=1 max_fails=3 fail_timeout=30s backup; # 备用服务器
        keepalive 32;                    # 连接池大小
    }
    
    # 【改/删】WebSocket 服务器(如不需要可删除整个 upstream 块)
    upstream websocket_servers {
        server 127.0.0.1:9002;
        keepalive 32;
    }
    
    # ══════════════════════════════════════════════════════════════════════
    # HTTP → HTTPS 自动重定向
    # ══════════════════════════════════════════════════════════════════════
    server {
        listen 80;                       # 监听 HTTP 80 端口
        
        # 【改】你的域名(多个域名用空格分隔)
        server_name example.com www.example.com;
        
        # 所有 HTTP 请求重定向到 HTTPS
        return 301 https://$server_name$request_uri;
    }
    
    # ══════════════════════════════════════════════════════════════════════
    # HTTPS 主配置
    # ══════════════════════════════════════════════════════════════════════
    server {
        listen 443 ssl http2;            # HTTPS 端口,启用 HTTP/2
        
        # 【改】你的域名
        server_name example.com www.example.com;
        
        # ──────────────────────────────────────────────────────────────────
        # 【改】SSL 证书配置
        # ──────────────────────────────────────────────────────────────────
        # 证书获取方式:
        #   1. 免费证书:Let's Encrypt(推荐)
        #      certbot certonly --nginx -d example.com -d www.example.com
        #      证书路径:/etc/letsencrypt/live/example.com/
        #
        #   2. 购买证书:阿里云、腾讯云等
        #      下载后放到指定目录
        #
        # 🔄 举一反三:
        #    Let's Encrypt:
        #        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        #        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        #    自签名证书(仅测试):
        #        ssl_certificate /etc/nginx/ssl/server.crt;
        #        ssl_certificate_key /etc/nginx/ssl/server.key;
        # ──────────────────────────────────────────────────────────────────
        ssl_certificate /etc/nginx/ssl/example.com.crt;         # 【改】证书路径
        ssl_certificate_key /etc/nginx/ssl/example.com.key;     # 【改】私钥路径
        ssl_session_timeout 1d;
        ssl_session_cache shared:SSL:50m;
        ssl_protocols TLSv1.2 TLSv1.3;   # 安全协议版本
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers off;
        
        # 安全响应头(防止常见攻击)
        add_header X-Frame-Options "SAMEORIGIN" always;         # 防点击劫持
        add_header X-Content-Type-Options "nosniff" always;     # 防 MIME 嗅探
        add_header X-XSS-Protection "1; mode=block" always;     # 防 XSS
        add_header Strict-Transport-Security "max-age=31536000" always; # 强制 HTTPS
        
        # 【改】请求体大小限制(影响普通 POST 请求)
        client_max_body_size 100M;
        
        # ══════════════════════════════════════════════════════════════════
        # 前端静态资源配置
        # ══════════════════════════════════════════════════════════════════
        location / {
            # 【改】前端打包文件路径
            root /usr/share/nginx/html;
            index index.html;
            try_files $uri $uri/ /index.html;  # SPA 路由支持
            
            # 静态资源缓存配置
            location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
                expires 30d;                   # 缓存 30 天
                add_header Cache-Control "public, immutable";
            }
        }
        
        # ══════════════════════════════════════════════════════════════════
        # API 反向代理配置
        # ══════════════════════════════════════════════════════════════════
        location /api/ {
            # 【改/删】路径重写(根据后端实际路径)
            rewrite ^/api/(.*)$ /$1 break;
            
            # 转发到后端服务器池
            proxy_pass http://backend_servers;
            
            # ---------- 请求头转发 ----------
            proxy_set_header Host $host;                                    # 原始域名
            proxy_set_header X-Real-IP $remote_addr;                        # 客户端 IP
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    # IP 链路
            proxy_set_header X-Forwarded-Proto $scheme;                     # 协议
            proxy_set_header X-Forwarded-Port $server_port;                 # 端口
            
            # Cookie 和自定义头转发
            proxy_set_header Cookie $http_cookie;
            proxy_set_header Authorization $http_authorization;
            # 【改】根据你的项目添加自定义请求头
            proxy_set_header x-request-user-id $http_x_request_user_id;
            
            # ---------- 响应头处理 ----------
            proxy_pass_header Set-Cookie;            # 透传登录 Cookie
            proxy_hide_header X-Powered-By;          # 隐藏后端技术栈
            
            # 【改】Cookie 路径映射
            proxy_cookie_path / /api/;
            
            # ---------- 连接配置 ----------
            proxy_http_version 1.1;
            proxy_set_header Connection "";          # 启用长连接
            
            # 超时配置
            proxy_connect_timeout 60s;               # 连接超时
            proxy_send_timeout 60s;                  # 发送超时
            proxy_read_timeout 60s;                  # 读取超时
            
            # 缓冲配置
            proxy_buffering on;
            proxy_buffer_size 4k;
            proxy_buffers 8 4k;
        }
        
        # ══════════════════════════════════════════════════════════════════
        # 【改/删】WebSocket 代理配置(如不需要可删除整个 location 块)
        # ══════════════════════════════════════════════════════════════════
        location /ws/ {
            rewrite ^/ws/(.*)$ /$1 break;
            
            proxy_pass http://websocket_servers;
            
            # WebSocket 必需配置(不要修改)
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # WebSocket 超时(设置较长,7天)
            proxy_connect_timeout 7d;
            proxy_send_timeout 7d;
            proxy_read_timeout 7d;
        }
        
        # ══════════════════════════════════════════════════════════════════
        # 【改/删】大文件上传专用配置(如不需要可删除整个 location 块)
        # ══════════════════════════════════════════════════════════════════
        location /api/upload/ {
            rewrite ^/api/upload/(.*)$ /upload/$1 break;
            
            proxy_pass http://backend_servers;
            
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            
            # 【改】大文件上传配置
            client_max_body_size 500M;       # 允许 500M 文件
            proxy_request_buffering off;     # 流式传输,不缓存到 Nginx
            proxy_connect_timeout 300s;      # 上传可能较慢,超时设长
            proxy_send_timeout 300s;
            proxy_read_timeout 300s;
        }
        
        # ══════════════════════════════════════════════════════════════════
        # 健康检查端点(负载均衡器探活用)
        # ══════════════════════════════════════════════════════════════════
        location /health {
            access_log off;                  # 不记录日志
            return 200 "OK";
            add_header Content-Type text/plain;
        }
        
        # ==================== 禁止访问隐藏文件 ====================
        location ~ /\. {
            deny all;
            access_log off;
            log_not_found off;
        }
    }
}

9.15 请求头/响应头转发总结

请求头(前端 → Nginx → 后端)
问题 默认行为 解决方案
Host 被改为后端地址 proxy_set_header Host $host;
自定义头没转发 通常会转发 proxy_set_header X-Custom $http_x_custom;
Cookie 没转发 通常会转发 proxy_set_header Cookie $http_cookie;
获取不到客户端真实 IP proxy_set_header X-Real-IP $remote_addr;
响应头(后端 → Nginx → 前端)
问题 默认行为 解决方案
Set-Cookie 没返回 通常会返回 proxy_pass_header Set-Cookie;
想隐藏某些头 不隐藏 proxy_hide_header X-Powered-By;
想添加响应头 add_header X-Custom 'value' always;
Cookie 路径不对 不修改 proxy_cookie_path /old /new;

十、最佳实践

10.1 CORS 配置最佳实践

java 复制代码
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;
        
        String origin = request.getHeader("Origin");
        
        // 1. 设置 CORS 响应头(先设置,防止后续异常导致丢失)
        if (origin != null) {
            response.setHeader("Access-Control-Allow-Origin", origin);
        }
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Max-Age", "3600");
        
        // 2. OPTIONS 预检请求直接返回
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        // 3. 继续处理
        chain.doFilter(req, res);
    }
}

10.2 拦截器最佳实践

java 复制代码
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
            throws Exception {
        
        // 1. OPTIONS 请求放行
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }
        
        // 2. 验证身份
        String userId = request.getHeader("x-request-user-id");
        if (StringUtils.isBlank(userId)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未授权\"}");
            return false;
        }
        
        // 3. 设置用户上下文
        UserContext.setUserId(userId);
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
            Object handler, Exception ex) {
        // 清理用户上下文
        UserContext.clear();
    }
}

10.3 总结对照表

场景 解决方案
跨域 Ajax 请求 配置 CORS
跨域携带 Cookie withCredentials + allowCredentials
预检请求被拦截 拦截器/Filter 放行 OPTIONS
异常导致跨域错误 CORS Filter 最高优先级,先设置响应头
跨站 Cookie SameSite=None; Secure 或 Token 方案
生产环境 使用同一顶级域名 + 代理转发

参考资料

相关推荐
Mars.CN5 小时前
obs-websocket 5.x.x Protocol 全中文翻译
网络·websocket·网络协议
阿杰同学5 小时前
Java 网络协议面试题答案整理,最新面试题
java·开发语言·网络协议
lcyw5 小时前
A MSE+Fmp4+websocket+H265播放器
网络·websocket·网络协议
koping_wu5 小时前
【计算机网络】OSI七层模型、TCP协议、HTTP协议
tcp/ip·计算机网络·http
卓码软件测评5 小时前
Gatling WebSocket测试支持:ws、wsConnect、sendText、checkTextMessage详解
网络·websocket·网络协议·测试工具·ci/cd·自动化
HIT_Weston5 小时前
55、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 单/多线程分析(七)
前端·http·gitlab
feathered-feathered5 小时前
网络套接字——Socket网络编程(TCP编程详解)
java·网络·后端·网络协议·tcp/ip
乾元20 小时前
SDN 与 AI 协同:控制面策略自动化与策略一致性校验
运维·网络·人工智能·网络协议·华为·系统架构·ansible
橘子真甜~20 小时前
C/C++ Linux网络编程10 - http协议
linux·服务器·网络·c++·网络协议·http