防止短信验证码接口被盗刷问题

一、Bug 场景

在一个基于 Java 的 Web 应用中,用户注册或找回密码等功能依赖短信验证码进行身份验证。然而,近期发现短信验证码接口被恶意用户频繁调用,导致大量短信被发送,不仅增加了运营成本,还影响了正常用户的使用体验,甚至可能因触发运营商短信发送限制而导致服务不可用。

二、代码示例

短信发送服务类(有缺陷)

java 复制代码
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 发送验证码
    }
}

短信验证码接口控制器(有缺陷)

java 复制代码
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("手机号不能为空");
        }
    }
}

三、问题描述

  1. 预期行为:正常用户在需要时能够获取短信验证码,且恶意用户无法通过频繁调用接口来盗刷短信验证码,确保短信发送资源合理使用,服务稳定运行。
  2. 实际行为:恶意用户可以通过编写自动化脚本,不断调用短信验证码接口,导致大量不必要的短信被发送。这是因为当前接口没有任何限制机制,无论是正常用户还是恶意用户,只要提供了手机号,就可以无限制地获取短信验证码。

四、解决方案

  1. 增加频率限制:通过记录用户请求频率,限制同一手机号在一定时间内的短信发送次数。可以使用 Redis 来存储手机号的发送记录和时间戳。
java 复制代码
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("发送频率过高,请稍后再试");
        }
    }
}

修改后的短信验证码接口控制器

java 复制代码
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("手机号不能为空");
        }
    }
}
  1. 使用图形验证码:在发送短信验证码之前,要求用户先输入图形验证码。用户只有正确输入图形验证码后,才能请求短信验证码,增加恶意调用的难度。
java 复制代码
// 图形验证码生成和验证逻辑(示例代码,实际需更完善实现)
public class CaptchaService {
    public String generateCaptcha() {
        // 生成图形验证码的逻辑,返回验证码字符串
        return "1234";
    }

    public boolean verifyCaptcha(String inputCaptcha, String storedCaptcha) {
        return inputCaptcha.equals(storedCaptcha);
    }
}

再次修改后的短信验证码接口控制器

java 复制代码
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("手机号不能为空");
        }
    }
}
  1. IP 访问限制:记录请求的 IP 地址,限制同一 IP 在一定时间内对短信验证码接口的访问次数。同样可以使用 Redis 来实现。
java 复制代码
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;
    }

    // 其他方法不变
}

最终修改后的短信验证码接口控制器

java 复制代码
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("手机号不能为空");
        }
    }
}
相关推荐
一嘴一个橘子1 分钟前
mybatis - 动态语句、批量注册mapper、分页插件
java
组合缺一2 分钟前
Json Dom 怎么玩转?
java·json·dom·snack4
危险、19 分钟前
一套提升 Spring Boot 项目的高并发、高可用能力的 Cursor 专用提示词
java·spring boot·提示词
kaico201823 分钟前
JDK11新特性
java
钊兵24 分钟前
java实现GeoJSON地理信息对经纬度点的匹配
java·开发语言
jiayong2328 分钟前
Tomcat性能优化面试题
java·性能优化·tomcat
秋刀鱼程序编程32 分钟前
Java基础入门(五)----面向对象(上)
java·开发语言
纪莫1 小时前
技术面:MySQL篇(InnoDB的锁机制)
java·数据库·java面试⑧股
Remember_9931 小时前
【LeetCode精选算法】滑动窗口专题二
java·开发语言·数据结构·算法·leetcode
Filotimo_1 小时前
在java开发中,cron表达式概念
java·开发语言·数据库