前端輸入手機號 → 點擊「獲取驗證碼」 → AJAX 請求後端
↓
後端驗證手機號格式 → 生成 6 位隨機碼 → 存入 Redis (key: phone, value: code, TTL: 5min)
↓
調用短信服務商 API 發送 → 返回前端成功 → 前端按鈕倒計時 60s
↓
用戶輸入驗證碼提交表單 → 後端從 Redis 取碼比對 → 刪除 Redis key → 業務邏輯處理
{
try {
smsService.send(phone, "您的驗證碼是:" + code + "," + TTL_MINUTES + " 分鐘內有效。");
} catch (Exception e) {
log.error("短信發送失敗: {}", phone, e);
// 發送失敗可選:刪除已存驗證碼、記錄失敗日誌、告警
}
});
return R.ok();
}
@PostMapping("/verify")
public R<Boolean> verifyCode(@RequestParam String phone, @RequestParam String code) {
String codeKey = CODE_PREFIX + phone;
String storedCode = redisTemplate.opsForValue().get(codeKey);
if (storedCode == null) {
return R.fail("驗證碼已過期或不存在");
}
if (!storedCode.equals(code)) {
return R.fail("驗證碼錯誤");
}
// 驗證成功後立即刪除,防止重複使用
redisTemplate.delete(codeKey);
return R.ok(true);
}
}
public interface SmsService {
void send(String phone, String content);
}
@Service
public class AliyunSmsService implements SmsService {
@Value("${sms.aliyun.access-key}") private String accessKey;
@Value("${sms.aliyun.secret-key}") private String secretKey;
@Value("${sms.aliyun.sign-name}") private String signName;
@Value("${sms.aliyun.template-code}") private String templateCode;
@Override
public void send(String phone, String content) {
// 使用阿里云 SDK 發送
DefaultAcsClient client = new DefaultAcsClient(
DefaultProfile.getProfile("cn-hangzhou", accessKey, secretKey));
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam("{\"code\":\"" + extractCode(content) + "\"}");
client.getAcsResponse(request);
}
private String extractCode(String content) {
// 從內容中提取純數字碼
return content.replaceAll("\\D", "");
}
}
<template>
<div class="form-group">
<input v-model="form.phone" type="tel" placeholder="請輸入手機號" />
<button
:disabled="sending || countdown > 0"
@click="sendCode"
class="btn-code"
>
{{ countdown > 0 ? countdown + 's 後重試' : '獲取驗證碼' }}
</button>
</div>
<input v-model="form.code" type="text" placeholder="請輸入驗證碼" maxlength="6" />
<button @click="submit" :disabled="submitting">提交</button>
</template>
<script setup>
import { ref } from 'vue'
import api from '@/utils/api'
const form = ref({ phone: '', code: '' })
const countdown = ref(0)
const sending = ref(false)
const sendCode = async () => {
if (!/^1[3-9]\d{9}$/.test(form.value.phone)) {
return ElMessage.error('手機號格式不正確')
}
sending.value = true
try {
await api.post('/api/sms/send', { phone: form.value.phone })
ElMessage.success('發送成功')
startCountdown()
} catch (e) {
ElMessage.error(e.response?.data?.msg || '發送失敗')
} finally {
sending.value = false
}
}
const startCountdown = () => {
countdown.value = 60
const timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) clearInterval(timer)
}, 1000)
}
const submit = async () => {
if (!form.value.code) return ElMessage.error('請輸入驗證碼')
try {
await api.post('/api/sms/verify', form.value)
ElMessage.success('驗證通過')
// 跳轉或執行業務邏輯
} catch (e) {
ElMessage.error(e.response?.data?.msg || '驗證失敗')
}
}
</script>
| 風險 | 防護方案 |
|---|---|
| 短信轟炸/惡意刷單 | Redis 設置發送間隔鎖 (60s) + 日限制 (如 10 次/天) + IP 限制 |
| 驗證碼暴力破解 | Redis 設置驗證碼 TTL (5min) + 驗證失敗計數 (5 次鎖定 15min) |
| 驗證碼重複使用 | 驗證成功後立即 redisTemplate.delete(key) |
| 短信內容模板審核 | 使用服務商模板變量,避免自定義內容被攔截 |
| 接口被爬蟲調用 | 增加圖形驗證碼/滑塊驗證 + 簽名校驗 + 限流 |
// 驗證碼存儲
sms:code:{phone} → "123456" (TTL 5min)
// 發送間隔鎖
sms:code:interval:{phone} → "1" (TTL 60s)
// 日發送次數計數
sms:code:daily:{phone} → "3" (TTL 24h)
// 驗證失敗計數
sms:code:fail:{phone} → "2" (TTL 15min)
短信驗證碼系統的核心在於狀態管理 (Redis) 與異步解耦 (發送不阻塞響應)。關鍵點: