buguge - Keep it simple,stupid

知识就是力量,但更重要的,是运用知识的能力why buguge?

导航

开发者暴露了一个无需授权访问的裸接口,我问:如果有人暴力请求怎么办?

如下方法,@UnAuthToken 注解代表无需授权访问。

这个方法逻辑很简单,入参“key”是一个md5串,程序根据这个“key”去redis拿到数据,经过转换后返回。

每次调用这个方法,意味着 ①会调redis ②会记操作日志

/**
 * 企业首页忘记登录密码通过key获取当前用户名
 *
 * @return
 */
@ApiOperation(value = "企业首页忘记登录密码 通过key获取当前用户名", notes = "企业首页忘记登录密码通过key获取当前用户名")
@RequestMapping(value = "/getLoginAccByKey", method = RequestMethod.GET)
@UnAuthToken
public Result<String> getLoginAccByKey(String key) {
    Object loginAccInfo = redisUtil.get(CommonConstant.PREFIX_PASS_SIGN+key);
    if (null == loginAccInfo){
        log.info("密码找回,sign:{},存储信息不存在",key);
        return Result.error("校验失败");
    }
    //索引下表存储信息 0 登录名  1 修改类型 2 当前企业ID
    String[] arr = loginAccInfo.toString().split(",");
    String loginAcc = arr[0];
    return Result.success(loginAcc);
}

那么,如果有人通过for循环多线程恶意频繁调用的话,可能就会大量地消耗redis资源。 怎么对这个方法做安全加固呢?

要防止这种恶意高频调用导致Redis资源过载,可以从如下几点入手,进行安全加固。(首先要保证的是操作日志异步化。“记操作日志”不是核心逻辑,将其异步化可以显著提升接口抗压能力。)

方案1:基础防护 - 参数校验与业务逻辑加固

这是最简单、最应立即实施的防护。

@ApiOperation(value = "企业首页忘记登录密码 通过key获取当前用户名", notes = "企业首页忘记登录密码通过key获取当前用户名")
@RequestMapping(value = "/getLoginAccByKey", method = RequestMethod.GET)
@UnAuthToken
public Result<String> getLoginAccByKey(String key) {
    // 增加基础参数校验
    if (StringUtils.isBlank(key) || key.length() != 32) { // MD5通常为32位
        log.warn("密码找回,收到非法key格式: {}", key);
        return Result.error("参数错误");
    }
    // 可选:增加格式校验(如是否纯16进制数)
    if (!key.matches("^[a-fA-F0-9]{32}$")) {
        log.warn("密码找回,收到非MD5格式key: {}", key);
        return Result.error("参数错误");
    }

    // ... 原有业务逻辑 ...
}

方案2:限流 - 阻止高频调用

这是防御恶意循环攻击最核心的手段。可在应用层网关层实现。

因为我们解决的redis资源过载的问题,所以注意不是用redis做限流器,而要用本地化限流器。

方案2A:使用Spring Boot + Guava RateLimiter(简单)

@Component
public class ApiLimitService {
    // 针对每个IP的限流器,每秒最多5次访问
    private final LoadingCache<String, RateLimiter> ipLimiters = CacheBuilder.newBuilder()
            .expireAfterAccess(1, TimeUnit.HOURS)
            .build(new CacheLoader<String, RateLimiter>() {
                @Override
                public RateLimiter load(String ip) {
                    return RateLimiter.create(5.0); // 每秒5个令牌
                }
            });

    public boolean tryAccess(String ip) {
        RateLimiter limiter = ipLimiters.getUnchecked(ip);
        return limiter.tryAcquire();
    }
}

// 在Controller中使用
@RestController
public class YourController {
    @Autowired
    private ApiLimitService apiLimitService;
    
    @ApiOperation(value = "企业首页忘记登录密码 通过key获取当前用户名", notes = "企业首页忘记登录密码通过key获取当前用户名")
    @RequestMapping(value = "/getLoginAccByKey", method = RequestMethod.GET)
    @UnAuthToken
    public Result<String> getLoginAccByKey(String key, HttpServletRequest request) {
        // 获取客户端IP(注意处理反向代理)
        String clientIp = getClientIp(request);
        
        // 限流检查
        if (!apiLimitService.tryAccess(clientIp)) {
            log.warn("密码找回接口触发限流,IP: {}, key:{}", clientIp, key);
            return Result.error("请求过于频繁,请稍后再试");
        }
        
        // ... 原有业务逻辑 ...
    }
    
