一、被盗刷的惨状:验证码的“春运”现场
想象一下这个场景:你的短信验证码接口就像双十一的购物车,一群“羊毛党”开着机器人拖拉机,以每秒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短信验证码的资料请关注代码网其它相关文章!
发表评论