跨域(CORS)与 Cookie/Session 问题全面总结
目录
- 一、同源策略
- 二、跨域请求(CORS)
- 三、预检请求(OPTIONS)
- [四、Spring Boot 中的 CORS 配置](#四、Spring Boot 中的 CORS 配置)
- [五、Cookie 与 Session](#五、Cookie 与 Session)
- [六、跨域携带 Cookie](#六、跨域携带 Cookie)
- 七、常见问题排查
- [八、扩展:跨站 Cookie(SameSite)](#八、扩展:跨站 Cookie(SameSite))
- [九、Nginx 解决跨域与跨站问题](#九、Nginx 解决跨域与跨站问题)
- 十、最佳实践
一、同源策略
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 预检请求
简单请求(不触发预检)
满足以下所有条件:
- 方法:
GET、HEAD、POST - 请求头:只包含以下头
AcceptAccept-LanguageContent-LanguageContent-Type(仅限text/plain、multipart/form-data、application/x-www-form-urlencoded)
- 没有自定义请求头
预检请求(需要 OPTIONS)
不满足简单请求条件的请求,比如:
- 使用
PUT、DELETE方法 - 使用
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
5.1 Cookie 的作用域
Cookie 由 Domain 和 Path 决定作用域,不区分端口!
localhost:8080 设置的 Cookie
↓
localhost:8081 也能访问(同一 Domain)
5.2 Cookie 属性
| 属性 | 说明 | 示例 |
|---|---|---|
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 → 服务器识别用户
六、跨域携带 Cookie
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 排查步骤
- 打开浏览器 F12 → Network
- 查看是否有 OPTIONS 请求
- 检查 OPTIONS 响应状态码(应该是 200/204)
- 检查响应头是否包含
Access-Control-Allow-* - 后端添加日志,确认请求是否到达拦截器/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
8.7 解决跨站 Cookie 问题
⚠️ 重要前提:跨站一定跨域,所以 CORS 的
credentials配置也是必须的!跨站携带 Cookie 需要同时配置:
- CORS:
withCredentials: true+Access-Control-Allow-Credentials: true- Cookie:
SameSite=None; Secure只配置其中一个都不行!
Cookie 的两个方向都会受限
"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
});
受到的限制:
- CORS 限制 :如果
withCredentials: false(默认),浏览器不会在跨域请求中携带 Cookie - 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
受到的限制:
- CORS 限制 :如果没有
Access-Control-Allow-Credentials: true,浏览器会忽略Set-Cookie响应头 - 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!
方案二:使用 Token 替代 Cookie
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));
9.8 跨站 + Cookie 完整方案(OpenResty/Lua)
📌 仅当无法统一域名时使用此方案
📁 配置文件: /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 方案 |
| 生产环境 | 使用同一顶级域名 + 代理转发 |