Java實(shí)現(xiàn)短信驗(yàn)證碼功能的完整代碼
一、被盜刷的慘狀:驗(yàn)證碼的“春運(yùn)”現(xiàn)場(chǎng)
想象一下這個(gè)場(chǎng)景:你的短信驗(yàn)證碼接口就像雙十一的購(gòu)物車(chē),一群“羊毛黨”開(kāi)著機(jī)器人拖拉機(jī),以每秒100次的速度瘋狂點(diǎn)擊“發(fā)送驗(yàn)證碼”按鈕。你的短信費(fèi)就像漏氣的氣球一樣癟下去,而真正的用戶(hù)卻收不到驗(yàn)證碼,急得像熱鍋上的螞蟻。
更可怕的是,可能:
- 用你的錢(qián)給隔壁老王發(fā)“我愛(ài)你”短信
- 測(cè)試出所有已注冊(cè)手機(jī)號(hào)(撞庫(kù)攻擊)
- 讓你的服務(wù)器累到懷疑人生,直接躺平(DDoS)
二、防御戰(zhàn)術(shù)大全:給接口裝上“金鐘罩”
1.頻率限制:給“點(diǎn)擊狂魔”戴上手銬
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 短信衛(wèi)士 - 專(zhuān)治各種手速過(guò)快
*/
public class SmsGuard {
// 使用Guava Cache存儲(chǔ)訪問(wèn)頻率
private static final Cache<String, AtomicInteger> IP_CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
private static final Cache<String, AtomicInteger> PHONE_CACHE =
CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
/**
* 檢查這個(gè)IP是不是在開(kāi)掛
* @param ip 客戶(hù)端IP
* @param maxAttempts 最大嘗試次數(shù)(比如1小時(shí)10次)
* @return true=正常用戶(hù),false=疑似黑客
*/
public static boolean isIpAllowed(String ip, int maxAttempts) {
try {
AtomicInteger counter = IP_CACHE.get(ip, () -> new AtomicInteger(0));
int attempts = counter.incrementAndGet();
if (attempts > maxAttempts) {
System.out.println("檢測(cè)到IP " + ip + " 疑似開(kāi)掛,已攔截!");
return false;
}
return true;
} catch (Exception e) {
return false; // 出錯(cuò)時(shí)保守一點(diǎn),拒絕訪問(wèn)
}
}
/**
* 檢查這個(gè)手機(jī)號(hào)是不是在刷驗(yàn)證碼
* @param phone 手機(jī)號(hào)
* @param maxSmsPerHour 每小時(shí)最多發(fā)幾條
* @return true=可以發(fā),false=發(fā)太多了
*/
public static boolean isPhoneAllowed(String phone, int maxSmsPerHour) {
try {
AtomicInteger counter = PHONE_CACHE.get(phone, () -> new AtomicInteger(0));
int sentCount = counter.incrementAndGet();
if (sentCount > maxSmsPerHour) {
System.out.println("手機(jī)號(hào) " + phone + " 今天已經(jīng)收到" + sentCount + "條驗(yàn)證碼,讓它歇會(huì)兒吧");
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
}
2.圖形驗(yàn)證碼:讓機(jī)器人“看圖說(shuō)話”
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.util.Random;
/**
* 驗(yàn)證碼生成器 - 專(zhuān)治眼瞎的機(jī)器人
*/
public class CaptchaGenerator {
/**
* 生成能讓機(jī)器人懷疑人生的驗(yàn)證碼
* @return [0]=圖片Base64, [1]=驗(yàn)證碼答案
*/
public static String[] generateCaptcha() {
int width = 120;
int height = 40;
// 創(chuàng)建一張讓機(jī)器人哭泣的圖片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
// 設(shè)置背景色(隨機(jī)淺色)
g.setColor(getRandomLightColor());
g.fillRect(0, 0, width, height);
// 畫(huà)干擾線(讓機(jī)器人眼花繚亂)
g.setColor(Color.BLACK);
Random random = new Random();
for (int i = 0; i < 10; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 生成隨機(jī)驗(yàn)證碼(避開(kāi)容易混淆的字符)
String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
StringBuilder captchaText = new StringBuilder();
for (int i = 0; i < 4; i++) {
char c = chars.charAt(random.nextInt(chars.length()));
captchaText.append(c);
// 扭曲、旋轉(zhuǎn)、變色 - 三連擊!
g.setFont(new Font("Arial", Font.BOLD | Font.ITALIC, 30 + random.nextInt(5)));
g.setColor(getRandomDarkColor());
// 輕微旋轉(zhuǎn)字符
double theta = random.nextDouble() * 0.5 - 0.25;
g.rotate(theta, 20 + i * 25, 25);
g.drawString(String.valueOf(c), 20 + i * 25, 25);
g.rotate(-theta, 20 + i * 25, 25);
}
g.dispose();
try {
// 轉(zhuǎn)換為Base64
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "png", baos);
String base64Image = java.util.Base64.getEncoder().encodeToString(baos.toByteArray());
return new String[]{"data:image/png;base64," + base64Image, captchaText.toString()};
} catch (Exception e) {
throw new RuntimeException("驗(yàn)證碼生成失敗", e);
}
}
private static Color getRandomLightColor() {
Random random = new Random();
return new Color(200 + random.nextInt(55),
200 + random.nextInt(55),
200 + random.nextInt(55));
}
private static Color getRandomDarkColor() {
Random random = new Random();
return new Color(random.nextInt(150),
random.nextInt(150),
random.nextInt(150));
}
}
3.滑動(dòng)驗(yàn)證碼:讓機(jī)器人“學(xué)走路”
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 滑動(dòng)驗(yàn)證碼 - 專(zhuān)治不會(huì)用鼠標(biāo)的機(jī)器人
*/
public class SlideCaptchaService {
// 存儲(chǔ)驗(yàn)證會(huì)話
private static final ConcurrentHashMap<String, SlideCaptchaData> SESSIONS =
new ConcurrentHashMap<>();
/**
* 生成滑動(dòng)驗(yàn)證碼挑戰(zhàn)
*/
public static SlideChallenge generateChallenge() {
String sessionId = UUID.randomUUID().toString();
// 隨機(jī)生成目標(biāo)位置(這里簡(jiǎn)化了,實(shí)際應(yīng)該有圖片處理)
int targetX = 100 + new Random().nextInt(200);
int targetY = 50 + new Random().nextInt(100);
SlideCaptchaData data = new SlideCaptchaData(targetX, targetY);
SESSIONS.put(sessionId, data);
// 設(shè)置5分鐘過(guò)期
new Timer().schedule(new TimerTask() {
@Override
public void run() {
SESSIONS.remove(sessionId);
}
}, 5 * 60 * 1000);
return new SlideChallenge(sessionId, targetX, targetY);
}
/**
* 驗(yàn)證滑動(dòng)結(jié)果
*/
public static boolean verify(String sessionId, int userX, int userY) {
SlideCaptchaData data = SESSIONS.get(sessionId);
if (data == null) {
return false; // 會(huì)話過(guò)期
}
// 允許±5像素的誤差(人類(lèi)手抖,機(jī)器人太精確反而可疑)
boolean isValid = Math.abs(userX - data.targetX) <= 5 &&
Math.abs(userY - data.targetY) <= 5;
if (isValid) {
SESSIONS.remove(sessionId); // 一次性使用
}
return isValid;
}
static class SlideCaptchaData {
int targetX;
int targetY;
SlideCaptchaData(int targetX, int targetY) {
this.targetX = targetX;
this.targetY = targetY;
}
}
static class SlideChallenge {
String sessionId;
int targetX;
int targetY;
SlideChallenge(String sessionId, int targetX, int targetY) {
this.sessionId = sessionId;
this.targetX = targetX;
this.targetY = targetY;
}
}
}
4.完整的短信發(fā)送服務(wù)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 短信發(fā)送服務(wù) - 武裝到牙齒的版本
*/
@Service
public class SmsService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private RiskControlService riskControlService;
/**
* 發(fā)送驗(yàn)證碼(安全加強(qiáng)版)
*/
public ApiResponse sendVerificationCode(String phone, String ip, String captchaCode, String sessionId) {
// 1. 檢查IP風(fēng)險(xiǎn)
if (!riskControlService.checkIpRisk(ip)) {
return ApiResponse.error("您的網(wǎng)絡(luò)環(huán)境存在風(fēng)險(xiǎn),請(qǐng)稍后再試");
}
// 2. 驗(yàn)證圖形驗(yàn)證碼(如果有)
if (captchaCode != null && !validateCaptcha(sessionId, captchaCode)) {
return ApiResponse.error("驗(yàn)證碼錯(cuò)誤,請(qǐng)重新輸入");
}
// 3. 頻率控制:同一手機(jī)號(hào)1分鐘內(nèi)只能發(fā)1次
String minuteKey = "sms:minute:" + phone;
if (Boolean.TRUE.equals(redisTemplate.hasKey(minuteKey))) {
return ApiResponse.error("操作過(guò)于頻繁,請(qǐng)1分鐘后再試");
}
// 4. 頻率控制:同一手機(jī)號(hào)1小時(shí)內(nèi)最多5次
String hourKey = "sms:hour:" + phone;
Long hourCount = redisTemplate.opsForValue().increment(hourKey);
if (hourCount != null && hourCount == 1) {
redisTemplate.expire(hourKey, 1, TimeUnit.HOURS);
}
if (hourCount != null && hourCount > 5) {
return ApiResponse.error("今日驗(yàn)證碼發(fā)送次數(shù)已達(dá)上限");
}
// 5. 生成6位隨機(jī)驗(yàn)證碼
String code = String.format("%06d", new Random().nextInt(999999));
// 6. 存儲(chǔ)驗(yàn)證碼(5分鐘過(guò)期)
String codeKey = "sms:code:" + phone;
redisTemplate.opsForValue().set(codeKey, code, 5, TimeUnit.MINUTES);
// 7. 設(shè)置1分鐘冷卻期
redisTemplate.opsForValue().set(minuteKey, "1", 1, TimeUnit.MINUTES);
// 8. 記錄發(fā)送日志(用于分析)
logSmsSent(phone, ip, code);
// 9. 調(diào)用第三方短信服務(wù)(實(shí)際發(fā)送)
boolean sent = realSendSms(phone, code);
if (sent) {
// 10. 返回脫敏的手機(jī)號(hào)
String maskedPhone = phone.substring(0, 3) + "****" + phone.substring(7);
return ApiResponse.success("驗(yàn)證碼已發(fā)送至" + maskedPhone);
} else {
return ApiResponse.error("短信發(fā)送失敗,請(qǐng)稍后重試");
}
}
/**
* 驗(yàn)證短信驗(yàn)證碼
*/
public boolean verifyCode(String phone, String userCode) {
String codeKey = "sms:code:" + phone;
String correctCode = redisTemplate.opsForValue().get(codeKey);
if (correctCode == null) {
return false; // 驗(yàn)證碼已過(guò)期
}
// 驗(yàn)證成功后刪除驗(yàn)證碼(防止重復(fù)使用)
boolean isValid = correctCode.equals(userCode);
if (isValid) {
redisTemplate.delete(codeKey);
}
return isValid;
}
private void logSmsSent(String phone, String ip, String code) {
// 這里應(yīng)該記錄到數(shù)據(jù)庫(kù)或日志系統(tǒng)
System.out.println(String.format(
"短信發(fā)送日志: phone=%s, ip=%s, code=%s, time=%s",
phone, ip, code, new java.util.Date()
));
}
private boolean realSendSms(String phone, String code) {
// 調(diào)用真實(shí)的短信服務(wù)商接口
// 這里簡(jiǎn)化處理,實(shí)際應(yīng)該用HTTP客戶(hù)端調(diào)用
try {
System.out.println(String.format(
"發(fā)送短信到 %s: 您的驗(yàn)證碼是%s,5分鐘內(nèi)有效,打死也不要告訴別人哦!",
phone, code
));
return true;
} catch (Exception e) {
return false;
}
}
}
5.風(fēng)控服務(wù):火眼金睛識(shí)破壞人
/**
* 風(fēng)控服務(wù) - 專(zhuān)治各種不服
*/
@Service
public class RiskControlService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 綜合風(fēng)險(xiǎn)評(píng)估
*/
public RiskLevel assessRisk(String phone, String ip, String userAgent) {
int riskScore = 0;
// 1. IP地址檢查
if (isSuspiciousIp(ip)) {
riskScore += 30;
}
// 2. User-Agent檢查
if (isSuspiciousUserAgent(userAgent)) {
riskScore += 20;
}
// 3. 請(qǐng)求頻率檢查
if (isHighFrequency(ip)) {
riskScore += 40;
}
// 4. 手機(jī)號(hào)歸屬地 vs IP歸屬地
if (!isLocationConsistent(phone, ip)) {
riskScore += 20;
}
// 5. 歷史行為檢查
if (hasBadHistory(ip)) {
riskScore += 50;
}
// 根據(jù)分?jǐn)?shù)返回風(fēng)險(xiǎn)等級(jí)
if (riskScore >= 80) {
return RiskLevel.HIGH;
} else if (riskScore >= 50) {
return RiskLevel.MEDIUM;
} else {
return RiskLevel.LOW;
}
}
/**
* 檢查IP風(fēng)險(xiǎn)
*/
public boolean checkIpRisk(String ip) {
String key = "risk:ip:" + ip;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, 1, TimeUnit.HOURS);
}
// 1小時(shí)內(nèi)超過(guò)50次請(qǐng)求視為風(fēng)險(xiǎn)
return count == null || count <= 50;
}
enum RiskLevel {
LOW, // 低風(fēng)險(xiǎn):正常通過(guò)
MEDIUM, // 中風(fēng)險(xiǎn):需要額外驗(yàn)證
HIGH // 高風(fēng)險(xiǎn):直接拒絕
}
// 其他檢測(cè)方法...
}
三、防御體系總結(jié):打造鐵桶陣
多層防御體系
第一層:前端防護(hù)
- 圖形驗(yàn)證碼(專(zhuān)治簡(jiǎn)單機(jī)器人)
- 滑動(dòng)驗(yàn)證碼(專(zhuān)治中級(jí)機(jī)器人)
- 點(diǎn)擊按鈕防重放(防止連續(xù)點(diǎn)擊)
第二層:頻率限制
- IP級(jí)別限流(防止單一IP攻擊)
- 手機(jī)號(hào)級(jí)別限流(防止針對(duì)特定號(hào)碼)
- 設(shè)備指紋限流(更精準(zhǔn)的識(shí)別)
第三層:行為分析
- 請(qǐng)求時(shí)間分布分析(機(jī)器人請(qǐng)求太規(guī)律)
- 鼠標(biāo)軌跡分析(機(jī)器人不會(huì)手抖)
- 操作間隔分析(機(jī)器人反應(yīng)太快)
第四層:業(yè)務(wù)邏輯
- 驗(yàn)證碼有效期控制(通常5分鐘)
- 驗(yàn)證碼一次性使用(用后即焚)
- 錯(cuò)誤次數(shù)限制(防止破解)
監(jiān)控與預(yù)警
/**
* 監(jiān)控服務(wù) - 短信接口的"心電圖"
*/
@Service
public class SmsMonitorService {
// 關(guān)鍵指標(biāo)監(jiān)控
public void monitorMetrics() {
// 1. 成功率監(jiān)控
// 2. 響應(yīng)時(shí)間監(jiān)控
// 3. 異常請(qǐng)求監(jiān)控
// 4. 費(fèi)用消耗監(jiān)控
// 發(fā)現(xiàn)異常立即告警
// - 短信量突增
// - 成功率突降
// - 特定IP大量請(qǐng)求
}
/**
* 自動(dòng)熔斷機(jī)制
*/
public void circuitBreaker(String phonePrefix) {
// 如果某個(gè)號(hào)段異常,自動(dòng)臨時(shí)屏蔽
// 比如:170號(hào)段被大量攻擊,自動(dòng)限制該號(hào)段
}
}
實(shí)踐建議
- 按需發(fā)送:只有必要的時(shí)候才發(fā)驗(yàn)證碼,比如注冊(cè)、登錄、支付
- 內(nèi)容脫敏:短信中不要包含完整手機(jī)號(hào)
- 成本控制:設(shè)置每日、每月短信預(yù)算上限
- 驗(yàn)證碼復(fù)雜度:6位數(shù)字足夠,別搞太復(fù)雜
- 失敗處理:失敗時(shí)給出友好提示,但不要泄露細(xì)節(jié)
- 定期審計(jì):定期檢查日志,發(fā)現(xiàn)異常模式
四、道高一尺,魔高一丈
安全是一場(chǎng)持久戰(zhàn)。今天防住了普通機(jī)器人,明天可能就有高級(jí)AI來(lái)挑戰(zhàn)。關(guān)鍵在于:
- 不要依賴(lài)單一防御:多層防御才靠譜
- 保持更新:安全方案需要與時(shí)俱進(jìn)
- 監(jiān)控報(bào)警:早發(fā)現(xiàn)、早處理、早止損
- 成本意識(shí):既要安全,也要考慮用戶(hù)體驗(yàn)和實(shí)現(xiàn)成本
最最重要的是:不要把驗(yàn)證碼接口當(dāng)成公共廁所,誰(shuí)想來(lái)就來(lái)! 給它裝上門(mén)禁、攝像頭、保安,還要收門(mén)票(驗(yàn)證手段),這樣才能讓羊毛黨知難而退。
以上就是Java實(shí)現(xiàn)短信驗(yàn)證碼功能的完整代碼的詳細(xì)內(nèi)容,更多關(guān)于Java短信驗(yàn)證碼的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- Java實(shí)現(xiàn)短信驗(yàn)證碼服務(wù)的完整代碼示例
- Java實(shí)現(xiàn)短信驗(yàn)證碼詳細(xì)過(guò)程
- Java如何生成4位、6位隨機(jī)數(shù)短信驗(yàn)證碼(高效實(shí)現(xiàn))
- Java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼+redis限制發(fā)送的次數(shù)功能
- Java實(shí)現(xiàn)短信驗(yàn)證碼的示例代碼
- Java實(shí)現(xiàn)用戶(hù)短信驗(yàn)證碼登錄功能實(shí)例代碼
- java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼
- 基于Java隨機(jī)生成手機(jī)短信驗(yàn)證碼的實(shí)例代碼
相關(guān)文章
Java使用釘釘創(chuàng)建企業(yè)內(nèi)部機(jī)器人的實(shí)現(xiàn)
釘釘?shù)某霈F(xiàn)為企業(yè)節(jié)約了大量人力成本,其中也開(kāi)放了大量的外部api接口方便企業(yè)使用。本文主要介紹了Java使用釘釘創(chuàng)建企業(yè)內(nèi)部機(jī)器人的實(shí)現(xiàn),感興趣的可以了解一下2021-11-11
注冊(cè)中心配置了spring?security后客戶(hù)端啟動(dòng)報(bào)錯(cuò)
這篇文章主要為大家介紹了注冊(cè)中心配置了spring?security后客戶(hù)端啟動(dòng)報(bào)錯(cuò)問(wèn)題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
靜態(tài)工具類(lèi)中注入Bean及引用Nacos配置方式
在Spring開(kāi)發(fā)中,靜態(tài)工具類(lèi)無(wú)法注入Bean或Nacos值,因靜態(tài)變量加載時(shí)Spring上下文未初始化,建議通過(guò)@Component標(biāo)記Bean,使用構(gòu)造器或Setter注入依賴(lài),或直接在Bean內(nèi)處理Nacos配置,實(shí)現(xiàn)靈活調(diào)用2025-07-07
Maven入門(mén)教程之如何在idea中配置Maven
Maven是非常出色的項(xiàng)目管理工具,我們可以用它管理本地項(xiàng)目,下面這篇文章主要給大家介紹了關(guān)于Maven入門(mén)教程之如何在idea中配置Maven的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04
Java封裝數(shù)組實(shí)現(xiàn)包含、搜索和刪除元素操作詳解
這篇文章主要介紹了Java封裝數(shù)組實(shí)現(xiàn)包含、搜索和刪除元素操作,結(jié)合實(shí)例形式分析了java針對(duì)數(shù)組元素的查找、刪除、判斷等相關(guān)操作封裝與使用技巧,需要的朋友可以參考下2020-03-03
java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲(chóng)代碼示例
這篇文章主要介紹了java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)爬蟲(chóng)代碼示例,還是挺不錯(cuò)的,這里分享給大家,需要的朋友可以參考下。2017-11-11

