完整的驗證碼發送類需解決:防暴力刷碼、同一裝置/手機號限制、驗證碼有效期管理。本文展示基於 Redis 的生產級實作。
EXPIRE_SEC = 1800)RESEND_SEC = 60)ONE_DAY_FREQ = 5)ONE_DAY_IMEI_COUNT = 3)<?php
class Api_Sms {
const EXPIRE_SEC = 1800;
const RESEND_SEC = 60;
const ONE_DAY_FREQ = 5;
const ONE_DAY_IMEI_COUNT = 3;
public $error = [];
public function sendVerifyCode($mobile, $imei) {
if (!$this->isMobile($mobile)) {
$this->error = ['code' => -1, 'msg' => '手機號格式錯誤'];
return false;
}
$redis = Api_Common::redis();
$vcKey = "VC_{$mobile}";
$limitKey = "VC_LIMIT_{$mobile}";
// 1. 重發頻率檢查
$data = json_decode($redis->get($vcKey), true);
if ($data && time() < $data['resend_expire']) {
$this->error = ['code' => -1, 'msg' => '短信已在1分鐘內發出,請耐心等待'];
return false;
}
// 2. IMEI 與手機號限制檢查
$sendCnt = $redis->zScore($limitKey, $imei);
if ($sendCnt && $sendCnt >= self::ONE_DAY_FREQ) {
$this->error = ['code' => -1, 'msg' => '今日發送次數已達上限'];
return false;
}
$imeiCnt = $redis->zCard($limitKey);
if ($imeiCnt >= self::ONE_DAY_IMEI_COUNT && !$sendCnt) {
$this->error = ['code' => -1, 'msg' => '已超過驗證碼發送設備限制'];
return false;
}
// 3. 產生/取得驗證碼
if (!$data) {
$vc = strval(rand(100000, 999999));
$data = ['vc' => $vc, 'resend_expire' => 0];
$redis->set($vcKey, json_encode($data));
$redis->expire($vcKey, self::EXPIRE_SEC);
}
$vc = $data['vc'];
// 4. 發送簡訊(需對接實際簡訊商 API)
$result = $this->send($mobile, "安全驗證碼:{$vc}");
if ($result) {
// 更新重發時間戳
$data['resend_expire'] = time() + self::RESEND_SEC;
$ttl = $redis->ttl($vcKey);
$redis->set($vcKey, json_encode($data));
$redis->expire($vcKey, $ttl);
// 記錄發送統計
$redis->zIncrBy($limitKey, 1, $imei);
$redis->expireAt($limitKey, strtotime(date('Y-m-d', strtotime('+1 day'))));
}
return $result;
}
public function send($mobile, $content) {
// 對接簡訊商 HTTP API,回傳 bool
return true;
}
private function isMobile($mobile) {
return preg_match('/^1\d{10}$/', $mobile);
}
public function checkVerifyCode($mobile, $vc) {
$data = json_decode(Api_Common::redis()->get("VC_{$mobile}"), true);
return $data && $data['vc'] === $vc;
}
public function cleanVerifyCode($mobile) {
$redis = Api_Common::redis();
$redis->del("VC_{$mobile}");
$redis->del("VC_LIMIT_{$mobile}");
}
}
VC_{mobile} (String):{vc, resend_expire},TTL 30 分鐘VC_LIMIT_{mobile} (Sorted Set):member=IMEI, score=發送次數,隔日 00:00 過期