✅为了防止接口被恶意调用,设计API秘钥方式提升接口安全性,并通过滑动窗口粗实现接口调用限流。

背景

在一个典型的业务场景中,我们提供了一个API ,被用来允许外部或内部客户端调用。这些 API 可能会暴露敏感数据或业务逻辑,因此需要确保只有授权的用户才能访问。同时,为了防止系统过载,需要对 API 调用进行限制。

技术选型

  1. API 认证:使用 API 密钥或 OAuth 令牌。
  2. API 限流:使用滑动窗口算法实现限流。
  3. 限流存储:使用 Redis 作为存储和计算滑动窗口的工具。

具体实现

API 认证

  1. 生成 API 密钥:为每个用户生成唯一的 API 密钥。当用户创建账户时,后端生成密钥并提供给用户。
  2. 客户端请求:客户端在发起请求时需在 HTTP 头部附带 API 密钥。
  3. 服务器端验证:服务器接收到请求后,提取并验证 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; 
    }
}

API 限流

  1. 滑动窗口算法:使用 Redis 来存储和计算每个用户的请求计数。
  2. 请求计数:每个请求到达时,使用 Redis 记录该请求的时间戳。
  3. 窗口计算:检查当前时间窗口内的请求数量,如果超过阈值,则拒绝请求。
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 密钥通过安全的方式传输(例如 HTTPS)。
  • 密钥旋转和管理:提供机制允许用户定期更换 API 密钥。
  • 错误处理和日志:合理记录错误和请求日志,以便于问题追踪和分析。

通过这种结合了 API 密钥认证和滑动窗口限流的策略,可以有效提高 API 的安全性和稳定性,防止滥用和系统过载,同时保证合法用户的正常访问。

学习资料

✅过滤器和拦截器的区别是什么?

✅如何基于Redis实现滑动窗口限流?

✅Cookie,Session,Token的区别是什么?

原文: https://www.yuque.com/hollis666/xkm7k3/nvfqzrus72dsztd0