返回

PHP Curl 登录302跳转后411错误原因及解决方案

php

PHP Curl 登录请求遭遇302跳转后返回411错误的原因与解决方案

当使用 PHP Curl 发送登录请求时,遇到服务器返回302状态码进行重定向,紧接着却收到411 "Length Required" 错误,这通常意味着重定向后的请求缺少必要的 Content-Length 头部。 这种情况比较特殊,因为 Curl 通常会自动处理重定向和 Content-Length 头部。 但某些服务器或应用可能对 Content-Length 的存在有严格要求,尤其是在处理 POST 请求时,即使重定向后的请求是 GET 请求也可能需要该头部。

问题分析

问题的核心在于:

  1. 首次 POST 请求成功 : PHP Curl 第一次发送 POST 请求到登录页面,服务器验证凭据后返回 302 Found 状态码,并提供 Location 头部指示重定向地址。同时,服务器设置了必要的认证 Cookie。

  2. Curl 自动跟随重定向CURLOPT_FOLLOWLOCATION 设置为 true, Curl 自动跟随重定向,发送 GET 请求到 Location 指定的地址。

  3. 重定向请求缺少 Content-Length : 尽管是 GET 请求,且通常不需要 Content-Length 头部,但目标服务器可能因为之前处理过 POST 请求或其他原因,要求后续请求也必须包含此头部。

  4. 服务器返回 411 Length Required :由于重定向后的请求缺少 Content-Length 头部,服务器拒绝处理请求,并返回 411 Length Required 状态码。

  5. Postman测试正常的原因 : Postman 可能在处理重定向时自动添加了 Content-Length 头部,或者服务器对 Postman 的请求有特殊处理。

解决方案

解决这个问题主要有以下几种思路:

1. 禁用自动重定向,手动处理重定向

这是最直接的解决方法。 通过禁用 Curl 的自动重定向功能 (CURLOPT_FOLLOWLOCATION = false) ,获取重定向地址后,再手动构造新的 Curl 请求,显式设置 Content-Length 头部。 即使 GET 请求,也可以尝试设置 Content-Length: 0

  • 原理 : 避免 Curl 自动处理重定向时可能遗漏 Content-Length 头部。
  • 操作步骤 :
    1. 设置 CURLOPT_FOLLOWLOCATIONfalse
    2. 执行第一次 Curl 请求,获取响应头中的 Location 值和 Cookie。
    3. 解析 Location 值,构建新的 Curl 请求,设置 CURLOPT_URL 为重定向地址。
    4. 手动添加 Content-Length: 0 头部。
    5. 重新发送请求,获取最终页面内容。
  • 代码示例 :
<?php
include('php/simple_html_dom.php');
$username = 'username';
$password = 'password';
$curl = curl_init();
curl_setopt_array($curl, array(
  CURLOPT_URL => 'https://somesite.com/',
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_TIMEOUT => 10,
  CURLOPT_ENCODING => '',
  CURLOPT_FOLLOWLOCATION => false, // Disable automatic redirection
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_CUSTOMREQUEST => 'GET',
  CURLOPT_HEADER => true, // Get the header
  CURLOPT_NOBODY => false,
  CURLOPT_COOKIEJAR => dirname(__FILE__) .'/cookie.txt',
  CURLOPT_COOKIEFILE => dirname(__FILE__) .'/cookie.txt',
  CURLOPT_USERAGENT=> 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36',
));

$response = curl_exec($curl);

