最近做的项目中用到了redis根据ip来限流,防止暴力破解。大致实现思路是,使用ip加密后的字段作为redis的key,过期时间比如设为1分钟,一分钟内该ip每访问一次,value值加一。当value值大于配置文件中配置的访问频率上线值时,返回错误给前端。在filter(也可以在AOP)中加入以下代码,用来做到接口的限流,最终保护你的网站。
@Slf4j
@Component
public class RateLimiterFilter extends OncePerRequestFilter {
private final static List<String> rateLimiterList = new ArrayList<>();
static {
//进行限流的接口
rateLimiterList.add("/api/v1/auth/login");
}
private final RedisService redisService;
//从配置文件中读取限流的频率
@Value("${auth.sms.rate}")
private int rate;
public RateLimiterFilter(RedisService redisService) {
this.redisService = redisService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String url = request.getRequestURI();
if (rateLimiterList.contains(url)) {
String ip = IpUtil.getIp(request);
String key = IpUtil.getMod5Url(url).substring(0, 8) + "-" + ip;
String cnt = redisService.getValue(key);
if (!Strings.isBlank(cnt) && Integer.parseInt(cnt) > rate) {
log.warn("[{}] - [{}] 访问频率上限[%d]次/分钟. key:[{}]", ip, url, key);
PrintWriter out = response.getWriter();
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
//这里返回固定的BaseResponse对象给前端,直接抛异常时发现,在filter中的异常,GlobalExceptionHandler全局异常处理类无法处理
out.print(JSONObject.toJSONString(BaseResponse.with(Code.ACCESS_RATE_LIMIT_REACHED.getCode(), "【请求频繁,请稍后再试】"), SerializerFeature.WriteNullStringAsEmpty));
out.flush();
out.close();
return;
}
redisService.increaseOrExpire(key, 1, TimeUnit.MINUTES);
}
//责任链模式,开发时忘了加下面这行,导致所有请求到这个过滤器就断掉了。。。。
chain.doFilter(request, response);
}
}
下面时redisService的实现。
@Component
@Slf4j
public class RedisServiceImpl implements RedisService {
private final static String PREFIX = "rework-stats-";
private final StringRedisTemplate redisTemplate;
public RedisServiceImpl(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
@Async
public Boolean delete(String key) {
log.debug("RedisUtils delete key:{}", genKey(key));
return redisTemplate.opsForSet().getOperations().delete(genKey(key));
}
@Override
public String getValue(String key) {
return redisTemplate.opsForValue().get(genKey(key));
}
@Override
public void increaseOrExpire(String key, long expire, TimeUnit timeUnit) {
String cnt = getValue(key);
key = genKey(key);
if (cnt != null) {
log.debug("key:{} cnt:{} increment ", key, cnt);
redisTemplate.opsForValue().increment(key);
} else {
redisTemplate.opsForValue().set(key, "1");
redisTemplate.expire(key, expire, timeUnit);
log.debug("create rateLimiter expire:{} - {} - {} ", key, expire, timeUnit);
}
}
/**
* 生成统一规则的前缀key
*
* @param key key值
* @return
*/
private static String genKey(String key) {
StringBuilder ret = new StringBuilder(PREFIX);
ret.append(key);
return ret.toString();
}
}
IpUtil的实现:
public class IpUtil {
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
public static String getMod5Url(String url) {
String ret = null;
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
ret = new BigInteger(1, digest.digest(url.getBytes())).toString(16);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return ret;
}
}
pom.xml 加入spring-boot-starter-data-redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
网友评论