当前位置: 代码网 > it编程>编程语言>Java > 为什么浏览器会进行OPTIONS预检请求深入讲解

为什么浏览器会进行OPTIONS预检请求深入讲解

2026年04月15日 Java 我要评论
一、什么是跨域请求1.1 同源策略(same-origin policy)同源策略是浏览器的一种安全机制,用于限制一个源(origin)的文档或脚本如何与另一个源的资源进行交互。什么是"源&

一、什么是跨域请求

1.1 同源策略(same-origin policy)

同源策略是浏览器的一种安全机制,用于限制一个源(origin)的文档或脚本如何与另一个源的资源进行交互。

什么是"源"(origin)

源由三部分组成:协议(protocol)+ 域名(domain)+ 端口(port)

https://www.example.com:443/api/users
└─┬─┘   └──────┬──────┘ └┬┘
协议        域名        端口

同源判断规则

只有当两个 url 的协议、域名、端口完全相同时,才被认为是同源。

当前页面 url目标 url是否同源原因
https://www.example.com/page1https://www.example.com/page2✅ 同源协议、域名、端口都相同
https://www.example.com/page1http://www.example.com/page2❌ 跨域协议不同(https vs http)
https://www.example.com/page1https://api.example.com/page2❌ 跨域域名不同(www vs api)
https://www.example.com/page1https://www.example.com:8080/page2❌ 跨域端口不同(443 vs 8080)
https://www.example.com/page1https://www.example.org/page2❌ 跨域域名不同(.com vs .org)

1.2 什么是跨域请求

跨域请求是指从一个源向另一个不同源的服务器发起的 http 请求。

典型的跨域场景

场景 1:前后端分离架构

前端页面:https://www.example.com
后端 api:https://api.example.com
→ 跨域(域名不同)

场景 2:本地开发环境

前端开发服务器:http://localhost:3000
后端开发服务器:http://localhost:8080
→ 跨域(端口不同)

场景 3:cdn 资源加载

网站页面:https://www.example.com
cdn 资源:https://cdn.example.com
→ 跨域(域名不同)

场景 4:第三方 api 调用

网站页面:https://www.mysite.com
第三方 api:https://api.thirdparty.com
→ 跨域(域名完全不同)

1.3 同源策略限制的内容

浏览器的同源策略会限制以下跨域行为:

1. ajax/fetch 请求

// 从 https://www.example.com 发起请求
fetch('https://api.example.com/users')  // ❌ 被同源策略阻止
  .then(response => response.json())
  .catch(error => console.error('跨域错误:', error));

2. cookie、localstorage、indexeddb 访问

// 无法读取其他域的 cookie
document.cookie  // 只能访问当前域的 cookie

3. dom 访问

// 无法访问 iframe 中不同源页面的 dom
const iframe = document.getelementbyid('myiframe');
iframe.contentwindow.document  // ❌ 跨域访问被阻止

1.4 不受同源策略限制的内容

以下资源加载不受同源策略限制:

1. 图片、css、javascript 文件

<!-- ✅ 允许跨域加载 -->
<img src="https://cdn.example.com/image.jpg">
<link rel="stylesheet" href="https://cdn.example.com/style.css" rel="external nofollow" >
<script src="https://cdn.example.com/script.js"></script>

2. 表单提交

<!-- ✅ 允许跨域提交 -->
<form action="https://api.example.com/submit" method="post">
  <input type="text" name="username">
  <button type="submit">提交</button>
</form>

3. 页面跳转

// ✅ 允许跨域跳转
window.location.href = 'https://other-site.com';

1.5 跨域解决方案

方案 1:cors(跨域资源共享)- 推荐

服务器通过设置响应头允许跨域请求:

access-control-allow-origin: https://www.example.com
access-control-allow-methods: get, post, put, delete
access-control-allow-headers: content-type, authorization

方案 2:jsonp(仅支持 get 请求)

利用 <script> 标签不受同源策略限制的特性:

