本文目录导读:

我来详细介绍PHP项目对接短信验证码的完整流程和最佳实践。
基本流程
用户输入手机号 → 生成验证码 → 调用短信API发送 → 存储验证码(带有效期) → 用户输入验证码 → 校验
主流短信服务商对接
阿里云短信服务
安装SDK:
composer require alibabacloud/dysmsapi
发送短信验证码:
<?php
namespace App\Service;
use AlibabaCloud\Client\AlibabaCloud;
use AlibabaCloud\Client\Exception\ClientException;
use AlibabaCloud\Client\Exception\ServerException;
class SmsService
{
private $accessKeyId;
private $accessKeySecret;
private $signName;
public function __construct()
{
$this->accessKeyId = '你的AccessKeyId';
$this->accessKeySecret = '你的AccessKeySecret';
$this->signName = '你的短信签名';
}
/**
* 发送验证码
* @param string $phone 手机号
* @param string $code 验证码
* @return bool
*/
public function sendVerifyCode($phone, $code)
{
AlibabaCloud::accessKeyClient($this->accessKeyId, $this->accessKeySecret)
->regionId('cn-hangzhou')
->asDefaultClient();
try {
$result = AlibabaCloud::rpc()
->product('Dysmsapi')
->version('2017-05-25')
->action('SendSms')
->method('POST')
->host('dysmsapi.aliyuncs.com')
->options([
'query' => [
'PhoneNumbers' => $phone,
'SignName' => $this->signName,
'TemplateCode' => 'SMS_你的模板ID',
'TemplateParam' => json_encode([
'code' => $code
]),
],
])
->request();
$response = $result->toArray();
if ($response['Code'] == 'OK') {
return true;
}
throw new \Exception($response['Message']);
} catch (ClientException $e) {
throw new \Exception($e->getErrorMessage());
} catch (ServerException $e) {
throw new \Exception($e->getErrorMessage());
}
}
}
腾讯云短信服务
安装SDK:
composer require tencentcloud/sms
发送代码:
<?php
namespace App\Service;
use TencentCloud\Common\Credential;
use TencentCloud\Sms\V20210111\SmsClient;
use TencentCloud\Sms\V20210111\Models\SendSmsRequest;
class TencentSmsService
{
private $secretId;
private $secretKey;
private $smsSdkAppId;
private $signName;
public function __construct()
{
$this->secretId = '你的SecretId';
$this->secretKey = '你的SecretKey';
$this->smsSdkAppId = '你的应用ID';
$this->signName = '你的签名';
}
public function sendVerifyCode($phone, $code)
{
try {
$cred = new Credential($this->secretId, $this->secretKey);
$client = new SmsClient($cred, "ap-guangzhou");
$req = new SendSmsRequest();
$req->PhoneNumberSet = ["+86" . $phone];
$req->SmsSdkAppId = $this->smsSdkAppId;
$req->SignName = $this->signName;
$req->TemplateId = "你的模板ID";
$req->TemplateParamSet = [$code];
$resp = $client->SendSms($req);
return $resp->SendStatusSet[0]->Code === 'Ok';
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
}
云片短信服务
<?php
namespace App\Service;
class YunpianSmsService
{
private $apiKey;
private $apiUrl = 'https://sms.yunpian.com/v2/sms/single_send.json';
public function __construct()
{
$this->apiKey = '你的API Key';
}
public function sendVerifyCode($phone, $code)
{
$data = [
'apikey' => $this->apiKey,
'mobile' => $phone,
'text' => "【公司名称】您的验证码是{$code},5分钟内有效。"
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->apiUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200) {
$result = json_decode($response, true);
return $result['code'] == 0;
}
return false;
}
}
验证码生成与存储
验证码生成
<?php
namespace App\Service;
class VerifyCodeService
{
/**
* 生成验证码
* @param int $length 长度
* @param bool $isNumeric 是否纯数字
* @return string
*/
public static function generate($length = 6, $isNumeric = true)
{
if ($isNumeric) {
$min = pow(10, $length - 1);
$max = pow(10, $length) - 1;
return strval(rand($min, $max));
}
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$code = '';
for ($i = 0; $i < $length; $i++) {
$code .= $chars[rand(0, strlen($chars) - 1)];
}
return $code;
}
}
使用Redis存储验证码
<?php
namespace App\Service;
use Predis\Client;
class SmsCodeManager
{
private $redis;
private $expireTime = 300; // 5分钟有效期
public function __construct()
{
$this->redis = new Client([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
]);
}
/**
* 存储验证码
* @param string $phone 手机号
* @param string $code 验证码
* @param string $type 验证码类型(register/login/reset)
*/
public function saveCode($phone, $code, $type = 'verify')
{
$key = "sms_code:{$type}:{$phone}";
$this->redis->setex($key, $this->expireTime, $code);
}
/**
* 验证验证码
* @param string $phone 手机号
* @param string $code 验证码
* @param string $type 验证码类型
* @return bool
*/
public function verifyCode($phone, $code, $type = 'verify')
{
$key = "sms_code:{$type}:{$phone}";
$storedCode = $this->redis->get($key);
if ($storedCode === null) {
return false; // 验证码已过期
}
if ($storedCode !== $code) {
return false; // 验证码错误
}
// 验证成功后删除验证码(防止重复使用)
$this->redis->del($key);
return true;
}
/**
* 检查发送频率限制
* @param string $phone 手机号
* @return bool
*/
public function checkRateLimit($phone)
{
$key = "sms_rate:{$phone}";
$count = $this->redis->get($key);
if ($count === null) {
// 60秒内只能发送1次
$this->redis->setex($key, 60, 1);
return true;
}
if ($count >= 1) {
return false; // 超过频率限制
}
$this->redis->incr($key);
return true;
}
}
使用数据库存储验证码
CREATE TABLE `sms_verify_codes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `phone` varchar(20) NOT NULL COMMENT '手机号', `code` varchar(10) NOT NULL COMMENT '验证码', `type` varchar(20) NOT NULL DEFAULT 'verify' COMMENT '验证码类型', `expire_time` datetime NOT NULL COMMENT '过期时间', `used` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否已使用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_phone_type` (`phone`, `type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
完整的发送流程
<?php
namespace App\Controller;
use App\Service\SmsService;
use App\Service\VerifyCodeService;
use App\Service\SmsCodeManager;
class SmsController
{
private $smsService;
private $codeManager;
public function __construct()
{
$this->smsService = new SmsService();
$this->codeManager = new SmsCodeManager();
}
/**
* 发送验证码接口
* @param string $phone 手机号
* @param string $type 验证码类型
* @return bool
*/
public function sendCode($phone, $type = 'verify')
{
try {
// 1. 验证手机号格式
if (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
throw new \Exception('手机号格式不正确');
}
// 2. 检查发送频率
if (!$this->codeManager->checkRateLimit($phone)) {
throw new \Exception('发送太频繁,请稍后再试');
}
// 3. 生成验证码
$code = VerifyCodeService::generate();
// 4. 发送短信
$result = $this->smsService->sendVerifyCode($phone, $code);
if ($result) {
// 5. 存储验证码
$this->codeManager->saveCode($phone, $code, $type);
return ['success' => true, 'message' => '验证码已发送'];
}
throw new \Exception('短信发送失败');
} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
/**
* 验证验证码接口
* @param string $phone 手机号
* @param string $code 验证码
* @param string $type 验证码类型
* @return bool
*/
public function verifyCode($phone, $code, $type = 'verify')
{
try {
if ($this->codeManager->verifyCode($phone, $code, $type)) {
return ['success' => true, 'message' => '验证成功'];
}
throw new \Exception('验证码错误或已过期');
} catch (\Exception $e) {
return ['success' => false, 'message' => $e->getMessage()];
}
}
}
最佳实践与安全建议
配置管理
// config/sms.php
return [
'driver' => env('SMS_DRIVER', 'aliyun'),
'aliyun' => [
'access_key_id' => env('ALIYUN_SMS_ACCESS_KEY_ID'),
'access_key_secret' => env('ALIYUN_SMS_ACCESS_KEY_SECRET'),
'sign_name' => env('ALIYUN_SMS_SIGN_NAME'),
'template_code' => env('ALIYUN_SMS_TEMPLATE_CODE'),
],
'tencent' => [
'secret_id' => env('TENCENT_SMS_SECRET_ID'),
'secret_key' => env('TENCENT_SMS_SECRET_KEY'),
'app_id' => env('TENCENT_SMS_APP_ID'),
'sign_name' => env('TENCENT_SMS_SIGN_NAME'),
'template_id' => env('TENCENT_SMS_TEMPLATE_ID'),
],
];
安全措施
// 1. IP限制
$ip = $_SERVER['REMOTE_ADDR'];
$ipKey = "sms_ip_limit:{$ip}";
if ($redis->get($ipKey) >= 10) { // 同一IP每小时最多10次
throw new \Exception('发送过于频繁');
}
// 2. 图形验证码
// 在发送短信前,需要验证图形验证码
public function beforeSendCode($captchaCode)
{
session_start();
if (strtolower($captchaCode) !== strtolower($_SESSION['captcha'])) {
throw new \Exception('图形验证码错误');
}
}
// 3. 手机号白名单
private $whiteList = ['13800138000']; // 测试手机号
if (!in_array($phone, $this->whiteList) && !$this->isProduction()) {
// 非生产环境,只对白名单手机号发送真实短信
$this->logVerifyCode($phone, $code);
return true;
}
异常处理
class SmsException extends \Exception
{
public function __construct($message, $code = 0)
{
parent::__construct($message, $code);
// 记录日志
Log::error("SMS Error: {$message}", [
'code' => $code,
'trace' => $this->getTraceAsString()
]);
}
}
测试环境处理
// 开发环境不真实发送短信
public function sendVerifyCode($phone, $code)
{
if (env('APP_ENV') !== 'production') {
// 记录日志或返回固定验证码
Log::info("测试验证码: {$phone} -> {$code}");
return true;
}
// 真实发送逻辑
}
注意事项
- 验证码有效期:通常5分钟,重要操作可缩短至1-2分钟
- 频率限制:同一手机号60秒内只能发送1次,每天最多10次
- 验证码长度:建议4-6位数字
- 防刷机制:
- IP限制
- 图形验证码
- 设备指纹
- 成本控制:配置不同短信模板,合理使用签名
- 监控告警:监控短信发送成功率,异常及时告警
选择合适的短信服务商,根据业务需求配置不同的验证码策略,注意安全防护和成本控制。