一、bug 场景
在一个基于 java 的 web 应用中,用户注册或找回密码等功能依赖短信验证码进行身份验证。然而,近期发现短信验证码接口被恶意用户频繁调用,导致大量短信被发送,不仅增加了运营成本,还影响了正常用户的使用体验,甚至可能因触发运营商短信发送限制而导致服务不可用。
二、代码示例
短信发送服务类(有缺陷)
import java.util.random;
public class smsservice {
public void sendsms(string phonenumber) {
// 生成 6 位随机验证码
int code = new random().nextint(900000) + 100000;
system.out.println("向 " + phonenumber + " 发送短信验证码: " + code);
// 实际应用中应调用短信发送 api 发送验证码
}
}
短信验证码接口控制器(有缺陷)
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("短信验证码已发送");
} else {
response.getwriter().println("手机号不能为空");
}
}
}
三、问题描述
- 预期行为:正常用户在需要时能够获取短信验证码,且恶意用户无法通过频繁调用接口来盗刷短信验证码,确保短信发送资源合理使用,服务稳定运行。
- 实际行为:恶意用户可以通过编写自动化脚本,不断调用短信验证码接口,导致大量不必要的短信被发送。这是因为当前接口没有任何限制机制,无论是正常用户还是恶意用户,只要提供了手机号,就可以无限制地获取短信验证码。
四、解决方案
- 增加频率限制:通过记录用户请求频率,限制同一手机号在一定时间内的短信发送次数。可以使用 redis 来存储手机号的发送记录和时间戳。
import redis.clients.jedis.jedis;
import java.util.random;
public class smsservice {
private static final int max_sends_per_minute = 3; // 每分钟最多发送 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); // 设置过期时间为 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 + " 发送短信验证码: " + code);
// 实际应用中应调用短信发送 api 发送验证码
} else {
system.out.println("发送频率过高,请稍后再试");
}
}
}
修改后的短信验证码接口控制器
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("短信验证码已发送");
} else {
response.getwriter().println("手机号不能为空");
}
}
}
- 使用图形验证码:在发送短信验证码之前,要求用户先输入图形验证码。用户只有正确输入图形验证码后,才能请求短信验证码,增加恶意调用的难度。
// 图形验证码生成和验证逻辑(示例代码,实际需更完善实现)
public class captchaservice {
public string generatecaptcha() {
// 生成图形验证码的逻辑,返回验证码字符串
return "1234";
}
public boolean verifycaptcha(string inputcaptcha, string storedcaptcha) {
return inputcaptcha.equals(storedcaptcha);
}
}
再次修改后的短信验证码接口控制器
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"; // 实际应用中应从 session 或其他存储中获取
if (captchaservice.verifycaptcha(inputcaptcha, storedcaptcha)) {
smsservice.sendsms(phonenumber);
response.getwriter().println("短信验证码已发送");
} else {
response.getwriter().println("图形验证码错误");
}
} else {
response.getwriter().println("手机号不能为空");
}
}
}
- ip 访问限制:记录请求的 ip 地址,限制同一 ip 在一定时间内对短信验证码接口的访问次数。同样可以使用 redis 来实现。
import redis.clients.jedis.jedis;
import java.util.random;
public class smsservice {
private static final int max_requests_per_ip_per_minute = 10; // 每分钟每个 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); // 设置过期时间为 1 分钟
}
return count <= max_requests_per_ip_per_minute;
}
// 其他方法不变
}
最终修改后的短信验证码接口控制器
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"; // 实际应用中应从 session 或其他存储中获取
if (captchaservice.verifycaptcha(inputcaptcha, storedcaptcha) && smsservice.cansendsmsfromip(clientip)) {
smsservice.sendsms(phonenumber);
response.getwriter().println("短信验证码已发送");
} else if (!captchaservice.verifycaptcha(inputcaptcha, storedcaptcha)) {
response.getwriter().println("图形验证码错误");
} else {
response.getwriter().println("请求频率过高,请稍后再试");
}
} else {
response.getwriter().println("手机号不能为空");
}
}
}
到此这篇关于java 防止短信验证码接口被盗刷问题解决的文章就介绍到这了,更多相关java 防止短信验证码接口被盗刷内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论