180行代码实现全球短信发送功能(无SKD依赖)

本文提供的代码不依赖官方SDK,为方便读者理解,文章中提供的代码片段包含详细的注释。完整功能代码见文末,删减冗余注释和空行后不足180行。

应用背景

在开发一款跨境电商APP时,产品要求使用短信验证手机号的真实性,开发团队无脑选择了一直在用的腾讯云作为服务商。基于合规要求,分别申请了国内和国际短信签名及模板;这导致发送短信时需要根据国内和国际号码选择对应的签名及模板,才能确保正确下发验证信息。

经PHP开发团队评审,一致认为腾讯云SDK需要导入的代码文件过多,影响项目的可读性和可维护性。于是,决定自己封装一个简单的SDK,以便于项目成员快速上手。

腾讯云短信概述

腾讯云短信(Short Message Service,SMS)沉淀腾讯十多年短信服务技术和经验,为 QQ、微信等亿级平台和10万+客户提供快速灵活接入的高质量的国内短信与国际/港澳台短信服务。

  • 国内短信验证秒级触达,99%到达率。
  • 国际/港澳台短信覆盖全球200+国家/地区,稳定可靠。

构造腾讯云基础类

此基础类签名算法符合腾讯云APIv3规范,不仅适用于本文的短信发送类,也适用于其他腾讯云产品接口。可大大简化脱离官方SDK进行单一产品接口开发的难度。

  • 获取受限的secretIdsecretKey请参阅 腾讯云短信-预设策略
代码语言:php
复制
/**
 * 腾讯云基础类
 * @author rehiy
 * @url https://www.rehiy.com/post/527/
 */
class QCloudBasic
{
    /**
     * @var string 接口域名
     */
    protected $domain = 'tencentcloudapi.com';
/**
 * @var string 接口服务
 */
protected $service = '';

/**
 * @var string 接口版本
 */
protected $version = '';

/**
 * @var string SecretId
 */
protected $secretId = '';

/**
 * @var string SecretKey
 */
protected $secretKey = '';

/**
 * 请求接口
 * @param string $region 地域
 * @param string $action 操作名称
 * @param array $data 请求参数
 */
protected function post($region, $action, $data)
{
    $timestamp = time();
    $payload = json_encode($data, 320);
    $authorization = $this->signature($action, $payload, $timestamp);

    $headers = [
        "Authorization: {$authorization}",
        "Content-Type: application/json; charset=utf-8",
        "Host: {$this->domain}",
        "X-TC-Action: {$action}",
        "X-TC-Timestamp: {$timestamp}",
        "X-TC-Version: {$this->version}",
        "X-TC-Region: {$region}"
    ];

    return $this->httpRequest('POST', "https://{$this->domain}", $payload, $headers);
}

/**
 * 生成签名
 * @param string $action 操作名称
 * @param string $payload 请求参数
 * @param number $timestamp 时间戳
 */
protected function signature($action, $payload = '{}', $timestamp = 0)
{
    $algorithm = 'TC3-HMAC-SHA256';
    $date = gmdate('Y-m-d', $timestamp);

    $canonicalUri = '/';
    $canonicalQuery = '';
    $canonicalHeaders = implode("\n", [
        'content-type:application/json; charset=utf-8',
        'host:' . $this->domain,
        'x-tc-action:' . strtolower($action),
        ''
    ]);

    $signedHeaders = 'content-type;host;x-tc-action';
    $canonicalRequest = implode("\n", [
        'POST',
        $canonicalUri,
        $canonicalQuery,
        $canonicalHeaders,
        $signedHeaders,
        hash('SHA256', $payload)
    ]);

    $credentialScope = "{$date}/{$this->service}/tc3_request";
    $stringToSign = implode("\n", [
        $algorithm,
        $timestamp,
        $credentialScope,
        hash('SHA256', $canonicalRequest),
    ]);

    $secretDate = hash_hmac('SHA256', $date, 'TC3' . $this->secretKey, true);
    $secretService = hash_hmac('SHA256', $this->service, $secretDate, true);
    $secretSigning = hash_hmac('SHA256', 'tc3_request', $secretService, true);
    $signature = hash_hmac('SHA256', $stringToSign, $secretSigning);

    return implode(', ', [
        "{$algorithm} Credential={$this->secretId}/{$credentialScope}",
        "SignedHeaders={$signedHeaders}",
        "Signature={$signature}"
    ]);
}

/**
 * 发送HTTP请求
 * @param string $method 请求方式
 * @param string $url 服务器地址
 * @param array|string $data 数据
 * @param array $header 请求头
 */
protected function httpRequest($method, $url, $data = null, $header = null)
{
    $ch = curl_init($url);

    curl_setopt($ch, CURLOPT_TIMEOUT, 25);
    curl_setopt($ch, CURLOPT_HEADER, false);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $header ?? []);