    private String getClientIp(HttpServletRequest request) {
        // 从请求头中获取真实IP(如果有反向代理)
        String ip = request.getHeader("X-Forwarded-For");
        if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

方案2B:使用Spring Boot Actuator + Micrometer(推荐,功能强大)

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: prometheus
  metrics:
    export:
      prometheus:
        enabled: true

spring:
  cloud:
    gateway:
      routes:
        - id: getLoginAccByKey_route
          uri: ${your.service.uri}
          predicates:
            - Path=/api/getLoginAccByKey
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 5   # 每秒5个请求
                redis-rate-limiter.burstCapacity: 10  # 峰值10个请求
                key-resolver: "#{@ipKeyResolver}"     # 按IP限流

方案2C:使用注解式限流(优雅)

// 自定义限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int value() default 5; // 默认每秒5次
    String key() default ""; // 限流key,支持SpEL,如 "#key"
}

// AOP实现
@Aspect
@Component
public class RateLimitAspect {
    // 存储限流器,key为 方法名:参数
    private final ConcurrentHashMap<String, RateLimiter> limiters = new ConcurrentHashMap<>();
    
    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();
        
        // 构造限流器的key(可按方法+IP,或方法+业务key进行更细粒度控制)
        String limitKey = methodName + ":" + getCurrentIp();
        
        RateLimiter limiter = limiters.computeIfAbsent(limitKey, 
            k -> RateLimiter.create(rateLimit.value()));
            
        if (!limiter.tryAcquire()) {
            throw new RuntimeException("请求过于频繁,请稍后再试");
        }
        return joinPoint.proceed();
    }
}

// 在Controller方法上使用
@ApiOperation(value = "企业首页忘记登录密码 通过key获取当前用户名", notes = "企业首页忘记登录密码通过key获取当前用户名")
@RequestMapping(value = "/getLoginAccByKey", method = RequestMethod.GET)
@UnAuthToken
@RateLimit(value = 3) // 该方法每秒最多调用3次
public Result<String> getLoginAccByKey(String key) {
    // ... 业务逻辑 ...
}

方案3:借助本地缓存,提升Redis访问效率与韧性

// 在Service层优化
@Service
public class PasswordRecoveryService {
    
    // 1. 引入本地缓存(Caffeine),防止短时间内相同key重复击穿Redis
    private final Cache<String, String> localCache = Caffeine.newBuilder()
            .expireAfterWrite(30, TimeUnit.SECONDS) // 本地缓存30秒
            .maximumSize(1000)
            .build();
    
    public Result<String> getLoginAccByKey(String key) {
        // 先查本地缓存
        String cachedLoginAcc = localCache.getIfPresent(key);
        if (cachedLoginAcc != null) {
            return Result.success(cachedLoginAcc);
        }
        
        // 2. Redis查询使用pipeline(如果需要批量查询时)
        // 3. 或使用Redis集群,分散压力
        // ... 原有Redis查询逻辑 ...
        
        // 将结果放入本地缓存
        if (loginAcc != null) {
            localCache.put(key, loginAcc);
        }
        
        return Result.success(loginAcc);
    }
}

方案4:防御性编程 - 增加请求难度

对于特别敏感的接口,可以增加一些“无害”的验证步骤,提高攻击成本。

public Result<String> getLoginAccByKey(String key, 
                                     @RequestParam(required = false) String timestamp,
                                     @RequestParam(required = false) String nonce,
                                     HttpServletRequest request) {
    
    // 1. 简单的时效性校验(防止重放)
    if (StringUtils.isNotBlank(timestamp)) {
        long ts = Long.parseLong(timestamp);
        if (Math.abs(System.currentTimeMillis() - ts) > 5 * 60 * 1000) { // 5分钟有效期
            return Result.error("请求已过期");
        }
    }
    
    // 2. 简单的签名校验(增加构造请求的难度)
    // 可以要求前端用固定规则生成一个sign,服务端校验
    // 即使算法简单,也能阻挡大部分无脑脚本攻击
    
    // ... 原有业务逻辑 ...
}

你问AI的话,它可能还会给你架构层面的建议

  1. API网关层统一防护:在Nginx或Spring Cloud Gateway等网关层实施全局限流、黑名单机制。
  2. WAF(Web应用防火墙):如果有条件,启用WAF的CC攻击防护规则。
  3. Redis监控与告警:监控Redis的QPS、连接数等指标,设置阈值告警。

posted on 2026-04-20 21:50  buguge  阅读(15)  评论(0)    收藏  举报