支持速率限制
然后把oauth单独放一个页面 一些UI调整
This commit is contained in:
@ -23,6 +23,12 @@
|
|||||||
- 自动清理过期令牌
|
- 自动清理过期令牌
|
||||||
- 重定向URI验证
|
- 重定向URI验证
|
||||||
- 客户端所有权验证
|
- 客户端所有权验证
|
||||||
|
- **速率限制** - 针对不同重要程度的接口设置不同限制
|
||||||
|
- 通用接口:15分钟内最多100次请求
|
||||||
|
- 登录/注册:15分钟内最多5次尝试
|
||||||
|
- OAuth授权:15分钟内最多3次尝试
|
||||||
|
- OAuth令牌:15分钟内最多10次请求
|
||||||
|
- OAuth客户端管理:15分钟内最多20次操作
|
||||||
|
|
||||||
### 📊 OAuth客户端管理
|
### 📊 OAuth客户端管理
|
||||||
- 创建OAuth客户端
|
- 创建OAuth客户端
|
||||||
|
4
index.js
4
index.js
@ -8,6 +8,7 @@ const OAuthToken = require('./models/OAuthToken');
|
|||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
const oauthRoutes = require('./routes/oauth');
|
const oauthRoutes = require('./routes/oauth');
|
||||||
const oauthClientRoutes = require('./routes/oauth-clients');
|
const oauthClientRoutes = require('./routes/oauth-clients');
|
||||||
|
const { generalLimiter } = require('./middleware/rateLimit');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@ -17,6 +18,9 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// 应用通用速率限制
|
||||||
|
app.use(generalLimiter);
|
||||||
|
|
||||||
// 请求日志中间件
|
// 请求日志中间件
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
|
||||||
|
69
middleware/rateLimit.js
Normal file
69
middleware/rateLimit.js
Normal file
@ -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
|
||||||
|
};
|
37
package.json
37
package.json
@ -10,33 +10,42 @@
|
|||||||
"test:oauth": "node test-oauth.js",
|
"test:oauth": "node test-oauth.js",
|
||||||
"test:oauth-flow": "node test-oauth-flow.js",
|
"test:oauth-flow": "node test-oauth-flow.js",
|
||||||
"test:redirect-uri": "node test-redirect-uri.js",
|
"test:redirect-uri": "node test-redirect-uri.js",
|
||||||
|
"test:rate-limit": "node test-rate-limit.js",
|
||||||
"dev:frontend": "vite",
|
"dev:frontend": "vite",
|
||||||
"build:frontend": "vite build",
|
"build:frontend": "vite build",
|
||||||
"preview:frontend": "vite preview"
|
"preview:frontend": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
|
||||||
"pg": "^8.11.3",
|
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"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": {
|
"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/react": "^11.11.0",
|
||||||
"@emotion/styled": "^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": "",
|
"author": "",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -20,6 +20,9 @@ importers:
|
|||||||
express:
|
express:
|
||||||
specifier: ^4.18.2
|
specifier: ^4.18.2
|
||||||
version: 4.21.2
|
version: 4.21.2
|
||||||
|
express-rate-limit:
|
||||||
|
specifier: ^8.0.1
|
||||||
|
version: 8.0.1(express@4.21.2)
|
||||||
express-validator:
|
express-validator:
|
||||||
specifier: ^7.0.1
|
specifier: ^7.0.1
|
||||||
version: 7.2.1
|
version: 7.2.1
|
||||||
@ -698,6 +701,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
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:
|
express-validator@7.2.1:
|
||||||
resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==}
|
resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@ -803,6 +812,10 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
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:
|
ipaddr.js@1.9.1:
|
||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@ -1926,6 +1939,11 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
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:
|
express-validator@7.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
@ -2067,6 +2085,8 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
ip-address@10.0.1: {}
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
is-arrayish@0.2.1: {}
|
is-arrayish@0.2.1: {}
|
||||||
|
@ -6,11 +6,12 @@ const {
|
|||||||
loginValidation,
|
loginValidation,
|
||||||
handleValidationErrors
|
handleValidationErrors
|
||||||
} = require('../middleware/validation');
|
} = require('../middleware/validation');
|
||||||
|
const { authLimiter } = require('../middleware/rateLimit');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// 注册路由
|
// 注册路由
|
||||||
router.post('/register', registerValidation, handleValidationErrors, async (req, res) => {
|
router.post('/register', authLimiter, registerValidation, handleValidationErrors, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, email, password } = req.body;
|
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 {
|
try {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const { body, validationResult } = require('express-validator');
|
const { body, validationResult } = require('express-validator');
|
||||||
const OAuthClient = require('../models/OAuthClient');
|
const OAuthClient = require('../models/OAuthClient');
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
|
const { oauthClientLimiter } = require('../middleware/rateLimit');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ const validateRedirectUris = (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. 创建OAuth客户端
|
// 1. 创建OAuth客户端
|
||||||
router.post('/clients', authenticateToken, createClientValidation, validateRedirectUris, handleValidationErrors, async (req, res) => {
|
router.post('/clients', oauthClientLimiter, authenticateToken, createClientValidation, validateRedirectUris, handleValidationErrors, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name, description, redirect_uris, scopes } = req.body;
|
const { name, description, redirect_uris, scopes } = req.body;
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
|
@ -5,6 +5,7 @@ const OAuthToken = require('../models/OAuthToken');
|
|||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
const { authenticateOAuthToken, requireScope } = require('../middleware/oauth');
|
const { authenticateOAuthToken, requireScope } = require('../middleware/oauth');
|
||||||
|
const { oauthAuthLimiter, oauthTokenLimiter } = require('../middleware/rateLimit');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ const handleValidationErrors = (req, res, next) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 1. 授权端点 - 验证参数并返回授权信息
|
// 1. 授权端点 - 验证参数并返回授权信息
|
||||||
router.get('/authorize', authenticateToken, async (req, res) => {
|
router.get('/authorize', oauthAuthLimiter, authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
response_type,
|
response_type,
|
||||||
@ -141,8 +142,8 @@ router.get('/authorize', authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 用户同意授权端点
|
// 2. 用户同意/拒绝授权端点
|
||||||
router.post('/authorize/consent', authenticateToken, async (req, res) => {
|
router.post('/authorize/consent', oauthAuthLimiter, authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
client_id,
|
client_id,
|
||||||
@ -247,7 +248,7 @@ router.post('/authorize/consent', authenticateToken, async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 3. 令牌端点 - 交换授权码获取访问令牌
|
// 3. 令牌端点 - 交换授权码获取访问令牌
|
||||||
router.post('/token', [
|
router.post('/token', oauthTokenLimiter, [
|
||||||
body('grant_type').notEmpty().withMessage('grant_type不能为空'),
|
body('grant_type').notEmpty().withMessage('grant_type不能为空'),
|
||||||
body('client_id').notEmpty().withMessage('client_id不能为空'),
|
body('client_id').notEmpty().withMessage('client_id不能为空'),
|
||||||
body('client_secret').notEmpty().withMessage('client_secret不能为空'),
|
body('client_secret').notEmpty().withMessage('client_secret不能为空'),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React from 'react'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
@ -9,89 +9,27 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
CardActions,
|
Divider
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
ListItemText,
|
|
||||||
ListItemIcon,
|
|
||||||
Divider,
|
|
||||||
Chip,
|
|
||||||
Dialog,
|
|
||||||
DialogTitle,
|
|
||||||
DialogContent,
|
|
||||||
DialogActions,
|
|
||||||
TextField,
|
|
||||||
Alert
|
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import {
|
import {
|
||||||
Person,
|
Person,
|
||||||
Email,
|
Email,
|
||||||
CalendarToday,
|
CalendarToday,
|
||||||
Security,
|
Logout,
|
||||||
Add,
|
Security
|
||||||
Delete,
|
|
||||||
Visibility,
|
|
||||||
VisibilityOff,
|
|
||||||
Logout
|
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import axios from 'axios'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const [clients, setClients] = useState([])
|
const navigate = useNavigate()
|
||||||
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 handleLogout = () => {
|
const handleLogout = () => {
|
||||||
logout()
|
logout()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
<Container maxWidth="md" sx={{ py: 4 }}>
|
||||||
<Box sx={{ mb: 4 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
个人中心
|
个人中心
|
||||||
@ -101,171 +39,74 @@ const Dashboard = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
|
|
||||||
{error}
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* 用户信息卡片 */}
|
{/* 用户信息卡片 */}
|
||||||
<Grid item xs={12} md={4}>
|
<Grid item xs={12}>
|
||||||
<Paper elevation={2} sx={{ p: 3 }}>
|
<Card>
|
||||||
<Box display="flex" alignItems="center" mb={2}>
|
<CardContent>
|
||||||
<Person sx={{ mr: 1, color: 'primary.main' }} />
|
<Typography variant="h6" gutterBottom>
|
||||||
<Typography variant="h6">用户信息</Typography>
|
用户信息
|
||||||
</Box>
|
</Typography>
|
||||||
<List>
|
<Divider sx={{ mb: 2 }} />
|
||||||
<ListItem>
|
|
||||||
<ListItemIcon>
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
<Person />
|
<Person sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
</ListItemIcon>
|
<Box>
|
||||||
<ListItemText primary="用户名" secondary={user?.username} />
|
<Typography variant="body2" color="text.secondary">
|
||||||
</ListItem>
|
用户名
|
||||||
<ListItem>
|
</Typography>
|
||||||
<ListItemIcon>
|
<Typography variant="body1">
|
||||||
<Email />
|
{user?.username}
|
||||||
</ListItemIcon>
|
</Typography>
|
||||||
<ListItemText primary="邮箱" secondary={user?.email} />
|
</Box>
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CalendarToday />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
primary="注册时间"
|
|
||||||
secondary={new Date(user?.created_at).toLocaleDateString()}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
startIcon={<Logout />}
|
|
||||||
onClick={handleLogout}
|
|
||||||
fullWidth
|
|
||||||
>
|
|
||||||
退出登录
|
|
||||||
</Button>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
{/* OAuth客户端管理 */}
|
|
||||||
<Grid item xs={12} md={8}>
|
|
||||||
<Paper elevation={2} sx={{ p: 3 }}>
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
|
||||||
<Box display="flex" alignItems="center">
|
|
||||||
<Security sx={{ mr: 1, color: 'primary.main' }} />
|
|
||||||
<Typography variant="h6">OAuth客户端</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
|
||||||
variant="contained"
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
startIcon={<Add />}
|
<Email sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
onClick={() => setOpenCreateDialog(true)}
|
<Box>
|
||||||
>
|
<Typography variant="body2" color="text.secondary">
|
||||||
创建客户端
|
邮箱
|
||||||
</Button>
|
</Typography>
|
||||||
</Box>
|
<Typography variant="body1">
|
||||||
|
{user?.email}
|
||||||
{clients.length === 0 ? (
|
</Typography>
|
||||||
<Box textAlign="center" py={4}>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
还没有OAuth客户端,点击上方按钮创建一个
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
|
||||||
<Grid container spacing={2}>
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
{clients.map((client) => (
|
<CalendarToday sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
<Grid item xs={12} key={client.client_id}>
|
<Box>
|
||||||
<Card variant="outlined">
|
<Typography variant="body2" color="text.secondary">
|
||||||
<CardContent>
|
注册时间
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
</Typography>
|
||||||
<Box>
|
<Typography variant="body1">
|
||||||
<Typography variant="h6" gutterBottom>
|
{user?.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '未知'}
|
||||||
{client.name}
|
</Typography>
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
</Box>
|
||||||
{client.description}
|
</CardContent>
|
||||||
</Typography>
|
</Card>
|
||||||
<Box mt={1}>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
客户端ID: {client.client_id}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box mt={1}>
|
|
||||||
{client.scopes.map((scope) => (
|
|
||||||
<Chip
|
|
||||||
key={scope}
|
|
||||||
label={scope}
|
|
||||||
size="small"
|
|
||||||
sx={{ mr: 0.5, mb: 0.5 }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<Button
|
|
||||||
color="error"
|
|
||||||
size="small"
|
|
||||||
onClick={() => handleDeleteClient(client.client_id)}
|
|
||||||
>
|
|
||||||
<Delete />
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 创建客户端对话框 */}
|
{/* 功能按钮 */}
|
||||||
<Dialog open={openCreateDialog} onClose={() => setOpenCreateDialog(false)} maxWidth="sm" fullWidth>
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 2 }}>
|
||||||
<DialogTitle>创建OAuth客户端</DialogTitle>
|
<Button
|
||||||
<DialogContent>
|
variant="outlined"
|
||||||
<TextField
|
startIcon={<Security />}
|
||||||
fullWidth
|
onClick={() => navigate('/oauth/authorize')}
|
||||||
label="客户端名称"
|
>
|
||||||
value={newClient.name}
|
OAuth 管理
|
||||||
onChange={(e) => setNewClient({ ...newClient, name: e.target.value })}
|
</Button>
|
||||||
margin="normal"
|
<Button
|
||||||
required
|
variant="outlined"
|
||||||
/>
|
color="error"
|
||||||
<TextField
|
onClick={handleLogout}
|
||||||
fullWidth
|
startIcon={<Logout />}
|
||||||
label="描述"
|
>
|
||||||
value={newClient.description}
|
退出登录
|
||||||
onChange={(e) => setNewClient({ ...newClient, description: e.target.value })}
|
</Button>
|
||||||
margin="normal"
|
</Box>
|
||||||
multiline
|
|
||||||
rows={2}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="重定向URI"
|
|
||||||
value={newClient.redirect_uris[0]}
|
|
||||||
onChange={(e) => setNewClient({
|
|
||||||
...newClient,
|
|
||||||
redirect_uris: [e.target.value]
|
|
||||||
})}
|
|
||||||
margin="normal"
|
|
||||||
required
|
|
||||||
placeholder="http://localhost:3001/callback"
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => setOpenCreateDialog(false)}>取消</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateClient}
|
|
||||||
variant="contained"
|
|
||||||
disabled={loading || !newClient.name}
|
|
||||||
>
|
|
||||||
{loading ? '创建中...' : '创建'}
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,22 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
|
CardActions,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Chip,
|
Chip,
|
||||||
Alert,
|
Alert,
|
||||||
CircularProgress
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Divider,
|
||||||
|
Tabs,
|
||||||
|
Tab
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import {
|
import {
|
||||||
Security,
|
Security,
|
||||||
@ -25,18 +34,30 @@ import {
|
|||||||
Person,
|
Person,
|
||||||
Email,
|
Email,
|
||||||
CalendarToday,
|
CalendarToday,
|
||||||
Visibility,
|
Add,
|
||||||
VisibilityOff
|
Delete,
|
||||||
|
Settings,
|
||||||
|
Apps,
|
||||||
|
Logout
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const OAuthAuthorize = () => {
|
const OAuthAuthorize = () => {
|
||||||
const { user } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [clientInfo, setClientInfo] = useState(null)
|
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 clientId = searchParams.get('client_id')
|
||||||
const redirectUri = searchParams.get('redirect_uri')
|
const redirectUri = searchParams.get('redirect_uri')
|
||||||
@ -45,53 +66,85 @@ const OAuthAuthorize = () => {
|
|||||||
const responseType = searchParams.get('response_type')
|
const responseType = searchParams.get('response_type')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 验证OAuth参数
|
// 如果有OAuth参数,显示授权页面
|
||||||
if (!clientId || !redirectUri || responseType !== 'code') {
|
if (clientId && redirectUri && responseType === 'code') {
|
||||||
setError('无效的授权请求')
|
setActiveTab(1)
|
||||||
|
fetchAuthInfo()
|
||||||
return
|
} else {
|
||||||
|
// 否则显示客户端管理页面
|
||||||
|
setActiveTab(0)
|
||||||
|
fetchClients()
|
||||||
}
|
}
|
||||||
|
}, [clientId, redirectUri, responseType])
|
||||||
|
|
||||||
// 获取授权信息
|
const fetchClients = async () => {
|
||||||
const fetchAuthInfo = async () => {
|
try {
|
||||||
try {
|
const response = await axios.get('/api/oauth/clients')
|
||||||
setLoading(true)
|
setClients(response.data.data.clients)
|
||||||
const params = new URLSearchParams({
|
} catch (error) {
|
||||||
response_type: 'code',
|
setError('获取客户端列表失败')
|
||||||
client_id: clientId,
|
}
|
||||||
redirect_uri: redirectUri,
|
}
|
||||||
scope: scope || 'read write',
|
|
||||||
state: state || ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const response = await axios.get(`/api/oauth/authorize?${params}`, {
|
const fetchAuthInfo = async () => {
|
||||||
headers: {
|
try {
|
||||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
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) {
|
const response = await axios.get(`/api/oauth/authorize?${params}`, {
|
||||||
console.log(response.data.data.redirect_uri)
|
headers: {
|
||||||
setClientInfo({
|
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||||
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 || '获取授权信息失败')
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
setError(error.response?.data?.message || '获取授权信息失败')
|
setError('删除客户端失败')
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fetchAuthInfo()
|
|
||||||
}, [clientId, redirectUri, scope, responseType, state])
|
|
||||||
|
|
||||||
const handleAuthorize = async () => {
|
const handleAuthorize = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -143,8 +196,13 @@ const OAuthAuthorize = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout()
|
||||||
|
navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
if (loading) {
|
if (loading && activeTab === 1) {
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="sm">
|
<Container maxWidth="sm">
|
||||||
<Box
|
<Box
|
||||||
@ -162,129 +220,327 @@ const OAuthAuthorize = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="sm">
|
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||||
<Box
|
<Box sx={{ mb: 4 }}>
|
||||||
sx={{
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
minHeight: '100vh',
|
OAuth 管理
|
||||||
display: 'flex',
|
</Typography>
|
||||||
alignItems: 'center',
|
<Typography variant="body1" color="text.secondary">
|
||||||
justifyContent: 'center'
|
管理OAuth客户端和第三方应用授权
|
||||||
}}
|
</Typography>
|
||||||
>
|
</Box>
|
||||||
<Paper
|
|
||||||
elevation={3}
|
|
||||||
sx={{
|
|
||||||
p: 4,
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 500
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box textAlign="center" mb={3}>
|
|
||||||
<Security sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
|
||||||
<Typography variant="h4" component="h1" gutterBottom>
|
|
||||||
授权请求
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
第三方应用请求访问您的账户
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError('')}>
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{clientInfo && (
|
{/* 标签页 */}
|
||||||
<>
|
<Paper sx={{ mb: 3 }}>
|
||||||
{/* 应用信息 */}
|
<Tabs value={activeTab} onChange={(e, newValue) => setActiveTab(newValue)}>
|
||||||
<Card variant="outlined" sx={{ mb: 3 }}>
|
<Tab
|
||||||
<CardContent>
|
icon={<Apps />}
|
||||||
<Typography variant="h6" gutterBottom>
|
label="客户端管理"
|
||||||
{clientInfo.name}
|
iconPosition="start"
|
||||||
</Typography>
|
/>
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
<Tab
|
||||||
{clientInfo.description}
|
icon={<Security />}
|
||||||
</Typography>
|
label="授权管理"
|
||||||
<Box mt={2}>
|
iconPosition="start"
|
||||||
<Typography variant="body2" color="text.secondary" gutterBottom>
|
/>
|
||||||
请求的权限:
|
</Tabs>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 客户端管理标签页 */}
|
||||||
|
{activeTab === 0 && (
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
{/* 用户信息卡片 */}
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
用户信息
|
||||||
|
</Typography>
|
||||||
|
<Divider sx={{ mb: 2 }} />
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Person sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
用户名
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{user?.username}
|
||||||
</Typography>
|
</Typography>
|
||||||
{clientInfo.scopes.map((scope) => (
|
|
||||||
<Chip
|
|
||||||
key={scope}
|
|
||||||
label={scope === 'read' ? '读取信息' : scope === 'write' ? '写入信息' : scope}
|
|
||||||
size="small"
|
|
||||||
sx={{ mr: 0.5, mb: 0.5 }}
|
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</Box>
|
||||||
</Card>
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||||
|
<Email sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
邮箱
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{user?.email}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<CalendarToday sx={{ mr: 2, color: 'primary.main' }} />
|
||||||
|
<Box>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
注册时间
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1">
|
||||||
|
{user?.created_at ? new Date(user.created_at).toLocaleDateString('zh-CN') : '未知'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{/* 用户信息 */}
|
{/* OAuth客户端管理 */}
|
||||||
<Card variant="outlined" sx={{ mb: 3 }}>
|
<Grid item xs={12} md={8}>
|
||||||
<CardContent>
|
<Card>
|
||||||
<Typography variant="h6" gutterBottom>
|
<CardContent>
|
||||||
您的账户信息
|
<Box display="flex" justifyContent="space-between" alignItems="center" mb={3}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
OAuth客户端
|
||||||
</Typography>
|
</Typography>
|
||||||
<List dense>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Person />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="用户名" secondary={user.username} />
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Email />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText primary="邮箱" secondary={user.email} />
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemIcon>
|
|
||||||
<CalendarToday />
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
|
||||||
primary="注册时间"
|
|
||||||
secondary={new Date(user.created_at).toLocaleDateString()}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
|
||||||
variant="outlined"
|
|
||||||
color="error"
|
|
||||||
startIcon={<Cancel />}
|
|
||||||
onClick={handleDeny}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
拒绝
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={6}>
|
|
||||||
<Button
|
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
startIcon={<CheckCircle />}
|
startIcon={<Add />}
|
||||||
onClick={handleAuthorize}
|
onClick={() => setOpenCreateDialog(true)}
|
||||||
disabled={loading}
|
|
||||||
>
|
>
|
||||||
{loading ? '授权中...' : '授权'}
|
创建客户端
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Box>
|
||||||
|
|
||||||
|
{clients.length === 0 ? (
|
||||||
|
<Box textAlign="center" py={4}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
还没有OAuth客户端,点击上方按钮创建一个
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{clients.map((client) => (
|
||||||
|
<Grid item xs={12} key={client.client_id}>
|
||||||
|
<Card variant="outlined">
|
||||||
|
<CardContent>
|
||||||
|
<Box display="flex" justifyContent="space-between" alignItems="flex-start">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{client.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
{client.description}
|
||||||
|
</Typography>
|
||||||
|
<Box mt={1}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
客户端ID: {client.client_id}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box mt={1}>
|
||||||
|
{client.scopes.map((scope) => (
|
||||||
|
<Chip
|
||||||
|
key={scope}
|
||||||
|
label={scope}
|
||||||
|
size="small"
|
||||||
|
sx={{ mr: 0.5, mb: 0.5 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
color="error"
|
||||||
|
size="small"
|
||||||
|
onClick={() => handleDeleteClient(client.client_id)}
|
||||||
|
>
|
||||||
|
<Delete />
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 授权管理标签页 */}
|
||||||
|
{activeTab === 1 && clientInfo && (
|
||||||
|
<Container maxWidth="sm">
|
||||||
|
<Paper elevation={3} sx={{ p: 4 }}>
|
||||||
|
<Box textAlign="center" mb={3}>
|
||||||
|
<Security sx={{ fontSize: 48, color: 'primary.main', mb: 2 }} />
|
||||||
|
<Typography variant="h4" component="h1" gutterBottom>
|
||||||
|
授权请求
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
第三方应用请求访问您的账户
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 应用信息 */}
|
||||||
|
<Card variant="outlined" sx={{ mb: 3 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
{clientInfo.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
{clientInfo.description}
|
||||||
|
</Typography>
|
||||||
|
<Box mt={2}>
|
||||||
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
||||||
|
请求的权限:
|
||||||
|
</Typography>
|
||||||
|
{clientInfo.scopes.map((scope) => (
|
||||||
|
<Chip
|
||||||
|
key={scope}
|
||||||
|
label={scope === 'read' ? '读取信息' : scope === 'write' ? '写入信息' : scope}
|
||||||
|
size="small"
|
||||||
|
sx={{ mr: 0.5, mb: 0.5 }}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 用户信息 */}
|
||||||
|
<Card variant="outlined" sx={{ mb: 3 }}>
|
||||||
|
<CardContent>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
您的账户信息
|
||||||
|
</Typography>
|
||||||
|
<List dense>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Person />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="用户名" secondary={user.username} />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Email />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="邮箱" secondary={user.email} />
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemIcon>
|
||||||
|
<CalendarToday />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary="注册时间"
|
||||||
|
secondary={new Date(user.created_at).toLocaleDateString()}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 操作按钮 */}
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={<Cancel />}
|
||||||
|
onClick={handleDeny}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
拒绝
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
<Grid item xs={6}>
|
||||||
)}
|
<Button
|
||||||
</Paper>
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<CheckCircle />}
|
||||||
|
onClick={handleAuthorize}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '授权中...' : '授权'}
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
</Container>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 创建客户端对话框 */}
|
||||||
|
<Dialog open={openCreateDialog} onClose={() => setOpenCreateDialog(false)} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>创建OAuth客户端</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="客户端名称"
|
||||||
|
value={newClient.name}
|
||||||
|
onChange={(e) => setNewClient({ ...newClient, name: e.target.value })}
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="描述"
|
||||||
|
value={newClient.description}
|
||||||
|
onChange={(e) => setNewClient({ ...newClient, description: e.target.value })}
|
||||||
|
margin="normal"
|
||||||
|
multiline
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="重定向URI"
|
||||||
|
value={newClient.redirect_uris[0]}
|
||||||
|
onChange={(e) => setNewClient({
|
||||||
|
...newClient,
|
||||||
|
redirect_uris: [e.target.value]
|
||||||
|
})}
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
placeholder="http://localhost:3001/callback"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpenCreateDialog(false)}>取消</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCreateClient}
|
||||||
|
variant="contained"
|
||||||
|
disabled={loading || !newClient.name}
|
||||||
|
>
|
||||||
|
{loading ? '创建中...' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 底部操作按钮 */}
|
||||||
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
>
|
||||||
|
返回个人中心
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
onClick={handleLogout}
|
||||||
|
startIcon={<Logout />}
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
133
test-rate-limit.js
Normal file
133
test-rate-limit.js
Normal file
@ -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
|
||||||
|
};
|
Reference in New Issue
Block a user