当前位置: 代码网 > it编程>编程语言>Java > Java实现短信验证码功能的完整代码

Java实现短信验证码功能的完整代码

2025年12月16日 Java 我要评论
一、被盗刷的惨状:验证码的“春运”现场想象一下这个场景:你的短信验证码接口就像双十一的购物车,一群“羊毛党”开着机器人拖拉机,以每秒100次的速度疯狂点

一、被盗刷的惨状:验证码的“春运”现场

想象一下这个场景:你的短信验证码接口就像双十一的购物车,一群“羊毛党”开着机器人拖拉机,以每秒100次的速度疯狂点击“发送验证码”按钮。你的短信费就像漏气的气球一样瘪下去,而真正的用户却收不到验证码,急得像热锅上的蚂蚁。

更可怕的是,可能:

  • 用你的钱给隔壁老王发“我爱你”短信
  • 测试出所有已注册手机号(撞库攻击)
  • 让你的服务器累到怀疑人生,直接躺平(ddos)

二、防御战术大全:给接口装上“金钟罩”

1.频率限制:给“点击狂魔”戴上手铐

import com.google.common.cache.cache;
import com.google.common.cache.cachebuilder;
import java.util.concurrent.timeunit;
import java.util.concurrent.atomic.atomicinteger;

/**
 * 短信卫士 - 专治各种手速过快
 */
public class smsguard {
    
    // 使用guava cache存储访问频率
    private static final cache<string, atomicinteger> ip_cache = 
        cachebuilder.newbuilder()
            .expireafterwrite(1, timeunit.hours)
            .build();
    
    private static final cache<string, atomicinteger> phone_cache = 
        cachebuilder.newbuilder()
            .expireafterwrite(1, timeunit.hours)
            .build();
    
    /**
     * 检查这个ip是不是在开挂
     * @param ip 客户端ip
     * @param maxattempts 最大尝试次数(比如1小时10次)
     * @return true=正常用户,false=疑似黑客
     */
    public static boolean isipallowed(string ip, int maxattempts) {
        try {
            atomicinteger counter = ip_cache.get(ip, () -> new atomicinteger(0));
            int attempts = counter.incrementandget();
            
            if (attempts > maxattempts) {
                system.out.println("检测到ip " + ip + " 疑似开挂,已拦截!");
                return false;
            }
            return true;
        } catch (exception e) {
            return false; // 出错时保守一点,拒绝访问
        }
    }
    
    /**
     * 检查这个手机号是不是在刷验证码
     * @param phone 手机号
     * @param maxsmsperhour 每小时最多发几条
     * @return true=可以发,false=发太多了
     */
    public static boolean isphoneallowed(string phone, int maxsmsperhour) {
        try {
            atomicinteger counter = phone_cache.get(phone, () -> new atomicinteger(0));
            int sentcount = counter.incrementandget();
            
            if (sentcount > maxsmsperhour) {
                system.out.println("手机号 " + phone + " 今天已经收到" + sentcount + "条验证码,让它歇会儿吧");
                return false;
            }
            return true;
        } catch (exception e) {
            return false;
        }
    }
}

2.图形验证码:让机器人“看图说话”

import javax.imageio.imageio;
import java.awt.*;
import java.awt.image.bufferedimage;
import java.io.bytearrayoutputstream;
import java.util.random;

/**
 * 验证码生成器 - 专治眼瞎的机器人
 */
public class captchagenerator {
    
