options请求预检和Vue中axios封装以及withCredentialstrue

9335次阅读 862人点赞 作者: WuBin 发布时间: 2025-11-11 09:29:29
扫码到手机查看

先说问题

先看一下我的本地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 请求:

  1. 请求方法不是简单方法(GET、POST、HEAD 之外的方法,或 POST 但 Content-Type 不是application/x-www-form-urlencodedmultipart/form-datatext/plain)。

  2. 请求头包含自定义头(如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-Credentialstrue,同时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的核心是解决跨域场景下的身份凭证传递问题,让后端能识别跨域请求的用户身份。它的使用必须配合后端的跨域头配置,否则会导致跨域错误。

而目前我发现的情况中,开启后跨域配置兼容,避免了重复日志,本质是后端规范化了之后正确处理了带凭证的预检请求

点赞 支持一下 觉得不错?客官您就稍微鼓励一下吧!
关键词:withCredentials,options,请求预检
推荐阅读
  • python基础-操作列表和迭代器

    python基础笔记-操作列表和迭代器的相关方法

    6708次阅读 164人点赞 发布时间: 2024-06-13 13:26:27 立即查看
  • uniapp实现被浏览器唤起的功能

    当用户打开h5链接时候,点击打开app若用户在已经安装过app的情况下直接打开app,若未安装过跳到应用市场下载安装这个功能在实现上主要分为两种场景,从普通浏览器唤醒以及从微信唤醒。

    12195次阅读 825人点赞 发布时间: 2022-12-14 16:34:53 立即查看
  • PHP

    【正则】一些常用的正则表达式总结

    在日常开发中,正则表达式是非常有用的,正则表达式在每个语言中都是可以使用的,他就跟JSON一样,是通用的。了解一些常用的正则表达式,能大大提高你的工作效率。

    15412次阅读 644人点赞 发布时间: 2021-10-09 15:58:58 立即查看
  • 【中文】免费可商用字体下载与考证

    65款免费、可商用、无任何限制中文字体打包下载,这些字体都是经过长期验证,经得住市场考验的,让您规避被无良厂商起诉的风险。

    16294次阅读 1300人点赞 发布时间: 2021-07-05 15:28:45 立即查看
  • Vue

    Vue3开发一个v-loading的自定义指令

    在vue3中实现一个自定义的指令,有助于我们简化开发,简化复用,通过一个指令的调用即可实现一些可高度复用的交互。

    18705次阅读 1501人点赞 发布时间: 2021-07-02 15:58:35 立即查看
  • JS

    关于手机上滚动穿透问题的解决

    当页面出现浮层的时候,滑动浮层的内容,正常情况下预期应该是浮层下边的内容不会滚动;然而事实并非如此。在PC上使用css即可解决,但是在手机端,情况就变的比较复杂,就需要禁止触摸事件才可以。

    16879次阅读 1365人点赞 发布时间: 2021-05-31 09:25:50 立即查看
  • Vue

    Vue+html2canvas截图空白的问题

    在使用vue做信网单页专题时,有海报生成的功能,这里推荐2个插件:一个是html2canvas,构造好DOM然后转canvas进行截图;另外使用vue-canvas-poster(这个截止到2021年3月...

    32703次阅读 2567人点赞 发布时间: 2021-03-02 09:04:51 立即查看
  • Vue

    vue-router4过度动画无效解决方案

    在初次使用vue3+vue-router4时候,先后遇到了过度动画transition进入和退出分别无效的情况,搜遍百度没没找到合适解决方法,包括vue-route4有一些API都进行了变化,以前的一些操...

    28520次阅读 2196人点赞 发布时间: 2021-02-23 13:37:20 立即查看
交流 收藏 目录