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

一、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 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长1 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn1 小时前
Spring 注解使用详解
java·spring
一条小小yu1 小时前
Spring IoC的理解
java·后端·spring
落魄实习生2 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈2 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
自强的小白3 小时前
核心功能(Service接口)
java·mybatis
乌暮4 小时前
深入理解 Java 泛型:把「万能盒子」用对、用稳
java·开发语言·后端·学习
大猫和小黄5 小时前
深入理解 Java Optional:从设计哲学到工程实战
java