    /**
     * 生成能让机器人怀疑人生的验证码
     * @return [0]=图片base64, [1]=验证码答案
     */
    public static string[] generatecaptcha() {
        int width = 120;
        int height = 40;
        
        // 创建一张让机器人哭泣的图片
        bufferedimage image = new bufferedimage(width, height, bufferedimage.type_int_rgb);
        graphics2d g = image.creategraphics();
        
        // 设置背景色(随机浅色)
        g.setcolor(getrandomlightcolor());
        g.fillrect(0, 0, width, height);
        
        // 画干扰线(让机器人眼花缭乱)
        g.setcolor(color.black);
        random random = new random();
        for (int i = 0; i < 10; i++) {
            int x1 = random.nextint(width);
            int y1 = random.nextint(height);
            int x2 = random.nextint(width);
            int y2 = random.nextint(height);
            g.drawline(x1, y1, x2, y2);
        }
        
        // 生成随机验证码(避开容易混淆的字符)
        string chars = "abcdefghjklmnpqrstuvwxyz23456789";
        stringbuilder captchatext = new stringbuilder();
        for (int i = 0; i < 4; i++) {
            char c = chars.charat(random.nextint(chars.length()));
            captchatext.append(c);
            
            // 扭曲、旋转、变色 - 三连击!
            g.setfont(new font("arial", font.bold | font.italic, 30 + random.nextint(5)));
            g.setcolor(getrandomdarkcolor());
            
            // 轻微旋转字符
            double theta = random.nextdouble() * 0.5 - 0.25;
            g.rotate(theta, 20 + i * 25, 25);
            g.drawstring(string.valueof(c), 20 + i * 25, 25);
            g.rotate(-theta, 20 + i * 25, 25);
        }
        
        g.dispose();
        
        try {
            // 转换为base64
            bytearrayoutputstream baos = new bytearrayoutputstream();
            imageio.write(image, "png", baos);
            string base64image = java.util.base64.getencoder().encodetostring(baos.tobytearray());
            
            return new string[]{"data:image/png;base64," + base64image, captchatext.tostring()};
        } catch (exception e) {
            throw new runtimeexception("验证码生成失败", e);
        }
    }
    
    private static color getrandomlightcolor() {
        random random = new random();
        return new color(200 + random.nextint(55), 
                        200 + random.nextint(55), 
                        200 + random.nextint(55));
    }
    
    private static color getrandomdarkcolor() {
        random random = new random();
        return new color(random.nextint(150), 
                        random.nextint(150), 
                        random.nextint(150));
    }
}

3.滑动验证码:让机器人“学走路”

import java.util.uuid;
import java.util.concurrent.concurrenthashmap;

/**
 * 滑动验证码 - 专治不会用鼠标的机器人
 */
public class slidecaptchaservice {
    
    // 存储验证会话
    private static final concurrenthashmap<string, slidecaptchadata> sessions = 
        new concurrenthashmap<>();
    
    /**
     * 生成滑动验证码挑战
     */
    public static slidechallenge generatechallenge() {
        string sessionid = uuid.randomuuid().tostring();
        
        // 随机生成目标位置(这里简化了,实际应该有图片处理)
        int targetx = 100 + new random().nextint(200);
        int targety = 50 + new random().nextint(100);
        
        slidecaptchadata data = new slidecaptchadata(targetx, targety);
        sessions.put(sessionid, data);
        
        // 设置5分钟过期
        new timer().schedule(new timertask() {
            @override
            public void run() {
                sessions.remove(sessionid);
            }
        }, 5 * 60 * 1000);
        
        return new slidechallenge(sessionid, targetx, targety);
    }
    
    /**
     * 验证滑动结果
     */
    public static boolean verify(string sessionid, int userx, int usery) {
        slidecaptchadata data = sessions.get(sessionid);
        if (data == null) {
            return false; // 会话过期
        }
        
        // 允许±5像素的误差(人类手抖,机器人太精确反而可疑)
        boolean isvalid = math.abs(userx - data.targetx) <= 5 && 
                         math.abs(usery - data.targety) <= 5;
        
        if (isvalid) {
            sessions.remove(sessionid); // 一次性使用
        }
        
        return isvalid;
    }
    
    static class slidecaptchadata {
        int targetx;
        int targety;
        
        slidecaptchadata(int targetx, int targety) {
            this.targetx = targetx;
            this.targety = targety;
        }
    }
    
    static class slidechallenge {
        string sessionid;
        int targetx;
        int targety;
        
        slidechallenge(string sessionid, int targetx, int targety) {
            this.sessionid = sessionid;
            this.targetx = targetx;
            this.targety = targety;
        }
    }
}

