MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于JWT的API速率限制实现

2022-05-307.5k 阅读

一、JWT基础概念

(一)JWT是什么

JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的。JWT通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

(二)JWT的结构

  1. 头部 头部通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,如HMAC SHA256或RSA。以下是一个头部的示例JSON:
{
  "alg": "HS256",
  "typ": "JWT"
}

然后,这个JSON会使用Base64Url编码,形成JWT的第一部分。

  1. 载荷 载荷是JWT的第二部分,其中包含声明(claims)。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(如iss、exp、sub等)、公共声明和私有声明。例如:
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

同样,这个JSON也会被Base64Url编码,成为JWT的第二部分。

  1. 签名 为了创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用HMAC SHA256算法,签名将按如下方式创建:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证JWT的发送者的身份。

二、API速率限制的重要性

(一)防止恶意攻击

  1. 暴力破解攻击 攻击者可能尝试通过不断尝试不同的密码组合来暴力破解用户账户。如果没有API速率限制,攻击者可以在短时间内发送大量的登录请求,增加成功破解的机会。例如,一个恶意用户可能使用自动化脚本每秒发送数百个登录请求,试图猜出正确的用户名和密码组合。通过实施API速率限制,我们可以限制每个用户或IP地址在一定时间内的登录请求次数,从而有效阻止这类攻击。

  2. DDoS攻击 分布式拒绝服务(DDoS)攻击旨在通过向目标服务器发送大量请求,使其资源耗尽,无法正常为合法用户提供服务。API速率限制可以作为一种防御机制,限制单个源(如IP地址或用户)在特定时间内能够发送的请求数量,避免服务器被大量恶意请求淹没。

(二)资源管理

  1. 服务器资源保护 服务器的资源(如CPU、内存和带宽)是有限的。如果没有速率限制,一些用户或应用可能会过度使用API,导致服务器资源被耗尽,影响其他用户的正常使用。例如,一个数据挖掘应用可能在短时间内频繁请求大量数据,消耗大量服务器资源。通过限制每个API调用者的请求速率,可以确保服务器资源得到合理分配,保证所有用户都能获得稳定的服务。

  2. 成本控制 对于一些按使用量计费的云服务,过高的API调用频率可能导致成本大幅增加。API速率限制可以帮助企业控制成本,避免因意外或恶意的高频率调用而产生巨额费用。

三、基于JWT实现API速率限制的原理

(一)JWT与用户标识

JWT中的载荷部分可以包含用户的唯一标识,如用户ID。当用户通过身份验证后,服务器会生成包含用户标识的JWT并返回给客户端。后续客户端在每次请求API时,都会在请求头中携带这个JWT。服务器在接收到请求后,首先验证JWT的有效性,并从中提取用户标识。

(二)速率限制策略

  1. 基于时间窗口的策略 常见的速率限制策略是基于时间窗口的方法。例如,我们可以设置每个用户在一分钟内最多可以发送100个请求。服务器会为每个用户维护一个计数器,记录其在当前时间窗口内的请求次数。每次接收到请求时,服务器检查计数器的值。如果计数器小于限制值,则允许请求通过,并将计数器加一;如果计数器达到限制值,则拒绝请求,并返回相应的错误信息。

  2. 滑动时间窗口策略 为了更精确地控制速率,我们可以使用滑动时间窗口策略。与固定时间窗口不同,滑动时间窗口随着每次请求而移动。例如,我们仍然设置每分钟100个请求的限制,但时间窗口不是固定的一分钟,而是随着每次请求动态更新。这样可以避免在时间窗口边界处出现请求集中爆发的情况,使速率限制更加平滑。

(三)存储与验证

  1. 存储请求计数 服务器需要一种方式来存储每个用户的请求计数。常见的存储方式包括内存缓存(如Redis)和数据库(如MySQL)。使用内存缓存的优点是速度快,适合高并发场景;而数据库则更适合持久化存储,但读写速度相对较慢。以Redis为例,我们可以使用哈希(Hash)数据结构来存储每个用户的请求计数。哈希的键可以是用户ID,值是一个包含请求计数和时间戳的对象。

  2. 验证速率限制 在接收到请求时,服务器首先从JWT中提取用户ID,然后根据用户ID从存储中获取当前的请求计数和时间戳。根据当前时间和存储的时间戳,计算是否在时间窗口内,并检查请求计数是否超过限制。如果请求在时间窗口内且计数未超过限制,则允许请求通过并更新计数;否则拒绝请求。

