基于JWT的API速率限制实现
一、JWT基础概念
(一)JWT是什么
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象的形式安全地传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的。JWT通常由三部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
(二)JWT的结构
- 头部 头部通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,如HMAC SHA256或RSA。以下是一个头部的示例JSON:
{
"alg": "HS256",
"typ": "JWT"
}
然后,这个JSON会使用Base64Url编码,形成JWT的第一部分。
- 载荷 载荷是JWT的第二部分,其中包含声明(claims)。声明是关于实体(通常是用户)和其他数据的陈述。有三种类型的声明:注册声明(如iss、exp、sub等)、公共声明和私有声明。例如:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
同样,这个JSON也会被Base64Url编码,成为JWT的第二部分。
- 签名 为了创建签名部分,需要使用编码后的头部、编码后的载荷、一个密钥(secret)和头部中指定的签名算法。例如,如果使用HMAC SHA256算法,签名将按如下方式创建:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名用于验证消息在传输过程中没有被更改,并且在使用私钥签名的情况下,还可以验证JWT的发送者的身份。
二、API速率限制的重要性
(一)防止恶意攻击
-
暴力破解攻击 攻击者可能尝试通过不断尝试不同的密码组合来暴力破解用户账户。如果没有API速率限制,攻击者可以在短时间内发送大量的登录请求,增加成功破解的机会。例如,一个恶意用户可能使用自动化脚本每秒发送数百个登录请求,试图猜出正确的用户名和密码组合。通过实施API速率限制,我们可以限制每个用户或IP地址在一定时间内的登录请求次数,从而有效阻止这类攻击。
-
DDoS攻击 分布式拒绝服务(DDoS)攻击旨在通过向目标服务器发送大量请求,使其资源耗尽,无法正常为合法用户提供服务。API速率限制可以作为一种防御机制,限制单个源(如IP地址或用户)在特定时间内能够发送的请求数量,避免服务器被大量恶意请求淹没。
(二)资源管理
-
服务器资源保护 服务器的资源(如CPU、内存和带宽)是有限的。如果没有速率限制,一些用户或应用可能会过度使用API,导致服务器资源被耗尽,影响其他用户的正常使用。例如,一个数据挖掘应用可能在短时间内频繁请求大量数据,消耗大量服务器资源。通过限制每个API调用者的请求速率,可以确保服务器资源得到合理分配,保证所有用户都能获得稳定的服务。
-
成本控制 对于一些按使用量计费的云服务,过高的API调用频率可能导致成本大幅增加。API速率限制可以帮助企业控制成本,避免因意外或恶意的高频率调用而产生巨额费用。
三、基于JWT实现API速率限制的原理
(一)JWT与用户标识
JWT中的载荷部分可以包含用户的唯一标识,如用户ID。当用户通过身份验证后,服务器会生成包含用户标识的JWT并返回给客户端。后续客户端在每次请求API时,都会在请求头中携带这个JWT。服务器在接收到请求后,首先验证JWT的有效性,并从中提取用户标识。
(二)速率限制策略
-
基于时间窗口的策略 常见的速率限制策略是基于时间窗口的方法。例如,我们可以设置每个用户在一分钟内最多可以发送100个请求。服务器会为每个用户维护一个计数器,记录其在当前时间窗口内的请求次数。每次接收到请求时,服务器检查计数器的值。如果计数器小于限制值,则允许请求通过,并将计数器加一;如果计数器达到限制值,则拒绝请求,并返回相应的错误信息。
-
滑动时间窗口策略 为了更精确地控制速率,我们可以使用滑动时间窗口策略。与固定时间窗口不同,滑动时间窗口随着每次请求而移动。例如,我们仍然设置每分钟100个请求的限制,但时间窗口不是固定的一分钟,而是随着每次请求动态更新。这样可以避免在时间窗口边界处出现请求集中爆发的情况,使速率限制更加平滑。
(三)存储与验证
-
存储请求计数 服务器需要一种方式来存储每个用户的请求计数。常见的存储方式包括内存缓存(如Redis)和数据库(如MySQL)。使用内存缓存的优点是速度快,适合高并发场景;而数据库则更适合持久化存储,但读写速度相对较慢。以Redis为例,我们可以使用哈希(Hash)数据结构来存储每个用户的请求计数。哈希的键可以是用户ID,值是一个包含请求计数和时间戳的对象。
-
验证速率限制 在接收到请求时,服务器首先从JWT中提取用户ID,然后根据用户ID从存储中获取当前的请求计数和时间戳。根据当前时间和存储的时间戳,计算是否在时间窗口内,并检查请求计数是否超过限制。如果请求在时间窗口内且计数未超过限制,则允许请求通过并更新计数;否则拒绝请求。
四、基于JWT实现API速率限制的代码示例(以Node.js和Express框架为例)
(一)环境搭建
-
安装Node.js 首先,确保你已经安装了Node.js。你可以从Node.js官方网站(https://nodejs.org/)下载并安装适合你操作系统的版本。
-
创建项目目录 在命令行中,创建一个新的项目目录:
mkdir jwt - rate - limit - api
cd jwt - rate - limit - api
- 初始化项目 运行以下命令初始化一个新的Node.js项目,并按照提示填写相关信息:
npm init -y
- 安装依赖
我们需要安装一些必要的npm包,包括
express
用于搭建Web服务器,jsonwebtoken
用于处理JWT,redis
用于存储请求计数。运行以下命令安装这些包:
npm install express jsonwebtoken redis
(二)JWT相关代码
- 生成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
};
- 验证JWT
在
auth.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速率限制代码
- 连接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;
- 实现速率限制中间件
在项目目录中创建一个
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安全
-
密钥管理 JWT的安全性依赖于密钥的保密性。密钥应该足够长且随机,并且只能在服务器端存储和使用。避免在客户端代码中暴露密钥,防止密钥被窃取,从而导致JWT被伪造。定期更换密钥也是一种增强安全性的措施。
-
防止重放攻击 虽然JWT本身不包含防止重放攻击的机制,但可以通过在JWT中添加唯一标识符(如JTI声明)并结合服务器端的存储来实现。每次验证JWT时,检查JTI是否已经使用过,如果已经使用过,则拒绝该请求。
(二)速率限制策略优化
-
动态调整限制 在实际应用中,可能需要根据不同的用户角色、服务等级或服务器负载动态调整速率限制。例如,付费用户可能有更高的请求限制,而免费用户则限制更严格。可以通过在JWT载荷中添加用户角色信息,并在速率限制中间件中根据角色调整限制值。
-
应对突发流量 对于一些正常的突发流量(如促销活动期间),固定的速率限制可能会导致用户体验下降。可以考虑使用弹性速率限制策略,如在短时间内允许一定比例的超额请求,同时记录超额情况,以便后续分析和调整策略。
(三)性能与可扩展性
-
缓存与存储优化 如果使用Redis等缓存进行请求计数存储,合理设置缓存的过期时间和数据结构非常重要。对于高并发场景,可以使用Redis的集群模式来提高性能和可扩展性。如果使用数据库存储,要注意数据库的索引优化和读写性能,避免成为性能瓶颈。
-
分布式系统中的速率限制 在分布式系统中,确保各个节点之间的速率限制数据一致性是一个挑战。可以使用分布式缓存(如Redis集群)来共享请求计数数据,同时结合分布式锁来保证数据的原子性操作,防止多个节点同时更新导致数据不一致。
六、不同编程语言和框架下的实现差异
(一)Python与Flask框架
- 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
- 速率限制
使用
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框架
- JWT处理
在Java中,使用
jjwt - api
和jjwt - 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.");
}
}
}
- 速率限制
使用
Spring Boot Actuator
和Guava
库来实现速率限制。在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的安全和稳定运行。