4.完整的短信发送服务

import org.springframework.beans.factory.annotation.autowired;
import org.springframework.data.redis.core.redistemplate;
import org.springframework.stereotype.service;
import java.util.concurrent.timeunit;

/**
 * 短信发送服务 - 武装到牙齿的版本
 */
@service
public class smsservice {
    
    @autowired
    private redistemplate<string, string> redistemplate;
    
    @autowired
    private riskcontrolservice riskcontrolservice;
    
    /**
     * 发送验证码(安全加强版)
     */
    public apiresponse sendverificationcode(string phone, string ip, string captchacode, string sessionid) {
        
        // 1. 检查ip风险
        if (!riskcontrolservice.checkiprisk(ip)) {
            return apiresponse.error("您的网络环境存在风险,请稍后再试");
        }
        
        // 2. 验证图形验证码(如果有)
        if (captchacode != null && !validatecaptcha(sessionid, captchacode)) {
            return apiresponse.error("验证码错误,请重新输入");
        }
        
        // 3. 频率控制:同一手机号1分钟内只能发1次
        string minutekey = "sms:minute:" + phone;
        if (boolean.true.equals(redistemplate.haskey(minutekey))) {
            return apiresponse.error("操作过于频繁,请1分钟后再试");
        }
        
        // 4. 频率控制:同一手机号1小时内最多5次
        string hourkey = "sms:hour:" + phone;
        long hourcount = redistemplate.opsforvalue().increment(hourkey);
        if (hourcount != null && hourcount == 1) {
            redistemplate.expire(hourkey, 1, timeunit.hours);
        }
        if (hourcount != null && hourcount > 5) {
            return apiresponse.error("今日验证码发送次数已达上限");
        }
        
        // 5. 生成6位随机验证码
        string code = string.format("%06d", new random().nextint(999999));
        
        // 6. 存储验证码(5分钟过期)
        string codekey = "sms:code:" + phone;
        redistemplate.opsforvalue().set(codekey, code, 5, timeunit.minutes);
        
        // 7. 设置1分钟冷却期
        redistemplate.opsforvalue().set(minutekey, "1", 1, timeunit.minutes);
        
        // 8. 记录发送日志(用于分析)
        logsmssent(phone, ip, code);
        
        // 9. 调用第三方短信服务(实际发送)
        boolean sent = realsendsms(phone, code);
        
        if (sent) {
            // 10. 返回脱敏的手机号
            string maskedphone = phone.substring(0, 3) + "****" + phone.substring(7);
            return apiresponse.success("验证码已发送至" + maskedphone);
        } else {
            return apiresponse.error("短信发送失败,请稍后重试");
        }
    }
    
    /**
     * 验证短信验证码
     */
    public boolean verifycode(string phone, string usercode) {
        string codekey = "sms:code:" + phone;
        string correctcode = redistemplate.opsforvalue().get(codekey);
        
        if (correctcode == null) {
            return false; // 验证码已过期
        }
        
        // 验证成功后删除验证码(防止重复使用)
        boolean isvalid = correctcode.equals(usercode);
        if (isvalid) {
            redistemplate.delete(codekey);
        }
        
        return isvalid;
    }
    
    private void logsmssent(string phone, string ip, string code) {
        // 这里应该记录到数据库或日志系统
        system.out.println(string.format(
            "短信发送日志: phone=%s, ip=%s, code=%s, time=%s",
            phone, ip, code, new java.util.date()
        ));
    }
    
    private boolean realsendsms(string phone, string code) {
        // 调用真实的短信服务商接口
        // 这里简化处理,实际应该用http客户端调用
        try {
            system.out.println(string.format(
                "发送短信到 %s: 您的验证码是%s,5分钟内有效,打死也不要告诉别人哦!",
                phone, code
            ));
            return true;
        } catch (exception e) {
            return false;
        }
    }
}

5.风控服务:火眼金睛识破坏人

/**
 * 风控服务 - 专治各种不服
 */
@service
public class riskcontrolservice {
    
    @autowired
    private redistemplate<string, string> redistemplate;
    