    if ($data && $method == 'POST') {
        if (is_array($data)) {
            if (in_array('Content-Type: application/json', $header)) {
                $data = json_encode($data, 320);
            } else {
                $data = http_build_query($data);
            }
        }
        curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
    }

    list($body, $errno, $error) = [
        curl_exec($ch), curl_errno($ch), curl_error($ch), curl_close($ch)
    ];

    if ($errno != 0) {
        throw new \Exception('CURL - ' . $error, $errno);
    }

    try {
        $res = json_decode($body, true);
        return $res['Response'] ?? '';
    } catch (\Exception $e) {
        return false;
    }
}

}

构造短信发送类

基于less is more原则,该类仅封装了发送短信接口,初始化时应以数组形式传入secretIdsecretKeysdkAppIdsignNametemplateCode 等参数。

  • 获取sdkAppId请参阅 腾讯云短信-创建应用
  • 获取signNametemplateCode请参阅 腾讯云短信-使用须知
代码语言:php
复制
/**

  • 腾讯云短信类

  • @author rehiy

  • @url https://www.rehiy.com/post/527/
    /
    class QCloudSms extends QCloudBasic
    {
    /
    *

    • @var string
      */
      protected $service = 'sms';

    /**

    • @var string
      */
      protected $version = '2021-01-11';

    /**

    • @var string
      */
      protected $sdkAppId = '';

    /**

    • @var string 短信签名
      */
      protected $signName = '';

    /**

    • @var string 短信模板CODE
      */
      protected $templateCode = '';

    /**

    • 构造函数
    • @param array $config
      */
      public function __construct($config)
      {
      this->domain = this->service . '.' . $this->domain;
      foreach (config as key => $value) {
      this->key = $value;
      }
      }

    /**

    • 发送短信

    • @param array $phoneNumbers 必填项,手机号码列表

    • @param array $templateParam 假如模板中存在变量需要替换则为必填项

    • @param string $extendCode 可选项,7位上行短信扩展码

    • @param string $sessionContext 可选项,携带上下文信息,server 会原样返回
      */
      public function send(phoneNumbers, templateParam, extendCode = '', sessionContext = '')
      {
      $data = [
      'PhoneNumberSet' => $phoneNumbers,
      'SmsSdkAppId' => $this->sdkAppId,
      'SignName' => $this->signName,
      'TemplateId' => $this->templateCode,
      'TemplateParamSet' => $templateParam,
      ];

      if ($extendCode) {
      data['ExtendCode'] = extendCode;
      }
      if ($sessionContext) {
      data['SessionContext'] = sessionContext;
      }

      return this->post('ap-guangzhou', 'SendSms', data);
      }
      }

调用短信发送类

本节代码仅用于演示如何调用短信发送类发送短信,实际业务代码请根据项目需求编写。

  • 请根据前面步骤获取的参数替换伪配置变量c_china</code>和<code>c_global的值
代码语言:php
复制
/**

  • 发送短信

  • @param string $phone 手机号码

  • @param string $code 验证码
    */
    function send_sms(phone, code)
    {
    // 国内短信配置
    $c_china = [
    'secretId' => 'AK----------------------',
    'secretKey' => 'SK----------------------',
    'sdkAppId' => '1400000000',
    'signName' => '签名',
    'templateCode' => '123456',
    ];

    // 国际短信配置
    $c_global = [
    'secretId' => 'AK----------------------',
    'secretKey' => 'SK----------------------',
    'sdkAppId' => '1400000000',
    'signName' => '签名',
    'templateCode' => '654321',
    ];

    // 国内号码去除+86
    phone = preg_replace(&#39;/^\+86(\d{11})/', '$1&#39;, $phone);
    config = strpos(phone, '+') === 0 ? c_global : c_china;

    // 发送短信并返回结果
    return (new QCloudSms(config))-&gt;send([phone], [$code]);
    }

  • // 发送国内短信
    print_r(
    send_sms('+8613800000000', '123456')
    );

    // 发送国际短信
    print_r(
    send_sms('+100000000000', '123456')
    );

    完整代码

    点击这里下载