四、基于JWT实现API速率限制的代码示例(以Node.js和Express框架为例)

(一)环境搭建

  1. 安装Node.js 首先,确保你已经安装了Node.js。你可以从Node.js官方网站(https://nodejs.org/)下载并安装适合你操作系统的版本。

  2. 创建项目目录 在命令行中,创建一个新的项目目录:

mkdir jwt - rate - limit - api
cd jwt - rate - limit - api
  1. 初始化项目 运行以下命令初始化一个新的Node.js项目,并按照提示填写相关信息:
npm init -y
  1. 安装依赖 我们需要安装一些必要的npm包,包括express用于搭建Web服务器,jsonwebtoken用于处理JWT,redis用于存储请求计数。运行以下命令安装这些包:
npm install express jsonwebtoken redis

(二)JWT相关代码

  1. 生成JWT 在项目目录中创建一个auth.js文件,用于处理身份验证和JWT生成。以下是生成JWT的示例代码:
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your - secret - key';

function generateToken(user) {
  const payload = {
    sub: user.id,
    name: user.name
  };
  return jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });
}

module.exports = {
  generateToken
};
  1. 验证JWTauth.js文件中添加验证JWT的函数:
function verifyToken(req, res, next) {
  const token = req.headers['authorization'];
  if (!token) {
    return res.status(401).send('Access denied. No token provided.');
  }
  try {
    const decoded = jwt.verify(token.replace('Bearer ', ''), SECRET_KEY);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(400).send('Invalid token.');
  }
}

module.exports = {
  generateToken,
  verifyToken
};

(三)API速率限制代码

  1. 连接Redis 在项目目录中创建一个redis.js文件,用于连接Redis:
const redis = require('redis');
const client = redis.createClient();

client.on('connect', function () {
  console.log('Redis client connected');
});

client.on('error', function (err) {
  console.log('Error'+ err);
});

module.exports = client;
  1. 实现速率限制中间件 在项目目录中创建一个rateLimit.js文件,实现基于JWT的API速率限制中间件:
const redis = require('./redis');
const { promisify } = require('util');

const LIMIT = 100; // 每分钟允许的请求数
const WINDOW = 60 * 1000; // 时间窗口为1分钟

async function rateLimit(req, res, next) {
  const userId = req.user.sub;
  const now = Date.now();
  const key = `rate - limit:${userId}`;

  const getAsync = promisify(redis.get).bind(redis);
  const multi = redis.multi();
  multi.incr(key);
  multi.expireat(key, Math.floor(now / 1000) + Math.ceil(WINDOW / 1000));
  const results = await multi.exec();
  const count = parseInt(results[0][1] || '0');

  if (count > LIMIT) {
    return res.status(429).send('Too many requests. Try again later.');
  }
  next();
}

module.exports = rateLimit;

(四)整合代码

在项目目录中创建app.js文件,整合所有代码:

const express = require('express');
const { generateToken, verifyToken } = require('./auth');
const rateLimit = require('./rateLimit');
const app = express();
app.use(express.json());

// 模拟用户数据
const users = [
  { id: 1, name: 'John Doe' }
];

// 登录路由,生成JWT
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.name === username);
  if (!user || user.password!== password) {
    return res.status(400).send('Invalid credentials.');
  }
  const token = generateToken(user);
  res.send({ token });
});

