[fastadmin] 第四十五篇 FastAdmin 集成阿里云新融合认证短信产品发送验证码

FastAdmin 集成阿里云新融合认证短信产品发送验证码

阿里云地址: dypns.console.aliyun.com/smsConnec...

前言

在现代 Web 应用开发中,短信验证码是用户身份验证的重要手段。本文将详细介绍如何在 FastAdmin 框架中集成阿里云新融合认证短信产品,实现验证码发送功能。

阿里云新融合认证短信产品相比传统短信服务,具有更高的到达率、更快的发送速度以及更好的安全性,是企业级应用的理想选择。

环境要求

  • FastAdmin 框架
  • PHP >= 5.5
  • Composer 包管理器
  • 阿里云账号及短信服务权限

第一步:安装阿里云 SDK 依赖

1.1 使用 Composer 安装

在 FastAdmin 项目根目录下执行以下命令:

composer require alibabacloud/dypnsapi-20170525 alibabacloud/darabonba-openapi alibabacloud/tea-console alibabacloud/tea-utils alibabacloud/credentials

1.2 解决权限问题

如果遇到权限问题(如 Permission denied),需要以管理员身份运行命令行:

Windows 系统:

  1. Win + R,输入 cmd
  2. Ctrl + Shift + Enter 以管理员身份运行
  3. 切换到项目目录后重新执行安装命令

1.3 手动配置方式

如果 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

第二步:配置阿里云参数

2.1 修改配置文件

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',
];

2.2 获取阿里云配置信息

  1. AccessKey ID 和 AccessKey Secret

    • 登录阿里云控制台
    • 进入”访问控制” > “用户管理”
    • 创建用户并授权短信服务权限
    • 获取 AccessKey 信息
  2. 短信签名

    • 在短信服务控制台申请签名
    • 等待审核通过后使用
  3. 短信模板

    • 创建验证码类型模板
    • 模板示例:您的验证码是${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);
}

使用示例

基本使用

use app\common\library\AliyunSms;

$aliyunSms = new AliyunSms();
$result = $aliyunSms->sendVerifyCode('13800138000', '1234');

if ($result['code'] === 1) {
    echo '发送成功';
} else {
    echo '发送失败:' . $result['msg'];
}

自定义模板参数

$templateParams = [
    'code' => '1234',
    'min' => 5,
    'product' => '您的应用名称'
];

$result = $aliyunSms->sendCustomSms('13800138000', $templateParams);

常见问题及解决方案

1. Composer 权限问题

问题Permission denied 错误
解决:以管理员身份运行命令行

2. 类找不到错误

问题Class not found 错误
解决:检查 autoload 文件是否正确加载

3. 短信发送失败

问题:返回错误码
解决

  • 检查 AccessKey 配置
  • 确认短信签名和模板已审核通过
  • 查看阿里云控制台错误日志

4. 验证码接收延迟

问题:验证码到达较慢
解决

  • 检查网络连接
  • 确认手机号格式正确
  • 联系阿里云技术支持

总结

通过以上步骤,我们成功在 FastAdmin 中集成了阿里云新融合认证短信产品。这个解决方案具有以下优势:

  1. 高可靠性:使用阿里云官方 SDK,稳定可靠
  2. 易于维护:代码结构清晰,便于后续维护
  3. 安全性高:包含频率限制、IP 限制等安全措施
  4. 扩展性强:支持自定义模板和参数
  5. 完善的错误处理:提供详细的错误信息和日志记录

以上都是吹牛的, 其实就是一个个人发验证码的,无需企业资质解决方案

在实际使用中,建议根据业务需求进一步完善安全策略和监控机制,确保短信服务的稳定运行。

本作品采用《CC 协议》,转载必须注明作者和本文链接
嗨,我是波波。曾经创业,有收获也有损失。我积累了丰富教学与编程经验,期待和你互动和进步! 公众号:上海PHP自学中心
wangchunbo
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
司机 @ 某医疗行业
文章
312
粉丝
352
喜欢
565
收藏
1135
排名:61
访问:12.6 万
私信
所有博文
社区赞助商