阿里云地址: https://dypns.console.aliyun.com/overview
在现代 Web 应用开发中,短信验证码是用户身份验证的重要手段。本文将详细介绍如何在 FastAdmin 框架中集成阿里云新融合认证短信产品,实现验证码发送功能。
阿里云新融合认证短信产品相比传统短信服务,具有更高的到达率、更快的发送速度以及更好的安全性,是企业级应用的理想选择。
FastAdmin 框架
PHP >= 5.5
Composer 包管理器
阿里云账号及短信服务权限
在 FastAdmin 项目根目录下执行以下命令:
composer require alibabacloud/dypnsapi-20170525 alibabacloud/darabonba-openapi alibabacloud/tea-console alibabacloud/tea-utils alibabacloud/credentials
如果 Composer 安装遇到问题,也可以手动编辑 composer.json 文件:
{
"require": {
"alibabacloud/dypnsapi-20170525": "^1.2.0",
"alibabacloud/darabonba-openapi": "^0.2.15",
"alibabacloud/tea-console": "^0.1.3",
"alibabacloud/tea-utils": "^0.2.21",
"alibabacloud/credentials": "^1.2.3"
}
}
然后执行:
composer update
创建手动安装脚本 manual_install_aliyun.php
<?php
// 手动安装阿里云 SDK 的简化版本
class ManualAliyunInstaller
{
public function install()
{
$vendorDir = __DIR__ . '/vendor';
// 创建必要的目录结构
$this->createDirectories($vendorDir);
// 下载并创建必要的类文件
$this->createCredentialClass($vendorDir);
$this->createConfigClass($vendorDir);
$this->createSmsClasses($vendorDir);
echo "手动安装完成!\n";
}
private function createDirectories($vendorDir)
{
$dirs = [
$vendorDir . '/alibabacloud/credentials/src',
$vendorDir . '/alibabacloud/darabonba-openapi/src/Models',
$vendorDir . '/alibabacloud/dypnsapi-20170525/src/Models',
$vendorDir . '/alibabacloud/tea-utils/src',
];
foreach ($dirs as $dir) {
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
}
}
private function createCredentialClass($vendorDir)
{
$content = '<?php
namespace AlibabaCloud\Credentials;
class Credential
{
private $accessKeyId;
private $accessKeySecret;
public function __construct($config = [])
{
$this->accessKeyId = $config["accessKeyId"] ?? "";
$this->accessKeySecret = $config["accessKeySecret"] ?? "";
}
public function getAccessKeyId()
{
return $this->accessKeyId;
}
public function getAccessKeySecret()
{
return $this->accessKeySecret;
}
}';
file_put_contents($vendorDir . '/alibabacloud/credentials/src/Credential.php', $content);
}
private function createConfigClass($vendorDir)
{
$content = '<?php
namespace Darabonba\OpenApi\Models;
class Config
{
public $credential;
public $endpoint;
public function __construct($config = [])
{
if (isset($config["credential"])) {
$this->credential = $config["credential"];
}
}
}';
file_put_contents($vendorDir . '/alibabacloud/darabonba-openapi/src/Models/Config.php', $content);
}
private function createSmsClasses($vendorDir)
{
// 创建简化的 SMS 相关类
// 这里只是示例,实际使用需要完整的 SDK
echo "SMS 类创建完成\n";
}
}
// 运行安装
$installer = new ManualAliyunInstaller();
$installer->install();
在 application/extra/site.php 中添加阿里云配置:
return [ // ... 其他配置 ... // 阿里云短信配置 'aliyun_access_key_id' => 'your_access_key_id', 'aliyun_access_key_secret' => 'your_access_key_secret', 'aliyun_sms_sign_name' => '速通互联验证平台', 'aliyun_sms_template_code' => '100001', ];
获取AccessKey ID 和 AccessKey Secret:
登录阿里云控制台
进入” 访问控制” > “用户管理”
创建用户并授权短信服务权限
获取 AccessKey 信息
短信签名:
在短信服务控制台申请签名
等待审核通过后使用
短信模板:
创建验证码类型模板
模板示例:您的验证码是${code},${min}分钟内有效
在 application/common/library/ 目录下创建 AliyunSms.php:
<?php
namespace app\common\library;
// 确保 autoload 文件被正确加载
if (!class_exists('AlibabaCloud\SDK\Dypnsapi\V20170525\Dypnsapi')) {
$autoloadPath = ROOT_PATH . 'vendor/autoload.php';
if (file_exists($autoloadPath)) {
require_once $autoloadPath;
} else {
throw new \Exception('Composer autoload file not found. Please run: composer install');
}
}
use AlibabaCloud\SDK\Dypnsapi\V20170525\Dypnsapi;
use AlibabaCloud\Credentials\Credential;
use AlibabaCloud\Tea\Utils\Utils;
use \Exception;
use AlibabaCloud\Tea\Exception\TeaError;
use Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Dypnsapi\V20170525\Models\SendSmsVerifyCodeRequest;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
class AliyunSms
{
private $accessKeyId;
private $accessKeySecret;
private $signName;
private $templateCode;
public function __construct()
{
// 检查必要的类是否存在
if (!class_exists('AlibabaCloud\Credentials\Credential')) {
throw new \Exception('阿里云 SDK 未正确安装,请检查 Composer 依赖');
}
// 从配置文件中读取参数
$this->accessKeyId = config('site.aliyun_access_key_id');
$this->accessKeySecret = config('site.aliyun_access_key_secret');
$this->signName = config('site.aliyun_sms_sign_name');
$this->templateCode = config('site.aliyun_sms_template_code');
// 验证配置参数
if (empty($this->accessKeyId) || empty($this->accessKeySecret)) {
throw new \Exception('阿里云 AccessKey 配置不完整');
}
}
/**
* 创建阿里云客户端
* @return Dypnsapi
*/
private function createClient()
{
try {
$credential = new Credential([
'accessKeyId' => $this->accessKeyId,
'accessKeySecret' => $this->accessKeySecret,
]);
$config = new Config([
"credential" => $credential
]);
$config->endpoint = "dypnsapi.aliyuncs.com";
return new Dypnsapi($config);
} catch (\Exception $e) {
throw new \Exception('创建阿里云客户端失败:' . $e->getMessage());
}
}
/**
* 发送短信验证码
* @param string $mobile 手机号
* @param string $code 验证码
* @param string $templateParam 模板参数(可选)
* @return array
*/
public function sendVerifyCode($mobile, $code, $templateParam = null)
{
try {
$client = $this->createClient();
// 默认模板参数
if (is_null($templateParam)) {
$templateParam = json_encode([
"code" => $code,
"min" => 10
]);
}
$sendSmsVerifyCodeRequest = new SendSmsVerifyCodeRequest([
"countryCode" => "86",
"phoneNumber" => $mobile,
"signName" => $this->signName,
"templateCode" => $this->templateCode,
"templateParam" => $templateParam,
"codeLength" => strlen($code),
"validTime" => 10, // 验证码有效期10分钟
"duplicatePolicy" => 1,
"codeType" => 1,
"returnVerifyCode" => false
]);
$runtime = new RuntimeOptions([]);
$resp = $client->sendSmsVerifyCodeWithOptions($sendSmsVerifyCodeRequest, $runtime);
// 解析响应
$responseBody = $resp->body;
if ($responseBody->code === 'OK') {
return [
'code' => 1,
'msg' => '短信发送成功',
'data' => [
'bizId' => $responseBody->bizId ?? '',
'requestId' => $responseBody->requestId ?? ''
]
];
} else {
return [
'code' => 0,
'msg' => '短信发送失败:' . ($responseBody->message ?? '未知错误'),
'data' => []
];
}
} catch (Exception $error) {
if ($error instanceof TeaError) {
return [
'code' => 0,
'msg' => '短信发送异常:' . $error->message,
'data' => []
];
} else {
return [
'code' => 0,
'msg' => '短信发送异常:' . $error->getMessage(),
'data' => []
];
}
}
}
/**
* 发送自定义短信
* @param string $mobile 手机号
* @param array $templateParams 模板参数
* @param string $templateCode 模板代码(可选)
* @return array
*/
public function sendCustomSms($mobile, $templateParams, $templateCode = null)
{
$templateCode = $templateCode ?: $this->templateCode;
$templateParam = json_encode($templateParams);
// 如果是验证码类型,提取验证码
$code = isset($templateParams['code']) ? $templateParams['code'] : rand(1000, 9999);
return $this->sendVerifyCode($mobile, $code, $templateParam);
}
}
4.1 修改 Index 控制器的 sendcode 方法
/**
* 发送短信验证码
*/
public function sendcode()
{
$mobile = $this->request->post('phone');
$rule = [
'mobile' => 'require|length:11,11|regex:/^1[3-9]\d{9}$/',
];
$data = [
'mobile' => $mobile,
];
$validate = new Validate($rule, [], ['mobile' => __('Phone')]);
$result = $validate->check($data);
if (!$result) {
$this->error($validate->getError());
}
// 判断手机号是否已经注册
$user = \app\admin\model\Admin::where("mobile", $mobile)->find();
if (!$user) {
$this->error("手机号未注册");
}
// 频率限制:同一手机号60秒内只能发送一次
$lastSendTime = cache('sms_send_time_' . $mobile);
if ($lastSendTime && (time() - $lastSendTime) < 60) {
$this->error('发送频率过快,请稍后再试');
}
// 生成随机的四位验证码
$code = rand(1000, 9999);
try {
// 使用阿里云融合短信发送验证码
$aliyunSms = new \app\common\library\AliyunSms();
$result = $aliyunSms->sendVerifyCode($mobile, $code);
if ($result['code'] === 1) {
// 短信发送成功,将验证码存储到缓存
\app\common\library\Sms::send($mobile, $code, 'usuallycode');
// 记录发送时间
cache('sms_send_time_' . $mobile, time(), 60);
$this->success(__('Send code successful'));
} else {
$this->error($result['msg']);
}
} catch (\Exception $e) {
// 异常处理
\think\Log::error('短信发送异常:' . $e->getMessage());
$this->error('短信发送失败:' . $e->getMessage());
}
}
4.2 验证码验证逻辑
在登录方法中验证短信验证码:
public function loginmobile()
{
// ... 前面的代码保持不变 ...
if ($this->request->isPost()) {
$mobile = $this->request->post('phone');
$code = $this->request->post('code');
$keeplogin = $this->request->post('keeplogin');
$token = $this->request->post('__token__');
// 验证表单数据
$rule = [
'phone' => 'require|length:11,11|regex:/^1[3-9]\d{9}$/',
'code' => 'require|length:4,4|number',
'__token__' => 'require|token',
];
$data = [
'phone' => $mobile,
'code' => $code,
'__token__' => $token,
];
$validate = new Validate($rule, [], ['phone' => __('Phone'), 'code' => __('Code')]);
$result = $validate->check($data);
if (!$result) {
$this->error($validate->getError(), '', ['token' => $this->request->token()]);
}
AdminLog::setTitle(__('MobileLogin'));
// 验证手机短信验证码
$ret = \app\common\library\Sms::check($mobile, $code, "usuallycode");
if ($ret) {
// 验证码正确,通过手机号查询用户
$user = \app\admin\model\Admin::where("mobile", $mobile)->find();
if ($user) {
$username = $user->username;
$result = $this->auth->loginByMobileCode($mobile, $keeplogin ? 86400 : 0);
if ($result === true) {
Hook::listen("admin_login_after", $this->request);
$this->success(__('Login successful'), $url, [
'url' => $url,
'id' => $this->auth->id,
'username' => $username,
'avatar' => $this->auth->avatar
]);
} else {
$msg = $this->auth->getError();
$msg = $msg ? $msg : __('Login failed');
$this->error($msg, '', ['token' => $this->request->token()]);
}
} else {
$this->error("手机号未注册", '', ['token' => $this->request->token()]);
}
} else {
$this->error("短信验证码错误", '', ['token' => $this->request->token()]);
}
}
// ... 后面的代码保持不变 ...
}
5.1 修改登录页面
在 application/admin/view/index/loginmobile.html 中添加发送验证码按钮:
<div class="form-group"> <label class="control-label">手机号</label> <input type="text" class="form-control" name="phone" placeholder="请输入手机号" /> </div> <div class="form-group"> <label class="control-label">验证码</label> <div class="input-group"> <input type="text" class="form-control" name="code" placeholder="请输入验证码" maxlength="4" /> <span class="input-group-btn"> <button class="btn btn-info btn-send-code" type="button">发送验证码</button> </span> </div> </div>
5.2 JavaScript 处理
$(document).ready(function() {
// 发送验证码
$('.btn-send-code').click(function() {
var $btn = $(this);
var phone = $('input[name="phone"]').val();
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
Toastr.error('请输入正确的手机号');
return;
}
// 防止重复点击
if ($btn.hasClass('disabled')) {
return;
}
$.ajax({
url: 'index/sendcode',
type: 'POST',
data: {
phone: phone
},
beforeSend: function() {
$btn.addClass('disabled').text('发送中...');
},
success: function(data) {
if (data.code === 1) {
Toastr.success('验证码发送成功');
// 开始倒计时
startCountdown($btn, 60);
} else {
Toastr.error(data.msg || '发送失败');
$btn.removeClass('disabled').text('发送验证码');
}
},
error: function() {
Toastr.error('网络错误,请重试');
$btn.removeClass('disabled').text('发送验证码');
}
});
});
// 倒计时函数
function startCountdown($btn, seconds) {
var timer = setInterval(function() {
seconds--;
$btn.text(seconds + 's后重发');
if (seconds <= 0) {
clearInterval(timer);
$btn.removeClass('disabled').text('发送验证码');
}
}, 1000);
}
});
6.1 添加频率限制
/**
* 检查发送频率
* @param string $mobile 手机号
* @return bool
*/
private function checkSendFrequency($mobile)
{
// 检查1分钟内发送次数
$minuteKey = 'sms_minute_' . $mobile . '_' . date('YmdHi');
$minuteCount = cache($minuteKey) ?: 0;
if ($minuteCount >= 1) {
return false;
}
// 检查1小时内发送次数
$hourKey = 'sms_hour_' . $mobile . '_' . date('YmdH');
$hourCount = cache($hourKey) ?: 0;
if ($hourCount >= 5) {
return false;
}
// 检查1天内发送次数
$dayKey = 'sms_day_' . $mobile . '_' . date('Ymd');
$dayCount = cache($dayKey) ?: 0;
if ($dayCount >= 10) {
return false;
}
return true;
}
/**
* 记录发送次数
* @param string $mobile 手机号
*/
private function recordSendCount($mobile)
{
// 记录分钟级别
$minuteKey = 'sms_minute_' . $mobile . '_' . date('YmdHi');
cache($minuteKey, (cache($minuteKey) ?: 0) + 1, 60);
// 记录小时级别
$hourKey = 'sms_hour_' . $mobile . '_' . date('YmdH');
cache($hourKey, (cache($hourKey) ?: 0) + 1, 3600);
// 记录天级别
$dayKey = 'sms_day_' . $mobile . '_' . date('Ymd');
cache($dayKey, (cache($dayKey) ?: 0) + 1, 86400);
}
6.2 添加 IP 限制
/**
* 检查 IP 发送频率
* @param string $ip IP地址
* @return bool
*/
private function checkIpFrequency($ip)
{
$ipKey = 'sms_ip_' . $ip . '_' . date('YmdH');
$ipCount = cache($ipKey) ?: 0;
// 每小时每个IP最多发送20条
return $ipCount < 20;
}
7.1 添加短信发送日志
/**
* 记录短信发送日志
* @param string $mobile 手机号
* @param string $code 验证码
* @param array $result 发送结果
*/
private function logSmsRecord($mobile, $code, $result)
{
$logData = [
'mobile' => $mobile,
'code' => $code,
'result' => $result,
'ip' => request()->ip(),
'user_agent' => request()->header('user-agent'),
'created_time' => time()
];
// 记录到日志文件
\think\Log::info('SMS发送记录', $logData);
// 也可以记录到数据库
// Db::name('sms_log')->insert($logData);
}