Java 防止短信驗(yàn)證碼接口被盜刷問題解決
一、Bug 場景
在一個(gè)基于 Java 的 Web 應(yīng)用中,用戶注冊或找回密碼等功能依賴短信驗(yàn)證碼進(jìn)行身份驗(yàn)證。然而,近期發(fā)現(xiàn)短信驗(yàn)證碼接口被惡意用戶頻繁調(diào)用,導(dǎo)致大量短信被發(fā)送,不僅增加了運(yùn)營成本,還影響了正常用戶的使用體驗(yàn),甚至可能因觸發(fā)運(yùn)營商短信發(fā)送限制而導(dǎo)致服務(wù)不可用。
二、代碼示例
短信發(fā)送服務(wù)類(有缺陷)
import java.util.Random;
public class SmsService {
public void sendSms(String phoneNumber) {
// 生成 6 位隨機(jī)驗(yàn)證碼
int code = new Random().nextInt(900000) + 100000;
System.out.println("向 " + phoneNumber + " 發(fā)送短信驗(yàn)證碼: " + code);
// 實(shí)際應(yīng)用中應(yīng)調(diào)用短信發(fā)送 API 發(fā)送驗(yàn)證碼
}
}
短信驗(yàn)證碼接口控制器(有缺陷)
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SmsVerificationCodeController {
private SmsService smsService = new SmsService();
public void sendVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String phoneNumber = request.getParameter("phoneNumber");
if (phoneNumber != null) {
smsService.sendSms(phoneNumber);
response.getWriter().println("短信驗(yàn)證碼已發(fā)送");
} else {
response.getWriter().println("手機(jī)號不能為空");
}
}
}
三、問題描述
- 預(yù)期行為:正常用戶在需要時(shí)能夠獲取短信驗(yàn)證碼,且惡意用戶無法通過頻繁調(diào)用接口來盜刷短信驗(yàn)證碼,確保短信發(fā)送資源合理使用,服務(wù)穩(wěn)定運(yùn)行。
- 實(shí)際行為:惡意用戶可以通過編寫自動(dòng)化腳本,不斷調(diào)用短信驗(yàn)證碼接口,導(dǎo)致大量不必要的短信被發(fā)送。這是因?yàn)楫?dāng)前接口沒有任何限制機(jī)制,無論是正常用戶還是惡意用戶,只要提供了手機(jī)號,就可以無限制地獲取短信驗(yàn)證碼。
四、解決方案
- 增加頻率限制:通過記錄用戶請求頻率,限制同一手機(jī)號在一定時(shí)間內(nèi)的短信發(fā)送次數(shù)。可以使用 Redis 來存儲手機(jī)號的發(fā)送記錄和時(shí)間戳。
import redis.clients.jedis.Jedis;
import java.util.Random;
public class SmsService {
private static final int MAX_SENDS_PER_MINUTE = 3; // 每分鐘最多發(fā)送 3 次
private Jedis jedis;
public SmsService(Jedis jedis) {
this.jedis = jedis;
}
public boolean canSendSms(String phoneNumber) {
String key = "sms:limit:" + phoneNumber;
Long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, 60); // 設(shè)置過期時(shí)間為 1 分鐘
}
return count <= MAX_SENDS_PER_MINUTE;
}
public void sendSms(String phoneNumber) {
if (canSendSms(phoneNumber)) {
int code = new Random().nextInt(900000) + 100000;
System.out.println("向 " + phoneNumber + " 發(fā)送短信驗(yàn)證碼: " + code);
// 實(shí)際應(yīng)用中應(yīng)調(diào)用短信發(fā)送 API 發(fā)送驗(yàn)證碼
} else {
System.out.println("發(fā)送頻率過高,請稍后再試");
}
}
}
修改后的短信驗(yàn)證碼接口控制器
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import java.io.IOException;
public class SmsVerificationCodeController {
private SmsService smsService;
public SmsVerificationCodeController(Jedis jedis) {
this.smsService = new SmsService(jedis);
}
public void sendVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String phoneNumber = request.getParameter("phoneNumber");
if (phoneNumber != null) {
smsService.sendSms(phoneNumber);
response.getWriter().println("短信驗(yàn)證碼已發(fā)送");
} else {
response.getWriter().println("手機(jī)號不能為空");
}
}
}
- 使用圖形驗(yàn)證碼:在發(fā)送短信驗(yàn)證碼之前,要求用戶先輸入圖形驗(yàn)證碼。用戶只有正確輸入圖形驗(yàn)證碼后,才能請求短信驗(yàn)證碼,增加惡意調(diào)用的難度。
// 圖形驗(yàn)證碼生成和驗(yàn)證邏輯(示例代碼,實(shí)際需更完善實(shí)現(xiàn))
public class CaptchaService {
public String generateCaptcha() {
// 生成圖形驗(yàn)證碼的邏輯,返回驗(yàn)證碼字符串
return "1234";
}
public boolean verifyCaptcha(String inputCaptcha, String storedCaptcha) {
return inputCaptcha.equals(storedCaptcha);
}
}
再次修改后的短信驗(yàn)證碼接口控制器
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import java.io.IOException;
public class SmsVerificationCodeController {
private SmsService smsService;
private CaptchaService captchaService;
public SmsVerificationCodeController(Jedis jedis) {
this.smsService = new SmsService(jedis);
this.captchaService = new CaptchaService();
}
public void sendVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String phoneNumber = request.getParameter("phoneNumber");
String inputCaptcha = request.getParameter("captcha");
if (phoneNumber != null) {
String storedCaptcha = "1234"; // 實(shí)際應(yīng)用中應(yīng)從 session 或其他存儲中獲取
if (captchaService.verifyCaptcha(inputCaptcha, storedCaptcha)) {
smsService.sendSms(phoneNumber);
response.getWriter().println("短信驗(yàn)證碼已發(fā)送");
} else {
response.getWriter().println("圖形驗(yàn)證碼錯(cuò)誤");
}
} else {
response.getWriter().println("手機(jī)號不能為空");
}
}
}
- IP 訪問限制:記錄請求的 IP 地址,限制同一 IP 在一定時(shí)間內(nèi)對短信驗(yàn)證碼接口的訪問次數(shù)。同樣可以使用 Redis 來實(shí)現(xiàn)。
import redis.clients.jedis.Jedis;
import java.util.Random;
public class SmsService {
private static final int MAX_REQUESTS_PER_IP_PER_MINUTE = 10; // 每分鐘每個(gè) IP 最多請求 10 次
private Jedis jedis;
public SmsService(Jedis jedis) {
this.jedis = jedis;
}
public boolean canSendSmsFromIp(String ip) {
String key = "sms:ip:limit:" + ip;
Long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, 60); // 設(shè)置過期時(shí)間為 1 分鐘
}
return count <= MAX_REQUESTS_PER_IP_PER_MINUTE;
}
// 其他方法不變
}
最終修改后的短信驗(yàn)證碼接口控制器
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import redis.clients.jedis.Jedis;
import java.io.IOException;
public class SmsVerificationCodeController {
private SmsService smsService;
private CaptchaService captchaService;
public SmsVerificationCodeController(Jedis jedis) {
this.smsService = new SmsService(jedis);
this.captchaService = new CaptchaService();
}
public void sendVerificationCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
String phoneNumber = request.getParameter("phoneNumber");
String inputCaptcha = request.getParameter("captcha");
String clientIp = request.getRemoteAddr();
if (phoneNumber != null) {
String storedCaptcha = "1234"; // 實(shí)際應(yīng)用中應(yīng)從 session 或其他存儲中獲取
if (captchaService.verifyCaptcha(inputCaptcha, storedCaptcha) && smsService.canSendSmsFromIp(clientIp)) {
smsService.sendSms(phoneNumber);
response.getWriter().println("短信驗(yàn)證碼已發(fā)送");
} else if (!captchaService.verifyCaptcha(inputCaptcha, storedCaptcha)) {
response.getWriter().println("圖形驗(yàn)證碼錯(cuò)誤");
} else {
response.getWriter().println("請求頻率過高,請稍后再試");
}
} else {
response.getWriter().println("手機(jī)號不能為空");
}
}
}
到此這篇關(guān)于Java 防止短信驗(yàn)證碼接口被盜刷問題解決的文章就介紹到這了,更多相關(guān)Java 防止短信驗(yàn)證碼接口被盜刷內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JAVA實(shí)現(xiàn)利用第三方平臺發(fā)送短信驗(yàn)證碼
- Java隨機(jī)生成手機(jī)短信驗(yàn)證碼的方法
- java實(shí)現(xiàn)短信驗(yàn)證碼5分鐘有效時(shí)間
- Java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼功能
- java實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼
- 基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例
- java短信驗(yàn)證碼獲取次數(shù)限制實(shí)例
- 基于Java隨機(jī)生成手機(jī)短信驗(yàn)證碼的實(shí)例代碼
- Java開發(fā)完整短信驗(yàn)證碼功能的全過程
相關(guān)文章
ArrayList的自動(dòng)擴(kuò)充機(jī)制實(shí)例解析
本文主要介紹了ArrayList的自動(dòng)擴(kuò)充機(jī)制,由一個(gè)題目切入主題,逐步向大家展示了ArrayList的相關(guān)內(nèi)容,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10
Java調(diào)用MySQL數(shù)據(jù)庫的存儲過程和自定義函數(shù)及區(qū)別解析
本文介紹了存儲過程和自定義函數(shù)的概念、特性和構(gòu)建方法,并通過Java代碼展示了如何調(diào)用存儲過程和自定義函數(shù),最后,對比了存儲過程和自定義函數(shù)的區(qū)別,感興趣的朋友跟隨小編一起看看吧2025-12-12