// Check if redirection is needed
if(curl_getinfo($curl, CURLINFO_HTTP_CODE) == 302){

    preg_match('/Location:(.*?)\n/', $response, $matches);
    $redirectUrl = trim($matches[1]);
    //Resolve relative url
    if(strpos($redirectUrl,'http') !== 0){
        $baseUrl = curl_getinfo($curl,CURLINFO_EFFECTIVE_URL);
        $baseParts = parse_url($baseUrl);
        $redirectUrl = $baseParts['scheme'].'://'.$baseParts['host'].$redirectUrl;
    }

    // echo "Redirect URL: " . $redirectUrl . "<br>";

    $html = str_get_html($response);
    $htmlFormData= [];
    foreach($html->find('input') as $input) {
        $htmlFormData[$input->name]=$input->value;
    }
    foreach($html->find('#header h3') as $h3) {
        if($h3->plaintext === 'Login'){//when page title is Login
            $htmlFormData['DES_Group'] = 'LOGIN';
            $htmlFormData['DES_JSE'] = '1';
            $htmlFormData['ctl00$ctl00$plcMain$contentMain$ucLogin$ctlAccountNumber$txtText'] = $username;
            $htmlFormData['ctl00$ctl00$plcMain$contentMain$ucLogin$ctlAuthorisationCode$txtText'] = $password;

            curl_setopt_array($curl, array(
              CURLOPT_URL => 'https://somesite.com/',
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_CUSTOMREQUEST => 'POST',
              CURLOPT_HEADER => true, // Get header for next redirect check
              CURLOPT_NOBODY => false,
              CURLOPT_ENCODING => '',
              CURLOPT_MAXREDIRS => 10,
              CURLOPT_TIMEOUT => 0,
              CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
              CURLOPT_FOLLOWLOCATION => false, // Still disable auto-redirect
              CURLOPT_COOKIEJAR => dirname(__FILE__) .'/cookie.txt',
              CURLOPT_COOKIEFILE => dirname(__FILE__) .'/cookie.txt',
              CURLOPT_POSTFIELDS => http_build_query($htmlFormData),
              CURLOPT_HTTPHEADER => array('Content-Type: application/x-www-form-urlencoded')
            ));
            $response = curl_exec($curl);
            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

            if($httpCode == 302){
                preg_match('/Location:(.*?)\n/', $response, $matches);
                $redirectUrl = trim($matches[1]);
                 //Resolve relative url
                if(strpos($redirectUrl,'http') !== 0){
                    $baseUrl = curl_getinfo($curl,CURLINFO_EFFECTIVE_URL);
                    $baseParts = parse_url($baseUrl);
                    $redirectUrl = $baseParts['scheme'].'://'.$baseParts['host'].$redirectUrl;
                }
            }
            // echo "Second Request Code: ". $httpCode . "<br>";
            // echo "Effective URL: " . $redirectUrl ."<br>";
            curl_setopt_array($curl, array(
              CURLOPT_URL => $redirectUrl,
              CURLOPT_RETURNTRANSFER => true,
              CURLOPT_CUSTOMREQUEST => 'GET',
              CURLOPT_HEADER => false,
              CURLOPT_NOBODY => false,
              CURLOPT_ENCODING => '',
              CURLOPT_TIMEOUT => 0,
              CURLOPT_HTTPHEADER => array('Content-Length: 0') // Add Content-Length header for redirection request
            ));

            $response = curl_exec($curl);
            $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

            $effectiveUrl = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);

            echo "HTTP Code: ".$httpCode."<br>";
            echo "Effective URL: ".$effectiveUrl."<br>";
            echo $response;
        }else{
          echo "Logged!. cookie.txt already contains .ASPXFORMSAUTH";
        }
    }
} else {
    echo $response;
}
curl_close($curl);
?>

2. 使用 CURL 钩子函数修改请求头

Curl 提供了一些钩子函数,可以在请求发送前修改请求头。 通过 CURLOPT_HEADERFUNCTION 可以在接收到 Header 数据时进行处理。 然而,这种方法并不适合直接添加 Content-Length ,因为它主要用于读取 Header。 但是,可以尝试在第二次请求(手动构造的请求,或在 Curl FollowLocation 后的 Header 回调函数中) ,修改请求 Header 来添加 Content-Length : 0。

  • 原理