options请求预检和Vue中axios封装以及withCredentialstrue
先说问题
先看一下我的本地phpstudy里面的apache的http.conf
#解开后 支持跨域读取
LoadModule headers_module modules/mod_headers.so
# 网站根目录配置
<Directory />
Options +Indexes +FollowSymLinks +ExecCGI
AllowOverride none
Order allow,deny
Allow from all
Require all granted
# CORS 配置
Header set Access-Control-Allow-Origin "*"
Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
Header set Access-Control-Allow-Credentials "true"
</Directory>再看后端接口:
<?php
header("content-type:application/json;chartset=uft-8");
$log = "{$_SERVER['REQUEST_METHOD']} " . date('Y-m-d H:i:s') . "\n";
$filename = __DIR__. '/debuglog/' . uniqid() . '_debug.log';
file_put_contents($filename, $log . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true) . "\n\n", FILE_APPEND);
exit(11);这就是一个简单的后端接口,前端发送请求->后端接收,然后在指定目录生成一个日志文件。
测试,在浏览器直接访问该接口,发现可以正常生成日志文件。
OK,到这一步目前没问题,然后,我再创建单独的html文件,使用JQuery的ajax和原生的fetch进行请求访问:
<script>
// $.ajax({
// url: 'get-image-result-1.php',
// success(res) {
// console.log(res);
// }
// })
fetch('get-image-result-1.php', {
method: 'GET', // 请求方法和 axios 一致
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer 12345567888' // 和你 axios 拦截器的 Token 一致
},
cache: 'no-cache', // 禁用缓存,避免浏览器重复请求
credentials: 'same-origin' // 和 axios 默认的 withCredentials 一致
})
.then(async response => {
// 解析响应(和 axios 自动解析 JSON 一致)
const res = await response.json();
console.log('原生 fetch 响应:', res);
})
.catch(err => {
console.error('原生 fetch 错误:', err);
});
</script>发现也能正常生成一个文件。也正常。
重点来了,下面在项目中,封装的axios(v1.13.2)请求:
/* eslint-disable */
import axios from 'axios'
import { ERR_OK, BASE_URL } from "./config";
import { showAlert } from "@/common/js/sweet-alert";
axios.defaults.baseURL = BASE_URL;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// axios向跨域请求携带cookie 必须配合服务端header
// axios.defaults.withCredentials = true;
// 请求拦截器:统一添加Token到请求头
axios.interceptors.request.use(
async (config) => { // 加 async
const token = '12345567888';
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config; // 异步函数返回的是 Promise,axios 处理更稳定
},
(error) => {
return Promise.reject(error);
}
);
/*
* params 必须是一个对象
* */
export function get(url, params) {
// 如果不是一个对象就包一下
if (!(typeof params == 'object')) {
params = { params }
}
return axios.get(url, {
params: params
}).then((res) => {
const serverData = res.data;
if (serverData.code === ERR_OK) {
return serverData.data
} else {
// 通用的错误处理
errorHandle(serverData);
// console.log(serverData.message);
throw new Error(serverData.msg || '接口请求失败');
}
}).catch((e) => {
console.error(e);
// 重新抛出错误,让上层调用者处理
throw e;
// return null;
})
}
/*
* params 必须是一个对象
* */
export function post(url, params) {
if (!(params instanceof FormData) && typeof params !== 'object') {
params = { params };
}
return axios.post(url, params).then((res) => {
const serverData = res.data;
if (serverData.code === ERR_OK) {
return serverData.data;
} else {
errorHandle(serverData);
throw new Error(serverData.msg || '接口请求失败');
}
}).catch((e) => {
console.error('post请求错误:', e);
throw e;
// return null;
});
}
// upload方法(专门处理文件上传,独立配置)
export function upload(url, formData) {
// 校验参数:确保传递的是FormData(避免传错格式)
if (!(formData instanceof FormData)) {
console.error('upload方法仅支持FormData参数!');
return Promise.reject(new Error('参数格式错误,需传递FormData'));
}
// 独立请求配置:覆盖默认头,确保文件格式正确
const requestConfig = {
headers: {
// 关键:设置为multipart/form-data(axios也会自动识别FormData并生成,这里显式设置更保险)
'Content-Type': 'multipart/form-data'
}
};
// 发送文件上传请求
return axios.post(url, formData, requestConfig).then((res) => {
const serverData = res.data;
if (serverData.code === ERR_OK) {
return serverData.data; // 返回后端的文件URL等数据
} else {
errorHandle(serverData);
console.log('文件上传失败:', serverData);
throw new Error(serverData.msg || '接口请求失败');
}
}).catch((e) => {
// return Promise.reject(e);
console.log('文件上传失败:', e);
/**
* return null 本质:成功态(resolved)会进入 await 的 “成功结果”,response 直接等于 null,用 if(!response) 判断失败即可。
* Promise.reject(null) 失败态(rejected) 会直接触发 catch(或 try/catch 的 catch 块),无法通过 response 拿到 null。
* */
return null;
});
}
axios发送get测试:
axios.get('get-image-result-1.php').then(res => {
console.log('axios 响应:', res);
}).catch(err => {
console.error('axios 错误:', err);
});这里,我将token放在了请求头里面,然后添加到拦截器中,再向后端发送请求,发现后端同时触发了2次!一下同时生成2个日志文件!但是从浏览器的控制台,只能看到发送了一次请求!**
从打印出来的日志中可以看到,两个文件的$_SERVER['REQUEST_METHOD']记录分别是一个OPTIONS和GET!
问题排查和解决
经过一下午的排查,发现问题根源在:添加Authorization头后触发了跨域预检请求(OPTIONS 请求),导致后端收到两次请求(OPTIONS + 实际 get),生成两个日志文件。
关键原因:跨域预检(CORS Preflight)
当前端请求满足以下条件时,浏览器会先发送一次 OPTIONS 预检请求,确认后端允许该跨域请求后,再发送实际的 POST 请求:
请求方法不是简单方法(GET、POST、HEAD 之外的方法,或 POST 但 Content-Type 不是
application/x-www-form-urlencoded、multipart/form-data、text/plain)。请求头包含自定义头(如
Authorization属于自定义头,不在浏览器默认允许的头列表中)。
添加Authorization头后,浏览器触发 OPTIONS 预检,后端将 OPTIONS 请求和后续的 POST 请求都当作有效请求处理,因此生成两个日志文件;注释掉Authorization头后(去掉拦截器中的header部分),没有自定义头,浏览器直接发送 POST 请求,只生成一个日志文件。
所以解决方案有两个:
(简单粗暴)让后端识别 OPTIONS 预检请求并直接响应(不执行业务逻辑、不生成日志)
因为我在apache中,对于网站根目录,配置了全局支持跨域,支持各种请求方法,所以用这种方法最直接,直接检测$_SERVER['REQUEST_METHOD'] 是不是options,是的话代表这次是预检请求,直接返回允许的跨域头,不执行后续业务代码。
对后端接口修改如下:
header("content-type:application/json;charset=utf-8");
// 处理 OPTIONS 预检请求:直接响应,不生成日志
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// 允许的跨域源(根据实际情况修改,如指定具体域名)
// 这里需要配合服务器设置,如果http.conf中设置了Allow-Origin: * 那么这里就不要重复添加了
// header("Access-Control-Allow-Origin: *");
// 允许的请求头(必须包含 Authorization,否则预检不通过)
header("Access-Control-Allow-Headers: Authorization, Content-Type");
// 允许的请求方法
header("Access-Control-Allow-Methods:GET, POST, OPTIONS");
// 允许跨域携带凭证(如果前端开启了 withCredentials 则需要)
header("Access-Control-Allow-Credentials: true");
exit; // 直接退出,不执行后续日志生成逻辑
}
// 以下是原有业务逻辑(仅处理 POST 请求)
$log = "xxx被调用 - taskId: {$taskId} - 时间: " . date('Y-m-d H:i:s') . "\n";
$filename = __DIR__. '/debuglog/' . uniqid() . '_debug.log';
file_put_contents($filename, $log . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true) . "\n\n", FILE_APPEND);
exit(11);修改之后,再使用axios请求(携带自定义的Authorization),发现后端仅为 POST 请求生成日志文件,OPTIONS 请求不会生成日志,只有一个。
关键说明
Access-Control-Allow-Headers必须包含Authorization,否则浏览器会认为后端不允许该自定义头,预检失败,不会发送实际 POST 请求。Access-Control-Allow-Origin建议指定具体域名(如http://localhost:8080),而非*,提高安全性。前端无需修改任何代码,只需后端正确响应 OPTIONS 请求即可。
添加Authorization头触发跨域预检是浏览器的默认行为,并非前端代码问题。解决的核心是让后端识别 OPTIONS 请求并直接响应,不执行业务逻辑,这样既保证了跨域请求的合法性,又不会生成多余的日志文件。
axios开启withCredentials: true,并修改服务器配置
要知道,正常服务器是不允许Allow-Headers: * 的,所以,我将服务器http.conf修复如下:
<Directory />
Options +Indexes +FollowSymLinks +ExecCGI
AllowOverride none
Order allow,deny
Allow from all
Require all granted
# CORS 配置
# Header set Access-Control-Allow-Origin "*"
# Header set Access-Control-Allow-Methods "GET, POST, OPTIONS, PUT, DELETE"
# Header set Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization"
# Header set Access-Control-Allow-Credentials "true"
</Directory>将CORS跨域的header,都注释掉了,统一让脚本进行处理。
修改后前端封装代码如下:
axios.defaults.baseURL = BASE_URL;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// 其他内容与上面的一致,仅仅将这里解开
axios.defaults.withCredentials = true;当withCredentials: true时,浏览器的跨域预检(OPTIONS 请求)会要求后端返回的Access-Control-Allow-Credentials为true,同时Access-Control-Allow-Origin不能是*(必须指定具体域名)。
开启 withCredentials: true 后,后端必须满足:
1、Access-Control-Allow-Origin:指定具体的前端域名(如http://localhost:8080),不能是 *;2、Access-Control-Allow-Credentials: true;3、Access-Control-Allow-Headers: Authorization, Content-Type(包含自定义头 Authorization)。否则会出现跨域错误,请求失败。
这不是 “解决了预检”,而是优化了跨域兼容:浏览器依然发送 OPTIONS 预检请求,但后端因为规范了处理流程,所以你看不到多余的日志文件,本质是跨域配置兼容了,而非取消了预检。
所以对后端代码修复如下:
header("content-type:application/json;charset=utf-8");
// 处理 OPTIONS 预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
// 1. 去掉 origin 末尾的斜杠,确保和前端 origin 完全一致
header("Access-Control-Allow-Origin: http://192.168.1.101:8081");
// 2. 允许的请求头:包含 Authorization(自定义头)和 Content-Type
header("Access-Control-Allow-Headers: Authorization, Content-Type");
// 3. 允许的请求方法:根据实际业务补充(如 GET、POST 等)
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
// 4. 允许携带凭证(和前端 withCredentials: true 配套)
header("Access-Control-Allow-Credentials: true");
// 5. 可选:指定预检结果缓存时间(减少 OPTIONS 请求次数)
header("Access-Control-Max-Age: 86400"); // 24小时
exit;
}
// 业务逻辑:仅处理非 OPTIONS 请求
$log = "{$_SERVER['REQUEST_METHOD']} " . date('Y-m-d H:i:s') . "\n";
$filename = __DIR__. '/debuglog/' . uniqid() . '_debug.log';
file_put_contents($filename, $log . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true) . "\n\n", FILE_APPEND);
exit(11);注意,如果上面的仅仅对options进行了处理,还没有处理普通的post\get请求,所以直接放最终版代码(服务器配置要去掉Header set Access-Control-Allow-Origin "*"):
<?php
header("content-type:application/json;charset=utf-8");
// 定义允许的前端域名白名单(数组形式,末尾无斜杠)
$allowedOrigins = [
'http://192.168.1.101:8080',
'http://localhost:8080',
'https://yourdomain.com' // 可添加更多域名
];
// 动态获取前端请求源(可能为空,如非跨域请求)
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// 判断当前请求源是否在白名单中
$isAllowed = in_array($origin, $allowedOrigins);
// 1. 处理 OPTIONS 预检请求
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
if ($isAllowed) {
header("Access-Control-Allow-Origin: {$origin}"); // 返回当前请求的域名
header("Access-Control-Allow-Headers: Authorization, Content-Type");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Credentials: true");
// Access-Control-Max-Age 让浏览器缓存预检结果,减少 OPTIONS 请求次数,提升性能
header("Access-Control-Max-Age: 86400"); // 缓存预检结果24小时
}
exit;
}
// 2. 处理实际请求(POST/GET)
if ($isAllowed) {
header("Access-Control-Allow-Origin: {$origin}"); // 返回当前请求的域名
header("Access-Control-Allow-Credentials: true");
}
// 原有业务逻辑
$log = "{$_SERVER['REQUEST_METHOD']} " . date('Y-m-d H:i:s') . "\n";
$filename = __DIR__. '/debuglog/' . uniqid() . '_debug.log';
file_put_contents($filename, $log . print_r(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS), true) . "\n\n", FILE_APPEND);
exit(11);
?>如果这时浏览器还报错:
Access to XMLHttpRequest at 'http://192.168.1.101/jimeng-ai/get-image-result-1.php' from origin 'http://192.168.1.101:8081' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'http://192.168.1.101:8081/, *', but only one is allowed.那么就是代表请求头Allow-Origin设置冲突了,要去掉http.conf中的Allow-Origin设置(参考我apache上面的设置)。删掉 Apache 配置中的Access-Control-Allow-Origin "*",避免和 PHP 代码的配置冲突,响应头只保留一个 Origin 值。
如果是nginx(参考):
location / {
add_header Access-Control-Allow-Origin http://192.168.1.101:8081;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Authorization, Content-Type';
add_header Access-Control-Allow-Credentials 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
}服务器配置修改完之后,记得要重启。
axios中withCredentials: true作用探究
withCredentials: true是 axios 中用于跨域请求时携带身份凭证(如 Cookie、HTTP 认证信息等)的配置项,理解它需要先搞清楚「跨域」和「身份凭证」的关系。
一句话总结核心作用:
让跨域请求能携带 Cookie 等身份信息,从而让后端识别用户身份。
在 Web 开发中,浏览器有一个「同源策略」安全限制:
当请求的域名、端口、协议三者中有任何一个不同时,就是「跨域请求」。
默认情况下,跨域请求不会携带 Cookie(包括登录态 Cookie、SessionID 等身份凭证),后端无法识别发起请求的用户是谁。
而withCredentials: true的作用就是打破这个默认限制,告诉浏览器:「这个跨域请求需要携带 Cookie 等身份凭证,请放行」。
举个实际例子:
假设你有两个服务:
前端:
http://localhost:8081(开发服务器)后端:
http://localhost:3000(API 服务器,两者端口不同,属于跨域)
未开启withCredentials: true时:
前端登录后,后端返回的Set-Cookie(如sessionid=xxx)会被浏览器存储,但后续前端发跨域请求时,浏览器不会把这个 Cookie 发给后端。
后端收到请求后,发现没有sessionid,会认为用户未登录,返回「未授权」错误。
开启withCredentials: true后:
跨域请求会自动携带存储的 Cookie(如sessionid=xxx),后端能通过 Cookie 识别用户身份,正常返回数据。
必须满足的配套条件:
withCredentials: true不能单独使用,否则会触发跨域错误,需要后端配合设置两个关键响应头:
Access-Control-Allow-Credentials: true告诉浏览器:「允许这个跨域请求携带凭证」。
Access-Control-Allow-Origin: 具体域名(如http://localhost:8081)必须指定明确的前端域名,不能用 *(浏览器规定:带凭证的跨域请求不允许 Origin 为 *)。
什么时候需要开启?
当你的业务满足以下场景时,必须开启:
跨域请求(前后端不同域)。
后端依赖 Cookie 识别用户身份(如传统的 Session 认证)。
如果后端用的是 Token 认证(Token 放在请求头Authorization中,而非 Cookie),且不需要携带其他 Cookie,那么可以不开启withCredentials: true(但你的情况中因为需要处理跨域预检,开启后反而兼容了配置,属于特殊场景)。
withCredentials: true的核心是解决跨域场景下的身份凭证传递问题,让后端能识别跨域请求的用户身份。它的使用必须配合后端的跨域头配置,否则会导致跨域错误。
而目前我发现的情况中,开启后跨域配置兼容,避免了重复日志,本质是后端规范化了之后正确处理了带凭证的预检请求。
目录