主要目的
保护系统资源、保障用户体验和确保业务安全
存在的场景
- 爬虫
大量抓取数据,耗尽服务器资源
- 暴力破解
针对登录接口高频尝试密码
- CC攻击
应用层DDoS,耗尽业务资源
- 业务滥用
如秒杀场景下的机器抢单
应对的策略
请求合法性校验
服务端处理请求之前,验证该请求是否来自可信的客户端、是否完整未被篡改、是否在有效期内的一系列措施。 它是接口防刷的第一道防线,用于拦截伪造、重放、篡改的恶意请求
-
API签名校验
// 客户端生成签名(伪代码) String secret = "your-secret"; String data = "param1=value1¶m2=value2"; long timestamp = System.currentTimeMillis(); String nonce = UUID.randomUUID().toString(); String sign = HmacSHA256(data + timestamp + nonce, secret);
// 服务端校验拦截器 public boolean preHandle(HttpServletRequest request) { String sign = request.getHeader("X-Sign"); long timestamp = Long.parseLong(request.getHeader("X-Timestamp")); String nonce = request.getHeader("X-Nonce"); String body = getRequestBody(request);
// 1. 时间戳检查 if (Math.abs(System.currentTimeMillis() - timestamp) > 5 * 60 * 1000) { return false; // 请求过期 } // 2. Nonce检查(Redis) String nonceKey = "nonce:" + nonce; if (redisTemplate.hasKey(nonceKey)) { return false; // 重复请求 } redisTemplate.opsForValue().set(nonceKey, "1", 5, TimeUnit.MINUTES); // 3. 重新计算签名 String serverSign = HmacSHA256(body + timestamp + nonce, getSecretByAppKey(appKey)); if (!serverSign.equals(sign)) { return false; // 签名错误 } return true;}
-
身份认证(JWT校验示例)
@GetMapping("/user/info") public Result getUserInfo(@RequestHeader("Authorization") String token) { try { // 解析JWT,验证签名和过期时间 Claims claims = Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token.replace("Bearer ", "")) .getBody(); String userId = claims.getSubject(); // 业务处理 } catch (Exception e) { return Result.error(401, "无效token"); } }
限流
-
基于redis demo
@Component public class RateLimitInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate;
// 加载Lua脚本 private DefaultRedisScript<Long> redisScript; @PostConstruct public void init() { redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("ratelimit.lua"))); redisScript.setResultType(Long.class); } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String ip = getClientIp(request); String key = "rate:ip:" + ip; long max = 10; // 10次 long period = 60; // 60秒 Long result = redisTemplate.execute(redisScript, Arrays.asList(key), String.valueOf(max), String.valueOf(period)); if (result == null || result == 0) { response.setStatus(429); response.getWriter().write("Too Many Requests"); return false; } return true; } private String getClientIp(HttpServletRequest request) { // 从x-forwarded-for等头获取真实IP String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr(); return ip; }}
验证码机制
验证码(CAPTCHA)是“区分计算机和人类的公开全自动图灵测试”的缩写
-
防止恶意程序自动提交表单(如注册、登录、评论)
-
阻止暴力破解密码、刷票、爬虫等自动化攻击
-
在触发限流策略后,作为二次验证手段,确认操作由真实用户发起
-
配置Kaptcha(生成图形验证码)
@Configuration public class KaptchaConfig { @Bean public Producer kaptchaProducer() { Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise"); properties.setProperty("kaptcha.obscurificator.impl", "com.google.code.kaptcha.impl.WaterRipple"); properties.setProperty("kaptcha.background.clear.from", "255,255,255"); properties.setProperty("kaptcha.background.clear.to", "255,255,255"); DefaultKaptcha kaptcha = new DefaultKaptcha(); kaptcha.setConfig(new Config(properties)); return kaptcha; } }
-
生成验证码接口(Controller)
@RestController public class CaptchaController { @Autowired private Producer captchaProducer; @Autowired private RedisTemplate<String, String> redisTemplate;
@GetMapping("/captcha") public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException { // 生成验证码文本 String captchaText = captchaProducer.createText(); // 生成验证码图片 BufferedImage image = captchaProducer.createImage(captchaText); // 存储到Redis(使用唯一标识,如UUID或手机号) String captchaKey = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("captcha:" + captchaKey, captchaText, 5, TimeUnit.MINUTES); // 将key返回给前端(可通过响应头或JSON) response.setHeader("Captcha-Key", captchaKey); // 输出图片 response.setContentType("image/jpeg"); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpg", out); out.close(); }}
黑/白名单
在接口防刷体系中,黑/白名单是一种基于来源(IP、用户ID、设备指纹等)的快速过滤机制
- 名单格式
IP黑/白名单:支持单个IP、IP段(CIDR,如192.168.1.0/24)、通配符(如192.168..)
用户ID黑/白名单:针对已登录用户的UID进行控制
设备指纹:针对移动端设备ID(如IMEI、IDFA)进行封禁
@Component
public class BlackWhiteListInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// IP工具:判断IP是否在某个CIDR网段内
private boolean ipMatchesCidr(String ip, String cidr) {
try {
SubnetUtils subnet = new SubnetUtils(cidr);
return subnet.getInfo().isInRange(ip);
} catch (Exception e) {
return false; // 格式错误视为不匹配
}
}
// 检查是否在白名单中
private boolean isInWhitelist(String ip) {
Set<String> whitelist = redisTemplate.opsForSet().members("whitelist:ip");
if (whitelist == null || whitelist.isEmpty()) return false;
for (String pattern : whitelist) {
if (pattern.contains("/")) { // CIDR网段
if (ipMatchesCidr(ip, pattern)) return true;
} else if (pattern.contains("*")) { // 通配符简单处理(如192.168.*.*)
// 可将通配符转换为正则,此处简化
String regex = pattern.replace(".", "\\.").replace("*", "\\d+");
if (ip.matches(regex)) return true;
} else { // 精确IP
if (pattern.equals(ip)) return true;
}
}
return false;
}
// 检查是否在黑名单中
private boolean isInBlacklist(String ip) {
// 直接判断Set中是否存在该IP(精确匹配),但IP段需要遍历判断
Set<String> blacklist = redisTemplate.opsForSet().members("blacklist:ip");
if (blacklist == null || blacklist.isEmpty()) return false;
for (String pattern : blacklist) {
if (pattern.contains("/")) {
if (ipMatchesCidr(ip, pattern)) return true;
} else if (pattern.contains("*")) {
String regex = pattern.replace(".", "\\.").replace("*", "\\d+");
if (ip.matches(regex)) return true;
} else {
if (pattern.equals(ip)) return true;
}
}
return false;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String ip = getClientIp(request);
// 1. 白名单检查:如果命中,直接放行
if (isInWhitelist(ip)) {
return true;
}
// 2. 黑名单检查:如果命中,拒绝请求
if (isInBlacklist(ip)) {
response.setStatus(403);
response.getWriter().write("Your IP is blocked due to abnormal behavior.");
return false;
}
// 3. 继续后续拦截器(如限流)
return true;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
// 处理多级代理,取第一个非unknown的IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
总结
接口防刷是一个系统工程,需结合多种手段层层设防
- 基础防御:签名、限流、黑白名单
- 动态防御:验证码、行为分析
- 兜底防御:异步队列、熔断降级