PAYATHON 2026

使用 Laravel 调用本地 MPESA Payment API 时出错

支付小周

结论

问题主要有两处:凭据编码方式不对,cURL 的响应处理也不完整。具体来说:

  1. base64_encode() 只接收一个字符串参数。Consumer Key 和 Consumer Secret 要先用冒号连接,再进行编码。
  2. 必须设置 CURLOPT_RETURNTRANSFER,否则 curl_exec() 不会将响应正文作为字符串返回。
  3. CURLOPT_HEADER 应设为 false,避免响应头混入 JSON,导致 json_decode() 解析失败。
  4. 不要关闭 SSL 证书验证。

Basic Authentication 凭据应这样生成:

$credentials = base64_encode($consumerKey . ':' . $consumerSecret);

使用 Laravel HTTP Client

如果当前 Laravel 版本支持 HTTP Client,可以直接使用 Http 门面。代码更短,错误也更容易处理:

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Http;

class MpesaAuthController extends Controller
{
    public function authorizeMpesa()
    {
        $consumerKey = config('services.mpesa.consumer_key');
        $consumerSecret = config('services.mpesa.consumer_secret');

        $response = Http::acceptJson()
            ->withBasicAuth($consumerKey, $consumerSecret)
            ->get(
                'https://sandbox.safaricom.co.ke/oauth/v1/generate',
                ['grant_type' => 'client_credentials']
            );

        if ($response->failed()) {
            return response()->json([
                'message' => 'Failed to obtain MPESA access token.',
                'status' => $response->status(),
                'response' => $response->json() ?? $response->body(),
            ], $response->status());
        }

        return response()->json($response->json());
    }
}

将凭据写入 .env:

MPESA_CONSUMER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MPESA_CONSUMER_SECRET=xxxxxxxxxxxx

再在 config/services.php 中添加配置:

'mpesa' => [
    'consumer_key' => env('MPESA_CONSUMER_KEY'),
    'consumer_secret' => env('MPESA_CONSUMER_SECRET'),
],

如果修改后读取的仍是旧配置,可以清除 Laravel 的配置缓存:

php artisan config:clear

请求成功后,接口通常会返回包含访问令牌和有效期信息的 JSON。具体字段以当前 MPESA 沙箱接口的实际响应为准。

使用原生 cURL

如果必须使用 cURL,可以参考下面的写法:

<?php

namespace App\Http\Controllers;

class MpesaAuthController extends Controller
{
    public function authorizeMpesa()
    {
        $url = 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials';

        $consumerKey = config('services.mpesa.consumer_key');
        $consumerSecret = config('services.mpesa.consumer_secret');

        $credentials = base64_encode(
            $consumerKey . ':' . $consumerSecret
        );

        $curl = curl_init();

        curl_setopt_array($curl, [
            CURLOPT_URL => $url,
            CURLOPT_HTTPGET => true,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER => false,
            CURLOPT_HTTPHEADER => [
                'Authorization: Basic ' . $credentials,
                'Accept: application/json',
            ],
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_TIMEOUT => 30,
        ]);

        $curlResponse = curl_exec($curl);

        if ($curlResponse === false) {
            $error = curl_error($curl);
            curl_close($curl);

            return response()->json([
                'message' => 'Unable to connect to the MPESA API.',
                'error' => $error,
            ], 502);
        }

        $statusCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        curl_close($curl);

        $data = json_decode($curlResponse, true);

        if (json_last_error() !== JSON_ERROR_NONE) {
            return response()->json([
                'message' => 'MPESA returned an invalid JSON response.',
                'status' => $statusCode,
                'response' => $curlResponse,
            ], 502);
        }

        return response()->json($data, $statusCode);
    }
}

原代码为什么失败

下面这行代码有误:

$credentials = base64_encode($CONSUMER_KEY, $CONSUMER_SECRET);

base64_encode() 的函数签名是:

base64_encode(string $string): string

它不能分别接收 Key 和 Secret。HTTP Basic Authentication 需要先将两者拼成下面的字符串:

consumer_key:consumer_secret

然后对整个字符串进行 Base64 编码。

原代码还缺少这项设置:

CURLOPT_RETURNTRANSFER => true

没有它,curl_exec() 可能会直接输出响应并返回布尔值。将布尔值传给 json_decode(),无法得到预期的解析结果。

另外,下面的设置也有问题:

CURLOPT_HEADER => true

它会让返回的字符串同时包含 HTTP 响应头和 JSON 正文,例如:

HTTP/1.1 200 OK
Content-Type: application/json

{"access_token":"...","expires_in":"..."}

这段内容并不是纯 JSON,因此 json_decode() 会解析失败。

注意事项

正式代码中不要使用:

CURLOPT_SSL_VERIFYPEER => false

关闭证书验证后,HTTPS 连接将无法正常校验服务器身份。如果本地环境出现 SSL 证书错误,应安装或配置正确的 CA 证书,不要长期禁用验证。

Consumer Key 和 Consumer Secret 不应直接写在控制器中,也不要提交到 Git 仓库。请通过 .env 和 Laravel 配置文件读取。

如果接口返回 401 Unauthorized,可以依次检查:

  • Consumer Key 和 Consumer Secret 是否来自同一个 MPESA 应用。
  • 凭据中是否有多余的空格或换行。
  • 是否采用了正确的 Basic Authentication 格式。
  • 沙箱凭据是否用于请求沙箱地址。
  • 配置缓存中是否还保存着旧凭据。

排查问题时可以记录 HTTP 状态码和响应正文,但不要将完整的 Authorization 请求头、Consumer Secret 或访问令牌写入日志。

备注:内容仅供参考。