    /**
     * 综合风险评估
     */
    public risklevel assessrisk(string phone, string ip, string useragent) {
        int riskscore = 0;
        
        // 1. ip地址检查
        if (issuspiciousip(ip)) {
            riskscore += 30;
        }
        
        // 2. user-agent检查
        if (issuspicioususeragent(useragent)) {
            riskscore += 20;
        }
        
        // 3. 请求频率检查
        if (ishighfrequency(ip)) {
            riskscore += 40;
        }
        
        // 4. 手机号归属地 vs ip归属地
        if (!islocationconsistent(phone, ip)) {
            riskscore += 20;
        }
        
        // 5. 历史行为检查
        if (hasbadhistory(ip)) {
            riskscore += 50;
        }
        
        // 根据分数返回风险等级
        if (riskscore >= 80) {
            return risklevel.high;
        } else if (riskscore >= 50) {
            return risklevel.medium;
        } else {
            return risklevel.low;
        }
    }
    
    /**
     * 检查ip风险
     */
    public boolean checkiprisk(string ip) {
        string key = "risk:ip:" + ip;
        long count = redistemplate.opsforvalue().increment(key);
        
        if (count == 1) {
            redistemplate.expire(key, 1, timeunit.hours);
        }
        
        // 1小时内超过50次请求视为风险
        return count == null || count <= 50;
    }
    
    enum risklevel {
        low,    // 低风险:正常通过
        medium, // 中风险:需要额外验证
        high    // 高风险:直接拒绝
    }
    
    // 其他检测方法...
}

三、防御体系总结:打造铁桶阵

多层防御体系

第一层:前端防护

  • 图形验证码(专治简单机器人)
  • 滑动验证码(专治中级机器人)
  • 点击按钮防重放(防止连续点击)

第二层:频率限制

  • ip级别限流(防止单一ip攻击)
  • 手机号级别限流(防止针对特定号码)
  • 设备指纹限流(更精准的识别)

第三层:行为分析

  • 请求时间分布分析(机器人请求太规律)
  • 鼠标轨迹分析(机器人不会手抖)
  • 操作间隔分析(机器人反应太快)

第四层:业务逻辑

  • 验证码有效期控制(通常5分钟)
  • 验证码一次性使用(用后即焚)
  • 错误次数限制(防止破解)

监控与预警

/**
 * 监控服务 - 短信接口的"心电图"
 */
@service
public class smsmonitorservice {
    
    // 关键指标监控
    public void monitormetrics() {
        // 1. 成功率监控
        // 2. 响应时间监控
        // 3. 异常请求监控
        // 4. 费用消耗监控
        
        // 发现异常立即告警
        // - 短信量突增
        // - 成功率突降
        // - 特定ip大量请求
    }
    
    /**
     * 自动熔断机制
     */
    public void circuitbreaker(string phoneprefix) {
        // 如果某个号段异常,自动临时屏蔽
        // 比如:170号段被大量攻击,自动限制该号段
    }
}

实践建议

  • 按需发送:只有必要的时候才发验证码,比如注册、登录、支付
  • 内容脱敏:短信中不要包含完整手机号
  • 成本控制:设置每日、每月短信预算上限
  • 验证码复杂度:6位数字足够,别搞太复杂
  • 失败处理:失败时给出友好提示,但不要泄露细节
  • 定期审计:定期检查日志,发现异常模式

四、道高一尺,魔高一丈

安全是一场持久战。今天防住了普通机器人,明天可能就有高级ai来挑战。关键在于:

  • 不要依赖单一防御:多层防御才靠谱
  • 保持更新:安全方案需要与时俱进
  • 监控报警:早发现、早处理、早止损
  • 成本意识:既要安全,也要考虑用户体验和实现成本

最最重要的是:不要把验证码接口当成公共厕所,谁想来就来! 给它装上门禁、摄像头、保安,还要收门票(验证手段),这样才能让羊毛党知难而退。

以上就是java实现短信验证码功能的完整代码的详细内容,更多关于java短信验证码的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com