问题
一个安全的接口限流肯定少不了,登录尤其如此。比如验证码发送,验证码验证试错,密码登录试错,这些虽然是不同的业务,但是目的都是一个就是,对于某些用户的某种行为在一段时间内的的允许次数进行限制
对于此我们抽象出来四个东西
- 用户的身份标识 userId
- 行为标识 actionKey
- 时间周期 period
- 允许最大次数 maxCount
接下来我们借助Redis来实现这功能
Redis中有一种数据类型 zset ,简单来说一种set,值唯一,除此之外还多了一个特性,zset结构还有一个score字段,可以对插入的值根据score进行排序,我们可以以此做文章。
首先可以以时间作为score的值,配合设置的时间周期period 来形成一个滑动窗口,然后统计窗口内的key数量,如果数量超过最大限制次数,则进行限制访问;如果小于最大限制次数,则可以继续访问。
@Component
public class SimpleRateLimiter {
@Autowired
private RedisTemplate redisTemplate;
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.currentTimeMillis();
List<Object> result = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations ops) throws DataAccessException {
ops.multi();
// 记录行为
ops.opsForZSet().add(key, "" + nowTs, nowTs);
// 移除窗口前的记录
ops.opsForZSet().removeRangeByScore(key, 0, nowTs - period * 1000 * 60);
// 获取窗口内行为数量
ops.opsForZSet().zCard(key);
// 设置过期时间等于窗口长度
ops.expire(key, period, TimeUnit.MINUTES);
return ops.exec();
}
});
// 获取第三步返回的结果
Long count = (Long) result.get(2);
return count <= maxCount;
}
}