// 前端
function handleresponse(data) {
  console.log(data);
}
const script = document.createelement('script');
script.src = 'https://api.example.com/data?callback=handleresponse';
document.body.appendchild(script);

方案 3:代理服务器

通过同源的代理服务器转发请求:

浏览器 → 同源代理服务器 → 目标服务器
1. 浏览器(https://www.example.com)
   ↓ 发起请求到同源代理
2. 代理服务器(https://www.example.com/proxy)
   ↓ 转发请求到目标服务器
3. 目标服务器(https://api.thirdparty.com)
   ↓ 返回数据给代理
4. 代理服务器
   ↓ 返回数据给浏览器
5. 浏览器接收数据(同源,没有跨域问题)

实际例子(node.js express):

// 前端代码(运行在 http://localhost:3000)
fetch('/proxy/api/users')  // 请求同源的代理接口
  .then(response => response.json())
  .then(data => console.log(data));
// 后端代理服务器代码(也运行在 http://localhost:3000)
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/proxy/api/users', async (req, res) => {
  // 代理服务器去请求真正的目标服务器
  const response = await axios.get('https://api.thirdparty.com/users');
  res.json(response.data);  // 返回给前端
});
app.listen(3000);

方案 4:nginx 反向代理

浏览器访问:https://www.example.com/api/users
              ↓
nginx 接收请求(https://www.example.com)
              ↓
nginx 根据 location 规则匹配 /api/
              ↓
nginx 转发到:https://api.example.com/users
              ↓
后端服务器返回数据
              ↓
nginx 返回给浏览

实际配置例子

server {
    listen 80;
    server_name www.example.com;
    # 前端静态资源
    location / {
        root /var/www/html;
        index index.html;
    }
    # api 请求代理到后端服务器
    location /api/ {
        # 将 /api/users 转发到 https://api.example.com/users
        proxy_pass https://api.example.com/;
        # 传递原始请求信息
        proxy_set_header host $host;
        proxy_set_header x-real-ip $remote_addr;
        proxy_set_header x-forwarded-for $proxy_add_x_forwarded_for;
    }
}

1.6 为什么需要同源策略

安全保护

  1. 防止 csrf 攻击:恶意网站无法读取其他网站的 cookie
  2. 保护用户隐私:防止恶意脚本窃取用户数据
  3. 隔离不同站点:确保网站之间的数据隔离

示例:没有同源策略的危险场景

// 假设没有同源策略
// 用户访问恶意网站 evil.com
fetch('https://bank.com/api/account')  // 能读取银行账户信息
  .then(response => response.json())
  .then(data => {
    // 恶意网站窃取用户银行数据
    sendtohacker(data);
  });

二、什么是 options 预检请求

options 请求是浏览器在发送跨域请求前自动发起的一种"预检请求"(preflight request),用于检查服务器是否允许实际的跨域请求。

1.1 触发条件

浏览器会在以下情况下自动发起 options 预检请求:

简单请求 vs 非简单请求

简单请求(不会触发 options)需要同时满足:

  • 请求方法为:getheadpost
  • http 头部仅包含:
    • accept
    • accept-language
    • content-language
    • content-type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain

非简单请求(会触发 options)包括:

  • 使用 putdeletepatch 等方法
  • 发送 application/json 类型的数据
  • 自定义 http 头部(如 authorizationx-custom-header
  • 携带 cookie 或认证信息

跨域请求 vs 同源请求

  • 情况 1:同源 + 简单请求 → ❌ 不发送 options 示例:
// 页面:http://localhost:9087/index.html
// api: http://localhost:9087/api/users
fetch('http://localhost:9087/api/users', {
    method: 'get'
});

结果: 直接发送 get 请求,没有 options 原因: 同源请求不需要 cors 检查

  • 情况 2:同源 + 非简单请求 → ❌ 不发送 options 示例:
// 页面:http://localhost:9087/index.html
// api: http://localhost:9087/api/users/123
fetch('http://localhost:9087/api/users/123', {
    method: 'put',  // 非简单方法
    headers: {
        'content-type': 'application/json',  // 非简单 content-type
        'authorization': 'bearer token123'   // 自定义头部
    },
    body: json.stringify({ name: '张三' })
});

结果: 直接发送 put 请求,没有 options 原因: 同源请求不需要 cors 检查(即使是非简单请求)

  • 情况 3:跨域 + 简单请求 → ❌ 不发送 options 示例:
// 页面:http://localhost:63342/index.html
// api: http://localhost:9087/api/users
fetch('http://localhost:9087/api/users', {
    method: 'post',
    headers: {
        'content-type': 'application/x-www-form-urlencoded'
    },
    body: 'name=张三'
});

结果: 直接发送 post 请求,没有 options 原因: 虽然跨域,但是简单请求,不需要预检

  • 情况 4:跨域 + 非简单请求 → ✅ 发送 options 示例:
// 页面:http://localhost:63342/index.html
// api: http://localhost:9087/api/users/123
fetch('http://localhost:9087/api/users/123', {
    method: 'put',  // 非简单方法
    headers: {
        'content-type': 'application/json'  // 非简单 content-type
    },
    body: json.stringify({ name: '张三' })
});

结果: 先发送 options 预检,预检通过后再发送 put 请求顺序: options /api/users/123 ← 预检请求 put /api/users/123 ← 实际请求 原因: 跨域 + 非简单请求,需要预检确认服务器是否允许

二、为什么浏览器要发起 options 请求

2.1 安全机制:同源策略(same-origin policy)

浏览器的同源策略是一种安全机制,限制了不同源之间的资源访问。options 预检请求是 cors(跨域资源共享)机制的核心组成部分。

2.2 options 请求的作用

  1. 安全验证:在发送实际请求前,先询问服务器是否允许跨域访问
  2. 权限确认:检查服务器是否允许特定的 http 方法和头部
  3. 避免副作用:防止非简单请求直接修改服务器数据
  4. 性能优化:通过缓存预检结果,减少后续请求的预检次数

2.3 工作流程

客户端                                服务器
  |                                    |
  |  1. options 预检请求                 |
  |  (询问是否允许跨域)                   |
  | ---------------------------------> |
  |                                    |
  |  2. 返回 cors 响应头                 |
  |  (告知允许的方法、头部等)              |
  | <--------------------------------- |
  |                                    |
  |  3. 发送实际请求                     |
  |  (post/put/delete 等)              |
  | ---------------------------------> |
  |                                    |
  |  4. 返回业务数据                     |
  | <--------------------------------- |

三、options 请求示例

3.1 预检请求示例

options /api/users/123 http/1.1                     # options 方法:预检请求;/api/users/123:请求路径(包含用户id 123);http/1.1:使用的 http 协议版本
accept: */*                                         # 客户端接受任何类型的响应内容
accept-encoding: gzip, deflate, br, zstd            # 客户端支持的压缩算法
accept-language: zh-cn,zh;q=0.9                     # 客户端首选中文语言
access-control-request-headers: content-type        #【预检关键】实际请求将携带的自定义请求头(content-type)
access-control-request-method: put                  #【预检关键】实际请求将使用的 http 方法(put)
connection: keep-alive                              # 保持 tcp 连接以便复用
host: localhost:9087                                # 目标服务器地址和端口
origin: http://localhost:63342                      #【预检关键】发起请求的源地址(前端页面地址)
referer: http://localhost:63342/                    # 请求的来源页面 url
sec-fetch-dest: empty                               # 请求的目标类型(empty 表示 cors 预检)
sec-fetch-mode: cors                                #【预检关键】请求模式为 cors 跨域请求
sec-fetch-site: same-site                           # 请求的站点关系(same-site 表示同站但不同端口)
user-agent: mozilla/5.0 (macintosh; intel mac os x 10_15_7) applewebkit/537.36 (khtml, like gecko) chrome/144.0.0.0 safari/537.36  # 浏览器标识信息

3.2 服务器响应示例

http/1.1 200                                         # http 
vary: origin                                         # 告诉缓存服务器:响应内容会根据请求的 origin 头而变化
vary: access-control-request-method                  # 告诉缓存服务器:响应内容会根据请求的方法而变化
vary: access-control-request-headers                 # 告诉缓存服务器:响应内容会根据请求的头部而变化
access-control-allow-origin: http://localhost:63342  # 【核心】允许的源地址,必须与请求的 origin 完全匹配
access-control-allow-methods: get,post,put,delete,options # 【核心】允许的 http 方法列表,告诉浏览器这些方法可以跨域使用
access-control-allow-headers: content-type           # 【核心】允许的请求头,响应预检请求中的 access-control-request-headers
access-control-expose-headers: content-disposition, x-file-size, x-download-token, x-total-count, x-page-size, x-current-page, x-total-pages, x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset
                                                    # 暴露给前端 javascript 的自定义响应头列表
                                                    # 前端可通过 response.headers.get('x-total-count') 等方式读取这些头
access-control-max-age: 0                           # 预检结果的缓存时间(秒),0 表示不缓存,每次都需要发送预检请求
content-length: 0                                   # 响应体长度为 0(预检请求无响应体)
date: mon, 09 feb 2026 02:09:02 gmt                 # 服务器响应时间
keep-alive: timeout=60                              # tcp 连接保持时间为 60 秒
connection: keep-alive                              # 保持 tcp 连接,避免频繁建立连接

四、gateway 层面的处理方案

4.1 核心配置要点

1. 允许 options 方法

确保 gateway 不会拦截或拒绝 options 请求。

2. 返回正确的 cors 响应头

必须包含以下关键响应头:

响应头说明示例值
access-control-allow-origin允许的源https://www.example.com*
access-control-allow-methods允许的 http 方法get, post, put, delete, options
access-control-allow-headers允许的请求头content-type, authorization,或*
access-control-max-age预检结果缓存时间(秒)86400(24小时)
access-control-allow-credentials是否允许携带凭证true

access-control-allow-credentials 是 cors 中用于控制是否允许跨域请求携带凭证(cookie、http 认证信息等)的响应头,一般情况下会设置成*。

3. 快速响应 options 请求

options 请求不需要转发到后端服务,应在 gateway 层直接返回 200 响应。

4.2 spring cloud gateway 配置方案

方案一:全局 cors 配置(推荐)

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.web.cors.corsconfiguration;
import org.springframework.web.cors.reactive.corswebfilter;
import org.springframework.web.cors.reactive.urlbasedcorsconfigurationsource;
/**
 * 全局 cors 跨域配置
 * 用于处理浏览器的 options 预检请求
 */
@configuration
public class corsconfig {
    @bean
    public corswebfilter corswebfilter() {
        corsconfiguration config = new corsconfiguration();
        // 允许的源(生产环境应指定具体域名)
        config.addallowedoriginpattern("*");
        // 允许的 http 方法
        config.addallowedmethod("get");
        config.addallowedmethod("post");
        config.addallowedmethod("put");
        config.addallowedmethod("delete");
        config.addallowedmethod("options");
        // 允许的请求头
        config.addallowedheader("*");
        // 是否允许携带凭证(cookie)
        config.setallowcredentials(true);
        // 预检请求的缓存时间(秒)
        config.setmaxage(86400l);
        // 暴露给前端的响应头
        config.addexposedheader("*");
        urlbasedcorsconfigurationsource source = new urlbasedcorsconfigurationsource();
        source.registercorsconfiguration("/**", config);
        return new corswebfilter(source);
    }
}

方案二:application.yml 配置

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            # 允许的源
            allowed-origin-patterns: "*"
            # 允许的方法
            allowed-methods:
              - get
              - post
              - put
              - delete
              - options
            # 允许的请求头
            allowed-headers: "*"
            # 是否允许携带凭证
            allow-credentials: true
            # 预检请求缓存时间(秒)
            max-age: 86400

方案三:自定义 globalfilter(高级场景)

import org.springframework.cloud.gateway.filter.gatewayfilterchain;
import org.springframework.cloud.gateway.filter.globalfilter;
import org.springframework.core.ordered;
import org.springframework.http.httpheaders;
import org.springframework.http.httpmethod;
import org.springframework.http.httpstatus;
import org.springframework.http.server.reactive.serverhttprequest;
import org.springframework.http.server.reactive.serverhttpresponse;
import org.springframework.stereotype.component;
import org.springframework.web.server.serverwebexchange;
import reactor.core.publisher.mono;
/**
 * 全局 cors 过滤器
 * 优先级最高,确保 options 请求被正确处理
 */
@component
public class corsglobalfilter implements globalfilter, ordered {
    @override
    public mono<void> filter(serverwebexchange exchange, gatewayfilterchain chain) {
        serverhttprequest request = exchange.getrequest();
        serverhttpresponse response = exchange.getresponse();
        httpheaders headers = response.getheaders();
        // 获取请求源
        string origin = request.getheaders().getorigin();
        // 设置 cors 响应头
        if (origin != null) {
            headers.add(httpheaders.access_control_allow_origin, origin);
        }
        headers.add(httpheaders.access_control_allow_methods, "get, post, put, delete, options");
        headers.add(httpheaders.access_control_allow_headers, "*");
        headers.add(httpheaders.access_control_allow_credentials, "true");
        headers.add(httpheaders.access_control_max_age, "86400");
        // 如果是 options 请求,直接返回 200
        if (request.getmethod() == httpmethod.options) {
            response.setstatuscode(httpstatus.ok);
            return mono.empty();
        }
        // 继续执行后续过滤器
        return chain.filter(exchange);
    }
    @override
    public int getorder() {
        // 设置最高优先级
        return ordered.highest_precedence;
    }
}

4.3 nginx gateway 配置方案

server {
    listen 80;
    server_name api.example.com;
    # 处理 options 预检请求
    if ($request_method = 'options') {
        add_header 'access-control-allow-origin' '$http_origin' always;
        add_header 'access-control-allow-methods' 'get, post, put, delete, options' always;
        add_header 'access-control-allow-headers' 'content-type, authorization, x-requested-with' always;
        add_header 'access-control-allow-credentials' 'true' always;
        add_header 'access-control-max-age' 86400 always;
        add_header 'content-length' 0;
        add_header 'content-type' 'text/plain charset=utf-8';
        return 204;
    }
    # 为所有响应添加 cors 头
    add_header 'access-control-allow-origin' '$http_origin' always;
    add_header 'access-control-allow-credentials' 'true' always;
    location /api/ {
        proxy_pass http://backend-service;
        proxy_set_header host $host;
        proxy_set_header x-real-ip $remote_addr;
    }
}

4.4 配置建议

生产环境安全配置

// ❌ 不推荐:允许所有源
config.addallowedorigin("*");
// ✅ 推荐:指定具体域名
config.addallowedorigin("https://www.example.com");
config.addallowedorigin("https://admin.example.com");
// ✅ 推荐:使用 pattern 支持多个子域名
config.addallowedoriginpattern("https://*.example.com");

性能优化配置

// 设置较长的缓存时间,减少预检请求频率
config.setmaxage(86400l); // 24 小时
// 仅暴露必要的响应头
config.addexposedheader("content-disposition");
config.addexposedheader("x-total-count");

五、浏览器这么做的意义

5.1 安全保障

  1. 防止 csrf 攻击:通过预检机制,确保只有授权的源才能发起跨域请求
  2. 保护用户数据:避免恶意网站未经授权访问用户的敏感数据
  3. 服务器主动控制:让服务器决定哪些跨域请求是安全的

5.2 兼容性保障

  1. 向后兼容:保护不支持 cors 的旧服务器不被意外访问
  2. 渐进增强:允许现代 web 应用安全地进行跨域通信

5.3 性能优化

  1. 缓存机制:通过 access-control-max-age 缓存预检结果
  2. 减少请求:缓存期内的相同请求不再发起预检
  3. 快速失败:在预检阶段就能发现跨域问题,避免浪费资源

六、常见问题与解决方案

6.1 问题:每次请求都发起 options

原因

  • 未设置 access-control-max-age 响应头
  • 缓存时间设置过短
  • 请求头或方法发生变化

解决方案

// 设置较长的缓存时间
config.setmaxage(86400l); // 24 小时

6.2 问题:options 请求返回 403 或 404

原因

  • gateway 未正确处理 options 请求
  • 路由配置不匹配 options 方法

解决方案

  • 确保 cors 过滤器优先级最高
  • 在 gateway 层直接返回 200,不转发到后端

6.3 问题:携带 cookie 的请求失败

原因

  • 未设置 access-control-allow-credentials: true
  • 使用了 access-control-allow-origin: *(与 credentials 冲突)

解决方案

config.setallowcredentials(true);
config.addallowedoriginpattern("https://www.example.com"); // 不能用 *

6.4 问题:设置 cookie 的接口跨域失败

现象

  • 前端调用登录接口,使用了 withcredentials: true
  • 后端设置了 config.setallowcredentials(false) 或未设置
  • 浏览器报错:cors policy blocked

总结

  • withcredentials: trueaccess-control-allow-credentials: true 必须同时存在
  • 这个配置不仅影响携带 cookie,也影响接收 cookie(set-cookie)
  • 设置 cookie 的登录接口也需要这个配置

6.5 问题:自定义请求头被拒绝

原因

  • 未在 access-control-allow-headers 中声明

解决方案

config.addallowedheader("authorization");
config.addallowedheader("x-custom-header");
// 或者允许所有头部(开发环境)
config.addallowedheader("*");

七、最佳实践总结

7.1 开发环境配置

// 宽松配置,方便开发调试
config.addallowedoriginpattern("*");
config.addallowedmethod("*");
config.addallowedheader("*");
config.setallowcredentials(true);
config.setmaxage(3600l);

7.2 生产环境配置

// 严格配置,确保安全
config.addallowedoriginpattern("https://*.example.com");
config.addallowedmethod("get");
config.addallowedmethod("post");
config.addallowedmethod("put");
config.addallowedmethod("delete");
config.addallowedheader("content-type");
config.addallowedheader("authorization");
config.setallowcredentials(true);
config.setmaxage(86400l);

7.3 监控与日志

@slf4j
@component
public class corsloggingfilter implements globalfilter, ordered {
    @override
    public mono<void> filter(serverwebexchange exchange, gatewayfilterchain chain) {
        serverhttprequest request = exchange.getrequest();
        if (request.getmethod() == httpmethod.options) {
            log.info("options 预检请求: {} from {}", 
                request.geturi(), 
                request.getheaders().getorigin());
        }
        return chain.filter(exchange);
    }
    @override
    public int getorder() {
        return ordered.highest_precedence + 1;
    }
}

八、总结

options 预检请求是浏览器的安全机制,虽然会增加一次额外的请求,但通过合理配置:

  1. 在 gateway 层统一处理 cors,避免每个微服务重复配置
  2. 设置合理的缓存时间,减少预检请求频率
  3. 生产环境严格控制允许的源,确保安全性
  4. 开发环境宽松配置,提高开发效率

这样既能保证安全性,又能优化性能,是现代 web 应用的标准做法。

九、参考资料

到此这篇关于为什么浏览器会进行options预检请求的文章就介绍到这了,更多相关浏览器options预检请求内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com