Java 防止短信驗(yàn)證碼接口被盜刷問(wèn)題解決
一、Bug 場(chǎng)景
在一個(gè)基于 Java 的 Web 應(yīng)用中,用戶注冊(cè)或找回密碼等功能依賴短信驗(yàn)證碼進(jìn)行身份驗(yàn)證。然而,近期發(fā)現(xiàn)短信驗(yàn)證碼接口被惡意用戶頻繁調(diào)用,導(dǎo)致大量短信被發(fā)送,不僅增加了運(yùn)營(yíng)成本,還影響了正常用戶的使用體驗(yàn),甚至可能因觸發(fā)運(yùn)營(yíng)商短信發(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ī)號(hào)不能為空");
}
}
}
三、問(wèn)題描述
- 預(yù)期行為:正常用戶在需要時(shí)能夠獲取短信驗(yàn)證碼,且惡意用戶無(wú)法通過(guò)頻繁調(diào)用接口來(lái)盜刷短信驗(yàn)證碼,確保短信發(fā)送資源合理使用,服務(wù)穩(wěn)定運(yùn)行。
- 實(shí)際行為:惡意用戶可以通過(guò)編寫自動(dòng)化腳本,不斷調(diào)用短信驗(yàn)證碼接口,導(dǎo)致大量不必要的短信被發(fā)送。這是因?yàn)楫?dāng)前接口沒(méi)有任何限制機(jī)制,無(wú)論是正常用戶還是惡意用戶,只要提供了手機(jī)號(hào),就可以無(wú)限制地獲取短信驗(yàn)證碼。
四、解決方案
- 增加頻率限制:通過(guò)記錄用戶請(qǐng)求頻率,限制同一手機(jī)號(hào)在一定時(shí)間內(nèi)的短信發(fā)送次數(shù)??梢允褂?Redis 來(lái)存儲(chǔ)手機(jī)號(hào)的發(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è)置過(guò)期時(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ā)送頻率過(guò)高,請(qǐng)稍后再試");
}
}
}
修改后的短信驗(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ī)號(hào)不能為空");
}
}
}
- 使用圖形驗(yàn)證碼:在發(fā)送短信驗(yàn)證碼之前,要求用戶先輸入圖形驗(yàn)證碼。用戶只有正確輸入圖形驗(yàn)證碼后,才能請(qǐng)求短信驗(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 或其他存儲(chǔ)中獲取
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ī)號(hào)不能為空");
}
}
}
- IP 訪問(wèn)限制:記錄請(qǐng)求的 IP 地址,限制同一 IP 在一定時(shí)間內(nèi)對(duì)短信驗(yàn)證碼接口的訪問(wèn)次數(shù)。同樣可以使用 Redis 來(lái)實(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 最多請(qǐng)求 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è)置過(guò)期時(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 或其他存儲(chǔ)中獲取
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("請(qǐng)求頻率過(guò)高,請(qǐng)稍后再試");
}
} else {
response.getWriter().println("手機(jī)號(hào)不能為空");
}
}
}
到此這篇關(guān)于Java 防止短信驗(yàn)證碼接口被盜刷問(wèn)題解決的文章就介紹到這了,更多相關(guān)Java 防止短信驗(yàn)證碼接口被盜刷內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- JAVA實(shí)現(xiàn)利用第三方平臺(tái)發(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ò)程
相關(guān)文章
mybatis-plus無(wú)法通過(guò)logback-spring輸出的解決方法
本文主要介紹了mybatis-plus無(wú)法通過(guò)logback-spring輸出,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11
SpringBoot為何可以使用Jar包啟動(dòng)詳解
springboot jar包啟動(dòng)腳本,適用于快速啟動(dòng),刪除,重啟,以及查看狀態(tài),下面這篇文章主要給大家介紹了關(guān)于SpringBoot為何可以使用Jar包啟動(dòng)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03
如何在SpringBoot項(xiàng)目中集成SpringSecurity進(jìn)行權(quán)限管理
在本文中,我們將討論如何在Spring?Boot項(xiàng)目中集成權(quán)限管理,我們將使用Spring?Security框架,這是一個(gè)專門用于實(shí)現(xiàn)安全性功能的框架,包括認(rèn)證和授權(quán),需要的朋友可以參考下2023-07-07
java開發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西
這篇文章主要為大家介紹了java開發(fā)實(shí)現(xiàn)訂閱到貨通知幫我們買到想買的東西示例demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
關(guān)于@OnetoMany關(guān)系映射的排序問(wèn)題,使用注解@OrderBy
這篇文章主要介紹了關(guān)于@OnetoMany關(guān)系映射的排序問(wèn)題,使用注解@OrderBy,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(20)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07
使用String類的compareTo方法比較規(guī)則詳解
這篇文章主要介紹了使用String類的compareTo方法比較規(guī)則,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-06-06

