Thinkphp 对接阿里云短信 生成二维码
记录一下.
对接阿里短信
参考与魔改的官方dome.
定义一个抽象类
文件路径app/lib/sms/SmsAbstract.php
<?php
namespace app\lib\sms;
abstract class SmsAbstract
{
abstract public function sendVerifyCode($code, $phone);
}
定义阿里云驱动类(主要)
文件路径app/lib/sms/driver/Aliyun.php
这里继承上面的抽象类,此时必须实现sendVerifyCode方法
<?php
namespace app\lib\sms\driver;
use app\lib\sms\SmsAbstract;
use Exception;
use stdClass;
use think\facade\Env;
class Aliyun extends SmsAbstract
{
public function sendVerifyCode($code, $phone)
{
$params = [];
// 必填:是否启用https
$security = false;
// 必填
$accessKeyId = Env::get('SMS_ALIYUN_ACCESSKEY_ID');
$accessKeySecret = Env::get('SMS_ALIYUN_ACCESSKEY_SECRET');
// 必填: 短信接收号码
$params["PhoneNumbers"] = $phone;
// 必填: 短信签名
$params["SignName"] = Env::get('SMS_ALIYUN_SIGNNAME');
// 必填: 短信模板Code
$params["TemplateCode"] = Env::get('SMS_ALIYUN_TEMPLATECODE');
// 可选: 设置模板参数, 假如模板中存在变量需要替换则为必填项
$params['TemplateParam'] = [
"code" => $code
];
// *** 需用户填写部分结束, 以下代码若无必要无需更改 ***
if (!empty($params["TemplateParam"]) && is_array($params["TemplateParam"])) {
$params["TemplateParam"] = json_encode($params["TemplateParam"], JSON_UNESCAPED_UNICODE);
}
$params = array_merge($params, ["RegionId" => "cn-hangzhou", "Action" => "SendSms", "Version" => "2017-05-25"]);
// 此处可能会抛出异常,注意catch
return $this->request($accessKeyId, $accessKeySecret, "dysmsapi.aliyuncs.com", $params, $security);
}
/**
* 生成签名并发起请求
*
* @param $accessKeyId string AccessKeyId (https://ak-console.aliyun.com/)
* @param $accessKeySecret string AccessKeySecret
* @param $domain string API接口所在域名
* @param $params array API具体参数
* @param $security boolean 使用https
* @param string $method boolean 使用GET或POST方法请求,VPC仅支持POST
* @return bool|stdClass 返回API接口调用结果,当发生错误时返回false
*/
public function request(string $accessKeyId, string $accessKeySecret, string $domain, array $params, $security = false, $method = 'POST')
{
$apiParams = array_merge([
"SignatureMethod" => "HMAC-SHA1",
"SignatureNonce" => uniqid(mt_rand(0, 0xffff), true),
"SignatureVersion" => "1.0",
"AccessKeyId" => $accessKeyId,
"Timestamp" => gmdate("Y-m-d\TH:i:s\Z"),
"Format" => "JSON",
], $params);
ksort($apiParams);
$sortedQueryStringTmp = "";
foreach ($apiParams as $key => $value) {
$sortedQueryStringTmp .= "&" . $this->encode($key) . "=" . $this->encode($value);
}
$stringToSign = "${method}&%2F&" . $this->encode(substr($sortedQueryStringTmp, 1));
$sign = base64_encode(hash_hmac("sha1", $stringToSign, $accessKeySecret . "&", true));
$signature = $this->encode($sign);
$url = ($security ? 'https' : 'http') . "://{$domain}/";
try {
$content = $this->fetchContent($url, $method, "Signature={$signature}{$sortedQueryStringTmp}");
return json_decode($content);
} catch (Exception $e) {
return false;
}
}
private function encode($str)
{
$res = urlencode($str);
$res = preg_replace("/\+/", "%20", $res);
$res = preg_replace("/\*/", "%2A", $res);
$res = preg_replace("/%7E/", "~", $res);
return $res;
}
private function fetchContent($url, $method, $body)
{
$ch = curl_init();
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, 1);//post提交方式
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} else {
$url .= '?' . $body;
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"x-sdk-client" => "php/2.0.0"
));
if (substr($url, 0, 5) == 'https') {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
}
$rtn = curl_exec($ch);
if ($rtn === false) {
// 大多由设置等原因引起,一般无法保障后续逻辑正常执行,
// 所以这里触发的是E_USER_ERROR,会终止脚本执行,无法被try...catch捕获,需要用户排查环境、网络等故障
trigger_error("[CURL_" . curl_errno($ch) . "]: " . curl_error($ch), E_USER_ERROR);
}
curl_close($ch);
return $rtn;
}
}
定义工厂模式类
文件路径app/lib/Sms.php
<?php
namespace app\lib;
use app\lib\sms\SmsAbstract;
use think\Container;
use think\facade\Env;
class Sms
{
public static function sendCode($code, $phone, $driver = '')
{
//获取驱动名字.此处.env里面定义的是ALIYUN.
$driver = $driver ?: Env::get('SMS_DRIVER');
//此处拼接完整的驱动类名,以后扩展别的短信平台,只需要再写一个驱动类,然后修改一下.env里面定义的驱动,或者直接使用这个方法的时候通过$driver变量指定
$class = '\\app\\lib\\sms\\driver\\' . ucfirst(strtolower($driver));
//此处把类放入使用Thinkphp的容器,相当于单例模式,没有的话会new一个然后保存,有的话直接返回出来.
$obj = Container::getInstance()->make($class);
//这里判断是否是抽象类的子类
if ($obj instanceof SmsAbstract) {
//抽象类的子类必须实现sendVerifyCode方法,此处可放心调用
return $obj->sendVerifyCode($code, $phone);
}
return false;
}
public static function getCode($length = 6): string
{
$code = '';
$chars = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'];
while (strlen($code) < $length) {
shuffle($chars);
$code .= current($chars);
}
return $code;
}
}
使用方法
<?php
namespace app\controller;
use app\lib\Sms;
use think\facade\Request;
use think\facade\Cache;
class Api
{
public function send_sms()
{
$phone = Request::post('phone');
if (!preg_match("/^1[3456789]\d{9}$/", $phone)) {
return $this->error('非法手机号');
}
if (!empty(Cache::get('login' . $phone))) {
return $this->error('请勿频繁请求');
}
//生成验证码
$code = Sms::getCode();
//发送短信
Sms::sendCode($code, $phone);
//验证码存入缓存10分钟
Cache::set($phone, $code, 600);
//缓存60秒,防止频繁请求API发送短信
Cache::set('login' . $phone, 'send sms', 60);
return $this->success('验证码发送成功');
}
}
Composer 加载endroid/qr-code 生成二维码
毕竟Composer更优雅一些(相对于单文件生成直接exit结束后续操作)
很简单的东西,但是百度了一圈,好多过时错了,这里记一下
composer require endroid/qr-code
Builder是这个类Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Builder\Builder;
//获取数据,这里如果是网址的话,get请求不能直接传,所以解码一下
$data = Request::get('data', '', 'urldecode');
if (empty($data)) {
return Json::error('缺少必要参数{data}');
}
//此处返回的是data:image/png;base64,iVBORw0KGgoAAA...这种格式的字符串,前端src直接引用也能显示图片,并不是直接的文件
//在此链式操作中最后一步是获取生成的数据,有4个方法getDataUri(),getString()直接成文件流设置一下header为图片就能直接返回图片资源,saveToFile()顾名思义应该是保存生成的二维码为图片,getMimeType()获取生成图片的MimeType比如'"image/png"'
$src = Builder::create()->size(200)->data($data)->build()->getDataUri();
return Json::success('', ['src' => $src]);
详细别的操作可访问https://github.com/endroid/qr-code