diff --git a/README.md b/README.md index 2a03bdf..555a4cc 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,12 @@ - 自动清理过期令牌 - 重定向URI验证 - 客户端所有权验证 +- **速率限制** - 针对不同重要程度的接口设置不同限制 + - 通用接口:15分钟内最多100次请求 + - 登录/注册:15分钟内最多5次尝试 + - OAuth授权:15分钟内最多3次尝试 + - OAuth令牌:15分钟内最多10次请求 + - OAuth客户端管理:15分钟内最多20次操作 ### 📊 OAuth客户端管理 - 创建OAuth客户端 diff --git a/index.js b/index.js index f228c60..4f27624 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const OAuthToken = require('./models/OAuthToken'); const authRoutes = require('./routes/auth'); const oauthRoutes = require('./routes/oauth'); const oauthClientRoutes = require('./routes/oauth-clients'); +const { generalLimiter } = require('./middleware/rateLimit'); const app = express(); const PORT = process.env.PORT || 3000; @@ -17,6 +18,9 @@ app.use(cors()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +// 应用通用速率限制 +app.use(generalLimiter); + // 请求日志中间件 app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`); diff --git a/middleware/rateLimit.js b/middleware/rateLimit.js new file mode 100644 index 0000000..cb66035 --- /dev/null +++ b/middleware/rateLimit.js @@ -0,0 +1,69 @@ +const rateLimit = require('express-rate-limit'); + +// 通用速率限制 +const generalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 100, // 限制每个IP 15分钟内最多100次请求 + message: { + success: false, + message: '请求过于频繁,请稍后再试' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// 登录注册接口限制(更严格) +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 5, // 限制每个IP 15分钟内最多5次登录/注册尝试 + message: { + success: false, + message: '登录/注册尝试过于频繁,请15分钟后再试' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// OAuth授权接口限制(最严格) +const oauthAuthLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 10, // 限制每个IP 15分钟内最多3次OAuth授权尝试 + message: { + success: false, + message: 'OAuth授权请求过于频繁,请15分钟后再试' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// OAuth令牌接口限制(严格) +const oauthTokenLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 10, // 限制每个IP 15分钟内最多10次令牌请求 + message: { + success: false, + message: '令牌请求过于频繁,请稍后再试' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +// OAuth客户端管理接口限制 +const oauthClientLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15分钟 + max: 20, // 限制每个IP 15分钟内最多20次客户端管理操作 + message: { + success: false, + message: '客户端管理操作过于频繁,请稍后再试' + }, + standardHeaders: true, + legacyHeaders: false, +}); + +module.exports = { + generalLimiter, + authLimiter, + oauthAuthLimiter, + oauthTokenLimiter, + oauthClientLimiter +}; \ No newline at end of file diff --git a/package.json b/package.json index 7a71df5..3de2692 100644 --- a/package.json +++ b/package.json @@ -10,33 +10,42 @@ "test:oauth": "node test-oauth.js", "test:oauth-flow": "node test-oauth-flow.js", "test:redirect-uri": "node test-redirect-uri.js", + "test:rate-limit": "node test-rate-limit.js", "dev:frontend": "vite", "build:frontend": "vite build", "preview:frontend": "vite preview" }, "dependencies": { - "express": "^4.18.2", - "pg": "^8.11.3", "bcryptjs": "^2.4.3", - "jsonwebtoken": "^9.0.2", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express-validator": "^7.0.1" + "express": "^4.18.2", + "express-rate-limit": "^7.1.5", + "express-validator": "^7.0.1", + "jsonwebtoken": "^9.0.2", + "pg": "^8.11.3" }, "devDependencies": { - "nodemon": "^3.0.1", - "axios": "^1.5.0", - "@vitejs/plugin-react": "^4.0.0", - "vite": "^4.4.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "@mui/material": "^5.14.0", - "@mui/icons-material": "^5.14.0", "@emotion/react": "^11.11.0", "@emotion/styled": "^11.11.0", - "react-router-dom": "^6.15.0" + "@mui/icons-material": "^5.14.0", + "@mui/material": "^5.14.0", + "@vitejs/plugin-react": "^4.0.0", + "axios": "^1.5.0", + "nodemon": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.15.0", + "vite": "^4.4.0" }, - "keywords": ["auth", "api", "postgresql", "login", "register", "oauth"], + "keywords": [ + "auth", + "api", + "postgresql", + "login", + "register", + "oauth" + ], "author": "", "license": "MIT" } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 432b401..d08d938 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: express: specifier: ^4.18.2 version: 4.21.2 + express-rate-limit: + specifier: ^8.0.1 + version: 8.0.1(express@4.21.2) express-validator: specifier: ^7.0.1 version: 7.2.1 @@ -698,6 +701,12 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + express-rate-limit@8.0.1: + resolution: {integrity: sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express-validator@7.2.1: resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==} engines: {node: '>= 8.0.0'} @@ -803,6 +812,10 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -1926,6 +1939,11 @@ snapshots: etag@1.8.1: {} + express-rate-limit@8.0.1(express@4.21.2): + dependencies: + express: 4.21.2 + ip-address: 10.0.1 + express-validator@7.2.1: dependencies: lodash: 4.17.21 @@ -2067,6 +2085,8 @@ snapshots: inherits@2.0.4: {} + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} is-arrayish@0.2.1: {} diff --git a/routes/auth.js b/routes/auth.js index 7f24ac1..69fc0fd 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -6,11 +6,12 @@ const { loginValidation, handleValidationErrors } = require('../middleware/validation'); +const { authLimiter } = require('../middleware/rateLimit'); const router = express.Router(); // 注册路由 -router.post('/register', registerValidation, handleValidationErrors, async (req, res) => { +router.post('/register', authLimiter, registerValidation, handleValidationErrors, async (req, res) => { try { const { username, email, password } = req.body; @@ -62,7 +63,7 @@ router.post('/register', registerValidation, handleValidationErrors, async (req, }); // 登录路由 -router.post('/login', loginValidation, handleValidationErrors, async (req, res) => { +router.post('/login', authLimiter, loginValidation, handleValidationErrors, async (req, res) => { try { const { username, password } = req.body; diff --git a/routes/oauth-clients.js b/routes/oauth-clients.js index 28bcf65..74b4bf2 100644 --- a/routes/oauth-clients.js +++ b/routes/oauth-clients.js @@ -2,6 +2,7 @@ const express = require('express'); const { body, validationResult } = require('express-validator'); const OAuthClient = require('../models/OAuthClient'); const { authenticateToken } = require('../middleware/auth'); +const { oauthClientLimiter } = require('../middleware/rateLimit'); const router = express.Router(); @@ -93,7 +94,7 @@ const validateRedirectUris = (req, res, next) => { }; // 1. 创建OAuth客户端 -router.post('/clients', authenticateToken, createClientValidation, validateRedirectUris, handleValidationErrors, async (req, res) => { +router.post('/clients', oauthClientLimiter, authenticateToken, createClientValidation, validateRedirectUris, handleValidationErrors, async (req, res) => { try { const { name, description, redirect_uris, scopes } = req.body; const userId = req.user.userId; diff --git a/routes/oauth.js b/routes/oauth.js index 7e77409..b24e393 100644 --- a/routes/oauth.js +++ b/routes/oauth.js @@ -5,6 +5,7 @@ const OAuthToken = require('../models/OAuthToken'); const User = require('../models/User'); const { authenticateToken } = require('../middleware/auth'); const { authenticateOAuthToken, requireScope } = require('../middleware/oauth'); +const { oauthAuthLimiter, oauthTokenLimiter } = require('../middleware/rateLimit'); const router = express.Router(); @@ -58,7 +59,7 @@ const handleValidationErrors = (req, res, next) => { }; // 1. 授权端点 - 验证参数并返回授权信息 -router.get('/authorize', authenticateToken, async (req, res) => { +router.get('/authorize', oauthAuthLimiter, authenticateToken, async (req, res) => { try { const { response_type, @@ -141,8 +142,8 @@ router.get('/authorize', authenticateToken, async (req, res) => { } }); -// 2. 用户同意授权端点 -router.post('/authorize/consent', authenticateToken, async (req, res) => { +// 2. 用户同意/拒绝授权端点 +router.post('/authorize/consent', oauthAuthLimiter, authenticateToken, async (req, res) => { try { const { client_id, @@ -247,7 +248,7 @@ router.post('/authorize/consent', authenticateToken, async (req, res) => { }); // 3. 令牌端点 - 交换授权码获取访问令牌 -router.post('/token', [ +router.post('/token', oauthTokenLimiter, [ body('grant_type').notEmpty().withMessage('grant_type不能为空'), body('client_id').notEmpty().withMessage('client_id不能为空'), body('client_secret').notEmpty().withMessage('client_secret不能为空'), diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx index 47847f3..43da861 100644 --- a/src/pages/Dashboard.jsx +++ b/src/pages/Dashboard.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import React from 'react' import { useAuth } from '../contexts/AuthContext' import { Container, @@ -9,89 +9,27 @@ import { Grid, Card, CardContent, - CardActions, - List, - ListItem, - ListItemText, - ListItemIcon, - Divider, - Chip, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - TextField, - Alert + Divider } from '@mui/material' import { Person, Email, CalendarToday, - Security, - Add, - Delete, - Visibility, - VisibilityOff, - Logout + Logout, + Security } from '@mui/icons-material' -import axios from 'axios' +import { useNavigate } from 'react-router-dom' const Dashboard = () => { const { user, logout } = useAuth() - const [clients, setClients] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') - const [openCreateDialog, setOpenCreateDialog] = useState(false) - const [newClient, setNewClient] = useState({ - name: '', - description: '', - redirect_uris: [''], - scopes: ['read', 'write'] - }) - - useEffect(() => { - fetchClients() - }, []) - - const fetchClients = async () => { - try { - const response = await axios.get('/api/oauth/clients') - setClients(response.data.data.clients) - } catch (error) { - setError('获取客户端列表失败') - } - } - - const handleCreateClient = async () => { - setLoading(true) - try { - await axios.post('/api/oauth/clients', newClient) - setOpenCreateDialog(false) - setNewClient({ name: '', description: '', redirect_uris: [''], scopes: ['read', 'write'] }) - fetchClients() - } catch (error) { - setError(error.response?.data?.message || '创建客户端失败') - } - setLoading(false) - } - - const handleDeleteClient = async (clientId) => { - if (window.confirm('确定要删除这个客户端吗?')) { - try { - await axios.delete(`/api/oauth/clients/${clientId}`) - fetchClients() - } catch (error) { - setError('删除客户端失败') - } - } - } + const navigate = useNavigate() const handleLogout = () => { logout() } return ( - + 个人中心 @@ -101,171 +39,74 @@ const Dashboard = () => { - {error && ( - setError('')}> - {error} - - )} - {/* 用户信息卡片 */} - - - - - 用户信息 - - - - - - - - - - - - - - - - - - - - - - - - - - {/* OAuth客户端管理 */} - - - - - - OAuth客户端 + + + + + 用户信息 + + + + + + + + 用户名 + + + {user?.username} + + - - - - {clients.length === 0 ? ( - - - 还没有OAuth客户端,点击上方按钮创建一个 - + + + + + + 邮箱 + + + {user?.email} + + - ) : ( - - {clients.map((client) => ( - - - - - - - {client.name} - - - {client.description} - - - - 客户端ID: {client.client_id} - - - - {client.scopes.map((scope) => ( - - ))} - - - - - - - - ))} - - )} - + + + + + + 注册时间 + + + {user?.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '未知'} + + + + + - {/* 创建客户端对话框 */} - setOpenCreateDialog(false)} maxWidth="sm" fullWidth> - 创建OAuth客户端 - - setNewClient({ ...newClient, name: e.target.value })} - margin="normal" - required - /> - setNewClient({ ...newClient, description: e.target.value })} - margin="normal" - multiline - rows={2} - /> - setNewClient({ - ...newClient, - redirect_uris: [e.target.value] - })} - margin="normal" - required - placeholder="http://localhost:3001/callback" - /> - - - - - - + {/* 功能按钮 */} + + + + ) } diff --git a/src/pages/OAuthAuthorize.jsx b/src/pages/OAuthAuthorize.jsx index 223489c..828648a 100644 --- a/src/pages/OAuthAuthorize.jsx +++ b/src/pages/OAuthAuthorize.jsx @@ -10,13 +10,22 @@ import { Grid, Card, CardContent, + CardActions, List, ListItem, ListItemIcon, ListItemText, Chip, Alert, - CircularProgress + CircularProgress, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + Divider, + Tabs, + Tab } from '@mui/material' import { Security, @@ -25,18 +34,30 @@ import { Person, Email, CalendarToday, - Visibility, - VisibilityOff + Add, + Delete, + Settings, + Apps, + Logout } from '@mui/icons-material' import axios from 'axios' const OAuthAuthorize = () => { - const { user } = useAuth() + const { user, logout } = useAuth() const [searchParams] = useSearchParams() const navigate = useNavigate() const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [clientInfo, setClientInfo] = useState(null) + const [clients, setClients] = useState([]) + const [openCreateDialog, setOpenCreateDialog] = useState(false) + const [newClient, setNewClient] = useState({ + name: '', + description: '', + redirect_uris: [''], + scopes: ['read', 'write'] + }) + const [activeTab, setActiveTab] = useState(0) const clientId = searchParams.get('client_id') const redirectUri = searchParams.get('redirect_uri') @@ -45,53 +66,85 @@ const OAuthAuthorize = () => { const responseType = searchParams.get('response_type') useEffect(() => { - // 验证OAuth参数 - if (!clientId || !redirectUri || responseType !== 'code') { - setError('无效的授权请求') - - return + // 如果有OAuth参数,显示授权页面 + if (clientId && redirectUri && responseType === 'code') { + setActiveTab(1) + fetchAuthInfo() + } else { + // 否则显示客户端管理页面 + setActiveTab(0) + fetchClients() } + }, [clientId, redirectUri, responseType]) - // 获取授权信息 - const fetchAuthInfo = async () => { - try { - setLoading(true) - const params = new URLSearchParams({ - response_type: 'code', - client_id: clientId, - redirect_uri: redirectUri, - scope: scope || 'read write', - state: state || '' - }) + const fetchClients = async () => { + try { + const response = await axios.get('/api/oauth/clients') + setClients(response.data.data.clients) + } catch (error) { + setError('获取客户端列表失败') + } + } - const response = await axios.get(`/api/oauth/authorize?${params}`, { - headers: { - Authorization: `Bearer ${localStorage.getItem('token')}` - } - }) + const fetchAuthInfo = async () => { + try { + setLoading(true) + const params = new URLSearchParams({ + response_type: 'code', + client_id: clientId, + redirect_uri: redirectUri, + scope: scope || 'read write', + state: state || '' + }) - if (response.data.success) { - console.log(response.data.data.redirect_uri) - setClientInfo({ - name: response.data.data.client.name, - description: response.data.data.client.description, - scopes: response.data.data.scopes, - clientId: response.data.data.client.id, - redirectUri: response.data.data.redirect_uri, - state: response.data.data.state - }) - } else { - setError(response.data.message || '获取授权信息失败') + const response = await axios.get(`/api/oauth/authorize?${params}`, { + headers: { + Authorization: `Bearer ${localStorage.getItem('token')}` } + }) + + if (response.data.success) { + setClientInfo({ + name: response.data.data.client.name, + description: response.data.data.client.description, + scopes: response.data.data.scopes, + clientId: response.data.data.client.id, + redirectUri: response.data.data.redirect_uri, + state: response.data.data.state + }) + } else { + setError(response.data.message || '获取授权信息失败') + } + } catch (error) { + setError(error.response?.data?.message || '获取授权信息失败') + } finally { + setLoading(false) + } + } + + const handleCreateClient = async () => { + setLoading(true) + try { + await axios.post('/api/oauth/clients', newClient) + setOpenCreateDialog(false) + setNewClient({ name: '', description: '', redirect_uris: [''], scopes: ['read', 'write'] }) + fetchClients() + } catch (error) { + setError(error.response?.data?.message || '创建客户端失败') + } + setLoading(false) + } + + const handleDeleteClient = async (clientId) => { + if (window.confirm('确定要删除这个客户端吗?')) { + try { + await axios.delete(`/api/oauth/clients/${clientId}`) + fetchClients() } catch (error) { - setError(error.response?.data?.message || '获取授权信息失败') - } finally { - setLoading(false) + setError('删除客户端失败') } } - - fetchAuthInfo() - }, [clientId, redirectUri, scope, responseType, state]) + } const handleAuthorize = async () => { setLoading(true) @@ -143,8 +196,13 @@ const OAuthAuthorize = () => { } } + const handleLogout = () => { + logout() + navigate('/login') + } + // 显示加载状态 - if (loading) { + if (loading && activeTab === 1) { return ( { } return ( - - - - - - - 授权请求 - - - 第三方应用请求访问您的账户 - - + + + + OAuth 管理 + + + 管理OAuth客户端和第三方应用授权 + + - {error && ( - - {error} - - )} + {error && ( + setError('')}> + {error} + + )} - {clientInfo && ( - <> - {/* 应用信息 */} - - - - {clientInfo.name} - - - {clientInfo.description} - - - - 请求的权限: + {/* 标签页 */} + + setActiveTab(newValue)}> + } + label="客户端管理" + iconPosition="start" + /> + } + label="授权管理" + iconPosition="start" + /> + + + + {/* 客户端管理标签页 */} + {activeTab === 0 && ( + + {/* 用户信息卡片 */} + + + + + 用户信息 + + + + + + + + 用户名 + + + {user?.username} - {clientInfo.scopes.map((scope) => ( - - ))} - - + + + + + + + 邮箱 + + + {user?.email} + + + + + + + + + 注册时间 + + + {user?.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '未知'} + + + + + + - {/* 用户信息 */} - - - - 您的账户信息 + {/* OAuth客户端管理 */} + + + + + + OAuth客户端 - - - - - - - - - - - - - - - - - - - - - - - - {/* 操作按钮 */} - - - - - - + + + {clients.length === 0 ? ( + + + 还没有OAuth客户端,点击上方按钮创建一个 + + + ) : ( + + {clients.map((client) => ( + + + + + + + {client.name} + + + {client.description} + + + + 客户端ID: {client.client_id} + + + + {client.scopes.map((scope) => ( + + ))} + + + + + + + + ))} + + )} + + + + + )} + + {/* 授权管理标签页 */} + {activeTab === 1 && clientInfo && ( + + + + + + 授权请求 + + + 第三方应用请求访问您的账户 + + + + {/* 应用信息 */} + + + + {clientInfo.name} + + + {clientInfo.description} + + + + 请求的权限: + + {clientInfo.scopes.map((scope) => ( + + ))} + + + + + {/* 用户信息 */} + + + + 您的账户信息 + + + + + + + + + + + + + + + + + + + + + + + + + {/* 操作按钮 */} + + + - - )} - + + + + + + + )} + + {/* 创建客户端对话框 */} + setOpenCreateDialog(false)} maxWidth="sm" fullWidth> + 创建OAuth客户端 + + setNewClient({ ...newClient, name: e.target.value })} + margin="normal" + required + /> + setNewClient({ ...newClient, description: e.target.value })} + margin="normal" + multiline + rows={2} + /> + setNewClient({ + ...newClient, + redirect_uris: [e.target.value] + })} + margin="normal" + required + placeholder="http://localhost:3001/callback" + /> + + + + + + + + {/* 底部操作按钮 */} + + + ) diff --git a/test-rate-limit.js b/test-rate-limit.js new file mode 100644 index 0000000..2095e77 --- /dev/null +++ b/test-rate-limit.js @@ -0,0 +1,133 @@ +const axios = require('axios'); + +const BASE_URL = 'http://localhost:3000'; + +// 测试速率限制的函数 +async function testRateLimit(endpoint, method = 'GET', data = null, description) { + console.log(`\n测试 ${description} 的速率限制...`); + + try { + const config = { + method, + url: `${BASE_URL}${endpoint}`, + headers: { + 'Content-Type': 'application/json' + } + }; + + if (data) { + config.data = data; + } + + const response = await axios(config); + console.log(`✅ 请求成功: ${response.status}`); + return true; + } catch (error) { + if (error.response?.status === 429) { + console.log(`⏰ 速率限制触发: ${error.response.data.message}`); + return false; + } else { + console.log(`❌ 请求失败: ${error.response?.data?.message || error.message}`); + return false; + } + } +} + +// 测试登录速率限制 +async function testLoginRateLimit() { + console.log('\n=== 测试登录速率限制 ==='); + + for (let i = 1; i <= 7; i++) { + const success = await testRateLimit( + '/api/auth/login', + 'POST', + { + username: `testuser${i}`, + password: 'testpass123' + }, + `登录尝试 ${i}` + ); + + if (!success) { + console.log('✅ 登录速率限制正常工作'); + break; + } + + if (i === 6) { + console.log('⚠️ 登录速率限制可能未生效'); + } + } +} + +// 测试OAuth授权速率限制 +async function testOAuthRateLimit() { + console.log('\n=== 测试OAuth授权速率限制 ==='); + + for (let i = 1; i <= 5; i++) { + const success = await testRateLimit( + '/api/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://localhost:3000/callback&scope=read&state=test', + 'GET', + null, + `OAuth授权请求 ${i}` + ); + + if (!success) { + console.log('✅ OAuth授权速率限制正常工作'); + break; + } + + if (i === 4) { + console.log('⚠️ OAuth授权速率限制可能未生效'); + } + } +} + +// 测试通用速率限制 +async function testGeneralRateLimit() { + console.log('\n=== 测试通用速率限制 ==='); + + for (let i = 1; i <= 105; i++) { + const success = await testRateLimit( + '/health', + 'GET', + null, + `健康检查请求 ${i}` + ); + + if (!success) { + console.log('✅ 通用速率限制正常工作'); + break; + } + + if (i === 101) { + console.log('⚠️ 通用速率限制可能未生效'); + } + } +} + +// 主测试函数 +async function runTests() { + console.log('🚀 开始测试速率限制功能...'); + + try { + await testGeneralRateLimit(); + await testLoginRateLimit(); + await testOAuthRateLimit(); + + console.log('\n✅ 速率限制测试完成'); + } catch (error) { + console.error('❌ 测试过程中发生错误:', error.message); + } +} + +// 运行测试 +if (require.main === module) { + runTests(); +} + +module.exports = { + testRateLimit, + testLoginRateLimit, + testOAuthRateLimit, + testGeneralRateLimit +}; \ No newline at end of file