主题
面试速答(先看这里)
**一句话结论:**通过这种结合了 API 密钥认证和滑动窗口限流的策略,可以有效提高 API 的安全性和稳定性,防止滥用和系统过载,同时保证合法用户的正常访问。
60秒标准回答:
在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑,因此需要确保只有授权的用户才能访问。同时,为了防止系统过载,需要对 API 调用进行限制
这里面的SlidingWindowRateLimiter实现如下
通过这种结合了 API 密钥认证和滑动窗口限流的策略,可以有效提高 API 的安全性和稳定性,防止滥用和系统过载,同时保证合法用户的正常访问
**答题顺序:**结论 → 原理/机制 → 关键流程 → 场景与取舍 → 易错点
回答主线:
- **要点1:**在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。
- **要点2:**这里面的SlidingWindowRateLimiter实现如下:
**记忆锚点:**API → Redis → 滑动窗口算法 → SlidingWindowRateLimiter → 设计API秘钥方式提升接口安全性 → 和滑动窗口限流的策略
关键取舍:
- 同时,为了防止系统过载,需要对 API 调用进行限制。
- 这里面的SlidingWindowRateLimiter实现如下: 其他安全和性能考虑 API 密钥安全性 :确保 API 密钥通过安全的方式传输(例如 HTTPS)。
加分表达:
- 在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。
- 如果密钥无效或缺失,请求将被拒绝。
- 窗口计算 :检查当前时间窗口内的请求数量,如果超过阈值,则拒绝请求。
追问准备:
- 围绕「API」:底层原理是什么?使用时有哪些边界和常见坑?
- 围绕「Redis」:底层原理是什么?使用时有哪些边界和常见坑?
- 围绕「滑动窗口算法」:底层原理是什么?使用时有哪些边界和常见坑?
- 如果线上出现异常,你会如何定位、验证并规避?
背景
在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑,因此需要确保只有授权的用户才能访问。同时,为了防止系统过载,需要对 API 调用进行限制。
技术选型
- API 认证:使用 API 密钥或 OAuth 令牌。
- API 限流:使用滑动窗口算法实现限流。
- 限流存储:使用 Redis 作为存储和计算滑动窗口的工具。
具体实现
API 认证
- 生成 API 密钥:为每个用户生成唯一的 API 密钥。当用户创建账户时,后端生成密钥并提供给用户。
- 客户端请求:客户端在发起请求时需在 HTTP 头部附带 API 密钥。
- 服务器端验证:服务器接收到请求后,提取并验证 API 密钥。如果密钥无效或缺失,请求将被拒绝。
javascript
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;
}
}API 限流
- 滑动窗口算法:使用 Redis 来存储和计算每个用户的请求计数。
- 请求计数:每个请求到达时,使用 Redis 记录该请求的时间戳。
- 窗口计算:检查当前时间窗口内的请求数量,如果超过阈值,则拒绝请求。
javascript
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实现如下:
javascript
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 密钥通过安全的方式传输(例如 HTTPS)。
- 密钥旋转和管理:提供机制允许用户定期更换 API 密钥。
- 错误处理和日志:合理记录错误和请求日志,以便于问题追踪和分析。
通过这种结合了 API 密钥认证和滑动窗口限流的策略,可以有效提高 API 的安全性和稳定性,防止滥用和系统过载,同时保证合法用户的正常访问。