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

一、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("手机号不能为空");
        }
    }
}
相关推荐
zhz5214几秒前
Spring Boot 接入国密实战:传输加密(TLCP)+ 密码加密(SM4)
java·spring boot·后端·国密·sm4
人道领域4 分钟前
【LeetCode刷题日记】617.合并二叉树(空间换安全,还是原地省内存)
java·数据结构·算法·leetcode
独自破碎E8 分钟前
机器人Java后端算法笔试题解析
java·windows·算法
我是一颗柠檬8 分钟前
【JDK8新特性】函数式接口Day2
java·开发语言·后端·intellij-idea
Bat U9 分钟前
JavaEE|JVM
java·jvm·java-ee
Mahir0811 分钟前
Spring Boot 自动装配深度解密:从原理到自定义 Starter 实战
java·spring boot·后端·自动装配·自定义starter·大厂面试题
淘源码d12 分钟前
产科系统源码,数字产科源码,Java(后端) + Vue + ElementUI(前端) + MySQL(数据库),确保系统稳定性与扩展性。
java·源码·数字产科·产科系统·智能化孕产服务·高危五色预警·智慧产科
wand codemonkey1 小时前
SpringbootWeb【入门】+MySQL【安装】+【DataDrip安装 】+【连接MySQL】
java·mysql·mybatis
Mahir089 小时前
Spring 循环依赖深度解密:从问题本质到三级缓存源码级解析
java·后端·spring·缓存·面试·循环依赖·三级缓存
RyFit10 小时前
SpringAI 常见问题及解决方案大全
java·ai