比如给客户端提供一个timestamp参数,值是13位的毫秒级时间戳,可以在第12位或者13位做一个校验位,通过一定的算法给其他12位的值做一个校验。
举例:现在实际时间是 1684059940123,我把前12位通过算法算出来校验位的值是9,参数则把最后一位改成9,即1684059940129,值传到服务端后通过前十二位也可以算出来值,来判断这个时间戳是不是合法的。
生成校验位的算法要确保前后端一致,比如校验位的值都取倒数第二位的值(可过滤掉90%)恶意请求。
其实也就是spring拦截器来实现。在需要防刷的方法上,加上防刷的注解,拦截器拦截这些注解的方法后,进行接口存储到redis中。当用户多次请求时,我们可以累积他的请求次数,达到了上限,我们就可以给他提示错误信息。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimit {
int seconds();
int maxCount();
}
public class SessionInterceptor extends HandlerInterceptorAdapter {
private static final Logger logger = LoggerFactory.getLogger(SessionInterceptor.class);
@Autowired
private UserAuthService userAuthService;
@Autowired
private UserContext userContext;
@Autowired
private RedisUtil redisUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String session = request.getHeader("Authorization");
if (StringUtils.isEmpty(session)) {
throw new UserException(ReturnCode.PARAMETERS_ERROR, "缺少session");
}
HandlerMethod hm = (HandlerMethod) handler;
//获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if(accessLimit != null){
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
//从redis中获取用户访问的次数
String ip = request.getHeader("x-forwarded-for"); // 有可能ip是代理的
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.getRemoteAddr();
}
Integer count = (Integer)redisUtil.get(RedisKey.SECOND_ACCESS + ip);
if(count == null){
//第一次访问
redisUtil.set(RedisKey.SECOND_ACCESS + ip, 1, seconds);
}else if(count < maxCount){
//加1
count = count + 1;
redisUtil.incr(RedisKey.SECOND_ACCESS + ip, count);
}else{
//超出访问次数
logger.info("访问过快ip ===> " + ip + " 且在 " + seconds + " 秒内超过最大限制 ===> " + maxCount + " 次数达到 ====> " + count);
response.setCharacterEncoding("UTF-8"); response.setContentType("application/json; charset=utf-8");
ReturnObject result = new ReturnObject(); result.setCode(ReturnCode.OPERATION_FAILED.getCode());
result.setData("操作太快了");
Object obj = JSONObject.toJSON(result); response.getWriter().write(JSONObject.toJSONString(obj));
return false;
}
}
Integer userId = userAuthService.getUserIdBySession(session);
//获取登录的session进行判断
if (userId == null || userId == 0) {
throw new UserException(ReturnCode.BAD_REQUEST, "无效session");
}
userContext.setUserId(userId);
return super.preHandle(request, response, handler);
}
}
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
private SessionInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor);
}
}
这里采用了注解方式(拦截器),结合redis来存储请求次数,达到上限就不让用户操作。当然,redis有时间限制,到了时间用户可以再次请求接口的。