今天遇到一个小坑,公司要求前端做页面缓存,但是缓存之后会造成一个问题,那就是有时候新增会发生重复请求,甚至重复请求5次,虽然前端已经做了防抖操作,但是这种系统层面的防抖还是做不到的,所以后端也必须做接口防抖。
参考网上的解决方案,大多都是通过写注解+过滤器来实现对接口防抖,但是这种方案需要在每个方法或者每个类上面写注解,未免也太过繁杂了,于是想着可不可以在gateway层面进行IP检测+接口防抖?说干就干!
首先在Gateway中注册一个全局过滤器,优先级放在最前面:
@Component
public class RequestLimitFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(RequestLimitFilter.class);
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
}
接着我们需要写一下获取用户真实IP地址的方法:
private static final String IP_UNKNOWN = "unknown";
private static final String IP_LOCAL = "127.0.0.1";
private static final int IP_LEN = 15;
/**
* 获取客户端真实ip
* @param request request
* @return 返回ip
*/
public static String getIP(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ipAddress = headers.getFirst("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = Optional.ofNullable(request.getRemoteAddress())
.map(address -> address.getAddress().getHostAddress())
.orElse("");
if (IP_LOCAL.equals(ipAddress)) {
// 根据网卡取本机配置的IP
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
// ignore
}
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > IP_LEN) {
int index = ipAddress.indexOf(",");
if (index > 0) {
ipAddress = ipAddress.substring(0, index);
}
}
return ipAddress;
}
这个时候还没完,这个方法需要nginx来支持,否则是获取不到用户真实IP的,附nginx配置:
location / {
add_header Cache-Control no-cache;
root html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
接下来我们就用redis进行记录用户访问操作,实现一秒内用一个IP只可访问一次接口:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
String ip = IpUtils.getIP(request);
String key = ip+ "_" + url;
//使用请求方式判断
// String method = request.getMethodValue();
// if(StrUtil.isNotBlank(method) || method.equals(HttpMethod.POST.name())){
// String value = (String) redisService.getCacheObject(key);
// if(StrUtil.isBlank(value)){
// redisService.setCacheObject(key,"try access",1L, TimeUnit.SECONDS);
// }else {
// return unauthorizedResponse(ip,exchange, "1秒内不可重复请求");
// }
// }
//使用请求路径判断
if(url.contains("save") || url.contains("Save") || url.contains("add") || url.contains("Add") || url.contains("insert") || url.contains("Insert")){
String value = (String) redisService.getCacheObject(key);
if(StrUtil.isBlank(value)){
redisService.setCacheObject(key,"try access",1L, TimeUnit.SECONDS);
}else {
return unauthorizedResponse(ip,exchange, "1秒内不可重复请求");
}
}
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
测试一下:
这样我们就实现了IP+接口防抖操作,还是挺新鲜的
完整代码在这里:
/**
* 同一个IP访问接口限制
*
* @author ruoyi
*/
@Component
public class RequestLimitFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(RequestLimitFilter.class);
@Autowired
private RedisService redisService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest.Builder mutate = request.mutate();
String url = request.getURI().getPath();
String ip = IpUtils.getIP(request);
String key = ip+ "_" + url;
//使用请求方式判断
// String method = request.getMethodValue();
// if(StrUtil.isNotBlank(method) || method.equals(HttpMethod.POST.name())){
// String value = (String) redisService.getCacheObject(key);
// if(StrUtil.isBlank(value)){
// redisService.setCacheObject(key,"try access",1L, TimeUnit.SECONDS);
// }else {
// return unauthorizedResponse(ip,exchange, "1秒内不可重复请求");
// }
// }
//使用请求路径判断
if(url.contains("save") || url.contains("Save") || url.contains("add") || url.contains("Add") || url.contains("insert") || url.contains("Insert")){
String value = (String) redisService.getCacheObject(key);
if(StrUtil.isBlank(value)){
redisService.setCacheObject(key,"try access",1L, TimeUnit.SECONDS);
}else {
return unauthorizedResponse(ip,exchange, "1秒内不可重复请求");
}
}
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
private Mono<Void> unauthorizedResponse(String ip,ServerWebExchange exchange, String msg) {
log.error("{}请求重复:{}",ip, exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.BAD_REQUEST);
}
@Override
public int getOrder() {
return -300;
}
}
欢迎大家关注我的公众号:Java技术与面经,所有的产品创作过程都会在这上面展示的哦