// 受保护的API路由,应用速率限制
app.get('/protected', verifyToken, rateLimit, (req, res) => {
  res.send('This is a protected route.');
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

五、基于JWT实现API速率限制的注意事项

(一)JWT安全

  1. 密钥管理 JWT的安全性依赖于密钥的保密性。密钥应该足够长且随机,并且只能在服务器端存储和使用。避免在客户端代码中暴露密钥,防止密钥被窃取,从而导致JWT被伪造。定期更换密钥也是一种增强安全性的措施。

  2. 防止重放攻击 虽然JWT本身不包含防止重放攻击的机制,但可以通过在JWT中添加唯一标识符(如JTI声明)并结合服务器端的存储来实现。每次验证JWT时,检查JTI是否已经使用过,如果已经使用过,则拒绝该请求。

(二)速率限制策略优化

  1. 动态调整限制 在实际应用中,可能需要根据不同的用户角色、服务等级或服务器负载动态调整速率限制。例如,付费用户可能有更高的请求限制,而免费用户则限制更严格。可以通过在JWT载荷中添加用户角色信息,并在速率限制中间件中根据角色调整限制值。

  2. 应对突发流量 对于一些正常的突发流量(如促销活动期间),固定的速率限制可能会导致用户体验下降。可以考虑使用弹性速率限制策略,如在短时间内允许一定比例的超额请求,同时记录超额情况,以便后续分析和调整策略。

(三)性能与可扩展性

  1. 缓存与存储优化 如果使用Redis等缓存进行请求计数存储,合理设置缓存的过期时间和数据结构非常重要。对于高并发场景,可以使用Redis的集群模式来提高性能和可扩展性。如果使用数据库存储,要注意数据库的索引优化和读写性能,避免成为性能瓶颈。

  2. 分布式系统中的速率限制 在分布式系统中,确保各个节点之间的速率限制数据一致性是一个挑战。可以使用分布式缓存(如Redis集群)来共享请求计数数据,同时结合分布式锁来保证数据的原子性操作,防止多个节点同时更新导致数据不一致。

六、不同编程语言和框架下的实现差异

(一)Python与Flask框架

  1. JWT处理 在Python中,使用PyJWT库来处理JWT。安装PyJWT库:
pip install PyJWT

生成JWT的示例代码如下:

import jwt
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'your - secret - key'

def generate_token(user):
    payload = {
      'sub': user['id'],
        'name': user['name']
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

验证JWT的代码:

def verify_token(func):
    def wrapper(*args, **kwargs):
        token = None
        if 'authorization' in request.headers:
            token = request.headers['authorization'].replace('Bearer ', '')
        if not token:
            return jsonify({'message': 'Access denied. No token provided.'}), 401
        try:
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
            return func(*args, **kwargs)
        except jwt.ExpiredSignatureError:
            return jsonify({'message': 'Token has expired.'}), 401
        except jwt.InvalidTokenError:
            return jsonify({'message': 'Invalid token.'}), 401
    return wrapper
  1. 速率限制 使用Flask - Limiter库来实现速率限制。安装Flask - Limiter库:
pip install Flask - Limiter

示例代码如下:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["200 per day", "50 per hour"]
)

@app.route('/protected', methods=['GET'])
@verify_token
@limiter.limit("100 per minute")
def protected():
    return jsonify({'message': 'This is a protected route.'})

(二)Java与Spring Boot框架

  1. JWT处理 在Java中,使用jjwt - apijjwt - impl库来处理JWT。在pom.xml文件中添加依赖:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt - api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt - impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt - jjwt - api</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

生成JWT的示例代码:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

public class JwtUtil {
    private static final String SECRET_KEY = "your - secret - key";

    public static String generateToken(User user) {
        Claims claims = Jwts.claims().setSubject(user.getId());
        claims.put("name", user.getName());
        return Jwts.builder()
              .setClaims(claims)
              .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
              .compact();
    }
}

验证JWT的代码:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token == null ||!token.startsWith("Bearer ")) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Access denied. No token provided.");
            return;
        }
        token = token.substring(7);
        try {
            Claims claims = Jwts.parserBuilder()
                  .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
                  .build()
                  .parseClaimsJws(token)
                  .getBody();
            request.setAttribute("user", claims);
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token.");
        }
    }
}
  1. 速率限制 使用Spring Boot ActuatorGuava库来实现速率限制。在pom.xml文件中添加依赖:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring - boot - starter - actuator</artifactId>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1 - jre</version>
</dependency>

示例代码如下:

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class RateLimitInterceptor implements HandlerInterceptor {
    private final RateLimiter rateLimiter;

    public RateLimitInterceptor(double permitsPerSecond) {
        this.rateLimiter = RateLimiter.create(permitsPerSecond);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!rateLimiter.tryAcquire(1, 500, TimeUnit.MILLISECONDS)) {
            response.setStatus(HttpServletResponse.SC_TOO_MANY_REQUESTS);
            return false;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 处理后操作
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 完成后操作
    }
}

然后在Spring Boot的配置类中注册拦截器:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RateLimitInterceptor(100.0))
              .addPathPatterns("/protected");
    }
}

不同编程语言和框架在实现基于JWT的API速率限制时,虽然原理相似,但具体的库和实现方式有所不同。开发者需要根据项目的技术栈和需求选择合适的实现方案。同时,无论使用哪种技术,都要确保JWT的安全性和速率限制策略的有效性,以保障API的安全和稳定运行。