#!/usr/bin/env node const axios = require('axios'); /** * 检测 API 是否支持 CORS(跨域访问) * @param {string} url - 要检测的 API 地址 * @param {string} method - HTTP 方法,如 'GET', 'POST' * @param {Object} postData - POST 请求体(可选) * @returns {Promise} - 检测结果对象 */ async function checkCORSForAPI(url, method = 'GET', postData = null) { // 自动去除 URL 两端空格 url = url.trim(); const result = { url, method, supportsCORS: false, allowOrigin: null, allowMethods: null, allowHeaders: null, preflightSuccess: false, // OPTIONS 请求是否成功 preflightSent: false, // 是否发送了 OPTIONS 请求 actualRequestSuccess: false, error: null }; console.log(`\n🚀 正在检测 CORS: ${method} ${url}`); // ============ 1. 判断是否需要发送预检请求 ============ // 根据 CORS 规范,只有 "非简单请求" 才需要预检。 // 简单请求: (GET/HEAD/POST) + (特定的简单头) const isSimpleMethod = ['GET', 'HEAD', 'POST'].includes(method.toUpperCase()); const isSimpleHeaders = true; // 在我们的检测中,我们添加了非简单头 'Authorization',所以严格来说都不是简单请求。 // 但为了更贴近真实浏览器行为,对于 GET/HEAD,我们通常可以跳过预检。 const shouldSendPreflight = !isSimpleMethod || method.toUpperCase() === 'POST'; if (shouldSendPreflight) { console.log(' Step 1. [可选] 正在发送 OPTIONS 预检请求...'); result.preflightSent = true; try { const preflightRes = await axios({ method: 'OPTIONS', url: url, headers: { 'Origin': 'https://example.com', 'Access-Control-Request-Method': method, 'Access-Control-Request-Headers': 'Content-Type, Authorization' }, timeout: 10000 }); const preflightHeaders = preflightRes.headers; result.preflightSuccess = true; result.allowOrigin = preflightHeaders['access-control-allow-origin']; result.allowMethods = preflightHeaders['access-control-allow-methods']; result.allowHeaders = preflightHeaders['access-control-allow-headers']; console.log(' ✅ OPTIONS 预检成功,响应头:'); console.log(' Access-Control-Allow-Origin:', result.allowOrigin); console.log(' Access-Control-Allow-Methods:', result.allowMethods); console.log(' Access-Control-Allow-Headers:', result.allowHeaders); } catch (preflightError) { console.log(' ⚠️ OPTIONS 预检请求失败或未响应,但这不影响最终 CORS 判断。'); console.log(' 错误详情:', preflightError.message); // 即使预检失败,我们仍然继续发送实际请求,因为服务器可能不支持 OPTIONS 但支持 CORS。 } } else { console.log(` Step 1. [跳过] ${method} 是简单请求,浏览器通常不发送预检,我们也跳过以贴近真实场景。`); } // ============ 2. 发送实际请求(GET / POST)这才是决定性步骤 ============ console.log(`\n Step 2. 正在发送实际 ${method} 请求...`); const requestConfig = { method: method, url: url, headers: { 'Origin': 'https://example.com', // 模拟浏览器发送 Origin 头 }, timeout: 10000 }; // 如果是 POST (或 PUT/DELETE),添加数据和 Content-Type if (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT') { requestConfig.data = postData || { test: 'cors-check' }; requestConfig.headers['Content-Type'] = 'application/json'; // 添加一个自定义头,使其成为 "非简单请求",触发浏览器预检 // 但在 Node.js 中,我们直接发,看服务器是否在实际响应中返回 CORS 头 requestConfig.headers['Authorization'] = 'Bearer fake-token-for-cors-check'; } try { const actualRes = await axios(requestConfig); const actualHeaders = actualRes.headers; const actualAllowOrigin = actualHeaders['access-control-allow-origin']; result.actualRequestSuccess = true; result.allowOrigin = actualAllowOrigin; // 以实际请求的响应头为准 console.log(` ✅ ${method} 请求成功,响应状态码:`, actualRes.status); console.log(' 实际响应中的 Access-Control-Allow-Origin:', actualAllowOrigin); // ============ 3. 核心判断:基于实际请求的响应头 ============ if (actualAllowOrigin && (actualAllowOrigin === '*' || actualAllowOrigin.includes('example.com'))) { result.supportsCORS = true; console.log(' 🎉 从实际响应中检测到有效的 CORS 头,接口支持 CORS!'); } else { console.log(' ❌ 实际响应中未包含有效的 Access-Control-Allow-Origin 头,接口不支持 CORS。'); } } catch (error) { result.error = error.message; console.error(' 🛑 发送实际请求时出错:', error.message); // 关键优化:即使实际请求因业务逻辑失败(如404, 500),我们也要检查响应头! if (error.response) { console.log(' 响应状态码:', error.response.status); console.log(' 响应头:', error.response.headers); const errHeaders = error.response.headers; const errAllowOrigin = errHeaders['access-control-allow-origin']; if (errAllowOrigin && (errAllowOrigin === '*' || errAllowOrigin.includes('example.com'))) { result.supportsCORS = true; result.allowOrigin = errAllowOrigin; result.actualRequestSuccess = false; // 业务失败,但 CORS 成功 console.log(' 💡 虽然请求因业务原因失败,但服务器返回了有效的 CORS 头,浏览器中跨域访问是被允许的。'); } else { console.log(' ❌ 服务器未返回有效的 CORS 头。'); } } else if (error.request) { console.log(' 请求已发出,但未收到响应(可能是网络问题、服务器宕机或防火墙拦截)'); } else { console.log(' 请求配置错误:', error.config); } } return result; } // ======================== // 🎯 命令行参数解析 // ======================== function parseArgs() { const args = process.argv.slice(2); // 跳过 node 和脚本名 if (args.length === 0) { console.error('用法: node check-cors-for-api.js [--method METHOD] [--data DATA]'); console.error('例: node check-cors-for-api.js https://api.example.com'); console.error('例: node check-cors-for-api.js https://api.example.com --method POST'); console.error('例: node check-cors-for-api.js https://api.example.com --method POST --data \'{"key":"value"}\''); process.exit(1); } const url = args[0].trim(); // 第一个参数是 URL let method = 'GET'; let postData = null; // 查找 --method 参数 const methodIndex = args.indexOf('--method'); if (methodIndex !== -1 && args[methodIndex + 1]) { method = args[methodIndex + 1].toUpperCase(); } // 查找 --data 参数 const dataIndex = args.indexOf('--data'); if (dataIndex !== -1 && args[dataIndex + 1]) { try { postData = JSON.parse(args[dataIndex + 1]); } catch (e) { console.error('❌ POST 数据格式无效,请使用有效的 JSON 字符串。'); process.exit(1); } } return { url, method, postData }; } // ======================== // 🚀 主执行逻辑 // ======================== (async () => { const { url, method, postData } = parseArgs(); const result = await checkCORSForAPI(url, method, postData); console.log('\n' + '='.repeat(30) + ' 最终检测报告 ' + '='.repeat(30)); console.log('🌐 URL:', result.url); console.log('📬 Method:', result.method); console.log('✅ 支持 CORS:', result.supportsCORS ? '是 ✅' : '否 ❌'); console.log('HeaderCode Access-Control-Allow-Origin:', result.allowOrigin || '无'); if (result.preflightSent) { console.log('OptionsResolver 预检请求成功:', result.preflightSuccess ? '是 ✅' : '否 ❌'); } console.log('ActionCode 实际请求成功:', result.actualRequestSuccess ? '是 ✅' : '否 ❌ (可能因业务逻辑失败)'); if (result.error && !result.actualRequestSuccess) { console.log('❗ 错误信息:', result.error); } console.log('\n' + '='.repeat(30) + ' 结论 ' + '='.repeat(30)); if (result.supportsCORS) { console.log('🎉 此接口可以在浏览器中跨域访问(支持 CORS)'); if (!result.actualRequestSuccess) { console.log(' 💡 注意:虽然 CORS 配置正确,但您的请求可能因其他原因(如路径错误、参数不对)失败。'); } } else { console.log('🚫 此接口在浏览器中跨域访问会被拦截(不支持 CORS 或配置不完整)'); } })();