关于PHP中$_SERVER['HTTP_ORIGIN']使用curl请求是空的情况
问题描述
部署到线上后发现这么个问题:
比如项目地址:A:https://ai2hinen9t1.thexin.cn/ai/x1/index.html
B:https://ai2hinen9t1.thexin.cn/ai/x1/chat.php用户鉴权地址:C:https://ai2hinen9t1.thexin.cn/user-check/check.php
C鉴权中,也进行了域名白名单的审核,代码如下:
ini_set('date.timezone', 'Asia/Shanghai');
try {
// 定义允许的前端域名白名单(数组形式,无斜杠)
$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");
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");
} else {
throw new Exception('您的域名不在白名单中', 1002);
}
} catch (Exception $e) {
http_response_code(401);
$errcode = $e->getCode();
$errMsg = $e->getMessage();
$json = json_encode([
'code' => $errcode,
'msg' => $errmsg,
'data' => []
], JSON_UNESCAPED_UNICODE);
exit($json);
}我在浏览器中,访问A页面,A在页面加载后会自动向C发送一个请求,来验证是否有权限使用,这时候:
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$_SERVER['HTTP_ORIGIN'] 可以获取到https://ai2hinen9t1.thexin.cn 这时候,通过浏览器进行请求,功能正常。
然后当我在页面进行对话的时候,会通过B发送对话请求,具体流程如下:
B 通过发送curl -> C (鉴权)-> B -> 鉴权成功 -> 向第三方发请求
鉴权失败 -> 抛错,中止请求我将B向C发送鉴权封装如下:
<?php
class UserManager {
const URL_USER_CHECK = '...地址';
static $userKey;
static $token;
static $appId;
// 执行用户权限检测
public static function requestCheck($appId, $token, $isSaveCount = 0)
{
$url = self::URL_USER_CHECK;
$params = [
'app_id' => $appId,
'token' => $token,
// 是否保存次数
'save_count' => $isSaveCount,
];
$ch = curl_init();
$options = array(
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
// 如果是application/json 那么必须json_encode 使用表单格式
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_HTTPHEADER => [
// 'Content-Type: application/json' // 必须使用file_get_contents('php://input');获取
'Content-Type: application/x-www-form-urlencoded',
// curl向服务端发送,要想获取origin,必须手动设置
// 'Origin: https://ai2hinen9t1.thexin.cn'
]
);
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
if(curl_error($ch)) {
$errMsg = 'Error: ' . curl_error($ch);
self::throwError('抱歉,账号验证请求失败,请稍候重试。', 1003);
curl_close($ch);
return false;
}
curl_close($ch);
return json_decode($response, true);
}
public static function throwError($message = '', $errCode = 0)
{
throw new Exception($message, $errCode);
}
}我只需要在需要的地方执行即可:
$commonUserInfo = UserManager::requestCheck(
$appId = $appid,
$token = $token,
$isSaveCount = $isNeedSaveCount ? 1 : 0
);然后我却发现,A->C通过浏览器发送的请求,可以正常获取$SERVER['HTTP_ORIGIN'],而通过B->C,用curl发送的请求,C中无法获取$SERVER['HTTP_ORIGIN']!
为什么在chat.php中通过 cURL 发送请求到check-token.php时,$_SERVER['HTTP_ORIGIN']会为空?
这背后涉及到跨域资源共享(CORS)的工作原理以及服务器端请求和客户端请求的区别。
核心原因
HTTP_ORIGIN是一个由浏览器在发起跨域请求时自动添加的请求头。它用来告诉目标服务器,这个请求来自于哪个源(协议 + 域名 + 端口)。
而你在chat.php中使用 cURL 发起的请求,是一个服务器端到服务器端的请求,并非由浏览器发起。因此,cURL 不会自动添加Origin请求头,check-token.php自然也就无法从$_SERVER中获取到HTTP_ORIGIN的值。
详细解释
让我们来对比一下两种请求流程:
从
index.html直接发送请求 (成功的情况)
浏览器 (https://ai2hinen9t1.thexin.cn/ai/x1/index.html)
|
| 发起 AJAX 请求
| 请求头中自动包含: Origin: https://ai2hinen9t1.thexin.cn
v
服务器 (ai2hinen9t1.thexin.cn)
|
| 执行 check.php
| $_SERVER['HTTP_ORIGIN'] 被设置为浏览器传来的 Origin 值
| 你的代码检查该值是否在白名单中,验证通过
v
返回数据给浏览器在这个流程中,浏览器是请求的发起者,它遵循 CORS 协议,主动提供了Origin信息。
从chat.php使用 cURL 发送请求 (失败的情况)
浏览器 (https://ai2hinen9t1.thexin.cn/ai/x1/chat.php)
|
| 访问 chat.php 页面
v
服务器 (ai2hinen9t1.thexin.cn)
|
| 执行 chat.php 代码
| 代码到达 cURL 部分,准备发起内部请求
|
| cURL 发起请求到 check.php
| 这个请求是从服务器内部发出的,没有浏览器参与
| 请求头中没有 Origin 字段
v
服务器 (ai2hinen9t1.thexin.cn)
|
| 执行 check.php
| $_SERVER['HTTP_ORIGIN'] 不存在,为空字符串 ''
| 你的代码检查 '' 是否在白名单中,验证失败,抛出异常
v
返回错误信息给 chat.php 的 cURL 调用在这个流程中,chat.php是一个中间层。浏览器请求chat.php,然后chat.php作为一个 “客户端” 请求check.php。由于这个内部请求不是由浏览器直接发起的,所以 CORS 的Origin机制不适用。
解决方案
方案一:在 cURL 请求中手动添加Origin头
这是最直接的解决方法。既然 cURL 不自动加,你可以手动把它加上。
修改chat.php中的requestCheck方法,在CURLOPT_HTTPHEADER中添加Origin:
// ... 在 chat.php 的 requestCheck 方法中 ...
$options = array(
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HEADER => false,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
// 手动添加 Origin 头,值可以是你允许的任何一个白名单域名
'Origin: https://ai2hinen9t1.thexin.cn'
]
);
// ...方案二:使用 API Key 进行服务器间认证 (推荐)
这是更安全、更规范的做法。对于服务器之间的通信,不应该依赖用于浏览器的 CORS 机制,而应该使用专门的认证方式,比如 API Key。
修改check.php
try {
$allowedOrigins = [
'https://ai2hinen9t1.thexin.cn',
];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
// 定义一个服务器间通信的 API Key
$serverApiKey = 'your_strong_secret_api_key_here';
$requestApiKey = $_REQUEST['api_key'] ?? ''; // 从 GET 或 POST 参数中获取
// 判断:要么 Origin 在白名单内,要么 API Key 正确
$isAllowed = in_array($origin, $allowedOrigins) || ($requestApiKey === $serverApiKey);
if (!$isAllowed) {
throw new Exception("Unauthorized request.", 1002);
}
// ... 后续逻辑 ...
} catch (Exception $e) {
// ...
}修改chat.php的requestCheck方法: 在请求参数中带上这个 API Key。
public static function requestCheck($appId, $token, $isSaveCount = 0)
{
$url = self::URL_USER_CHECK;
$params = [
'app_id' => $appId,
'token' => $token,
'save_count' => $isSaveCount,
'api_key' => 'your_strong_secret_api_key_here' // 带上 API Key
];
// ... cURL 选项 ...
}这样,服务端与服务端的curl通信,使用api_key作为凭证进行验证即可,就避免了curl为空的问题。
HTTP_ORIGIN为空是正常现象?
HTTP_ORIGIN并不是一个标准的 CGI/HTTP 环境变量,它是由浏览器在跨域请求(如 AJAX、Fetch API)中自动添加的请求头,用于标识请求来源域。
$_SERVER中不存在HTTP_ORIGIN是完全正常的现象。
为什么 有的get请求HTTP_ORIGIN不存在?
HTTP_ORIGIN这个请求头只有在特定情况下才会被浏览器发送,它不是一个标准的、每次请求都会携带的头。具体来说:
跨域请求(CORS):当你的 JavaScript 代码(例如在
a.com的页面)尝试向另一个域名(例如b.com)发送 AJAX 请求时,浏览器会自动在请求头中添加Origin: http://a.com。服务器端的$_SERVER['HTTP_ORIGIN']变量就是从这个请求头来的。非跨域请求:当你直接在浏览器地址栏输入 URL、或者点击同域名下的链接时,请求是同源的,浏览器不会发送
Origin请求头。因此,$_SERVER中自然也就没有HTTP_ORIGIN这个元素。简单请求与预检请求(Preflight):对于一些复杂的跨域请求(例如使用
POST方法发送application/json数据),浏览器会先发送一个OPTIONS方法的 “预检请求”,这个请求一定会包含Origin头。如果服务器允许该来源的跨域请求,浏览器才会发送真正的请求。服务端到服务端的curl请求,默认不会携带origin,因此
HTTP_ORIGIN也不存在。
当浏览器发送的是一个简单的、同源的GET请求(或者从浏览器中直接访问地址),所以不会附加Origin请求头。因此,你的 PHP 脚本也就无法在$_SERVER中找到HTTP_ORIGIN。
如何正确处理HTTP_ORIGIN?
如果你是在编写处理跨域请求(CORS)的 API 接口,那么你需要考虑HTTP_ORIGIN。处理逻辑应该是这样的:
1、检查请求是否包含Origin头:
// 注意:要用 $_SERVER['HTTP_ORIGIN'] ?? '' 来避免 Notice: Undefined index 错误
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; 2、如果$origin不为空,说明这是一个跨域请求。你需要验证这个$origin是否在你允许的域名列表中。
$allowedOrigins = [
'https://your-frontend.com',
'https://www.your-frontend.com'
];
if (in_array($origin, $allowedOrigins)) {
// 允许该来源的跨域请求
header("Access-Control-Allow-Origin: $origin");
// 如果你需要支持带凭据的请求 (credentials: 'include'),需要设置为 true
header("Access-Control-Allow-Credentials: true");
}3、如果$origin为空,说明这是一个同源请求或者不是由浏览器发起的请求(例如 curl、Postman),这种情况下通常不需要做任何 CORS 处理。
不要担心$_SERVER里没有HTTP_ORIGIN。它只在跨域的 AJAX/Fetch 请求时才会出现。
当浏览器执行的是同源get请求、请求很简单的时候就会为空。
目录