在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑,因此需要确保只有授权的用户才能访问。同时,为了防止系统过载,需要对 API 调用进行限制。
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ApiKeyAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String apiKey = httpRequest.getHeader("X-API-KEY");
if (!isValidApiKey(apiKey)) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.getWriter().write("Unauthorized");
return;
}
chain.doFilter(request, response);
}
private boolean isValidApiKey(String apiKey) {
// 实现 API 密钥的验证逻辑
return true;
}
}
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class RateLimitingFilter implements Filter {
@Autowired
SlidingWindowRateLimiter limiter;
private final RedisTemplate<String, String> redisTemplate;
private static final int LIMIT = 100; // 设置每分钟的请求限制
private static final int WINDOW_SIZE_IN_SECONDS = 60; // 时间窗口大小
public RateLimitingFilter(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String apiKey = httpRequest.getHeader("X-API-KEY");
String key = "rate_limit:" + apiKey;
long currentTime = Instant.now().getEpochSecond();
long windowStart = currentTime - WINDOW_SIZE_IN_SECONDS;
boolean result = limiter.allowRequest(key);
if (result) {
httpResponse.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
httpResponse.getWriter().write("Too Many Requests");
return;
}
chain.doFilter(request, response);
}
}
这里面的SlidingWindowRateLimiter实现如下:
import redis.clients.jedis.Jedis;
public class SlidingWindowRateLimiter {
private Jedis jedis;
private String key;
private int limit;
public SlidingWindowRateLimiter(Jedis jedis, String key, int limit) {
this.jedis = jedis;
this.key = key;
this.limit = limit;
}
public boolean allowRequest(String key) {
// 当前时间戳
long currentTime = System.currentTimeMillis();
// 使用Lua脚本来确保原子性操作
String luaScript = "local window_start = ARGV[1] - 60000\n" +
"redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" +
"local current_requests = redis.call('ZCARD', KEYS[1])\n" +
"if current_requests < tonumber(ARGV[2]) then\n" +
" redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
Object result = jedis.eval(luaScript, 1, key, String.valueOf(currentTime), String.valueOf(limit));
return (Long) result == 1;
}
}
通过这种结合了 API 密钥认证和滑动窗口限流的策略,可以有效提高 API 的安全性和稳定性,防止滥用和系统过载,同时保证合法用户的正常访问。