v.0.2.0-beta

目前oauth已经可以正常使用
This commit is contained in:
2025-07-29 17:20:26 -07:00
parent 0c481c7a0e
commit 66a901c676
7 changed files with 943 additions and 43 deletions

View File

@ -192,10 +192,105 @@ npm run dev:frontend
### 4. 测试OAuth流程
#### 方法一:使用前端界面
1. 注册/登录用户
2. 在个人中心创建OAuth客户端
3. 访问授权页面:`http://localhost:3001/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3001/callback&scope=read%20write&state=test123`
4. 完成授权流程
4. 在授权页面查看应用信息和请求的权限
5. 选择"同意授权"或"拒绝授权"
6. 系统会重定向到第三方应用并附带授权码或错误信息
#### 方法二:使用第三方应用示例
1. 启动后端和前端服务
2. 在个人中心创建OAuth客户端
3. 打开第三方应用示例:`http://localhost:3001/third-party-app.html`
4. 填入客户端ID和密钥
5. 点击"开始OAuth授权"按钮
6. 完成授权流程并查看API响应
#### 方法三:使用测试脚本
```bash
# 运行完整的OAuth流程测试
npm run test:oauth-flow
```
### 5. OAuth 2.0 授权流程详解
#### 完整的授权码流程 (Authorization Code Flow)
**步骤 1: 获取授权信息**
```
GET /api/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:3001/callback&scope=read%20write&state=test123
```
**响应示例:**
```json
{
"success": true,
"message": "授权信息获取成功",
"data": {
"client": {
"id": "YOUR_CLIENT_ID",
"name": "应用名称",
"description": "应用描述"
},
"scopes": ["read", "write"],
"state": "test123",
"redirect_uri": "http://localhost:3001/callback"
}
}
```
**步骤 2: 用户同意/拒绝授权**
用户在前端授权页面选择同意或拒绝授权:
**同意授权:**
```
POST /api/oauth/authorize/consent
Content-Type: application/json
Authorization: Bearer USER_JWT_TOKEN
{
"client_id": "YOUR_CLIENT_ID",
"redirect_uri": "http://localhost:3001/callback",
"scope": "read write",
"state": "test123",
"approved": true
}
```
**拒绝授权:**
```
POST /api/oauth/authorize/consent
Content-Type: application/json
Authorization: Bearer USER_JWT_TOKEN
{
"client_id": "YOUR_CLIENT_ID",
"redirect_uri": "http://localhost:3001/callback",
"scope": "read write",
"state": "test123",
"approved": false
}
```
**步骤 3: 获取授权码或错误**
- **同意授权**:重定向到 `redirect_uri` 并附带授权码
```
http://localhost:3001/callback?code=AUTH_CODE&state=test123
```
- **拒绝授权**:重定向到 `redirect_uri` 并附带错误信息
```
http://localhost:3001/callback?error=access_denied&error_description=用户拒绝授权&state=test123
```
**步骤 4: 使用授权码交换访问令牌**
```
POST /api/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&code=AUTH_CODE&redirect_uri=http://localhost:3001/callback
```
## 前端技术栈

165
demo-oauth-flow.md Normal file
View File

@ -0,0 +1,165 @@
# OAuth 2.0 授权流程演示
## 概述
本演示展示了完整的OAuth 2.0授权码流程,包括用户同意授权的步骤。
## 系统架构
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 第三方应用 │ │ OAuth提供商 │ │ 用户 │
│ (Client App)│ │ (Your App) │ │ (User) │
└─────────────┘ └─────────────┘ └─────────────┘
```
## 完整流程演示
### 步骤 1: 启动服务
```bash
# 启动后端API服务
npm start
# 启动前端服务
npm run dev:frontend
```
### 步骤 2: 用户注册和登录
1. 访问 `http://localhost:3001`
2. 注册新用户或登录现有用户
3. 进入个人中心
### 步骤 3: 创建OAuth客户端
1. 在个人中心点击"创建OAuth客户端"
2. 填写应用信息:
- 应用名称:测试应用
- 应用描述这是一个测试OAuth流程的应用
- 重定向URI`http://localhost:3001/third-party-app.html`
3. 创建客户端并记录客户端ID和密钥
### 步骤 4: 使用第三方应用示例
1. 打开第三方应用示例:`http://localhost:3001/third-party-app.html`
2. 填入刚才创建的客户端ID和密钥
3. 点击"开始OAuth授权"按钮
### 步骤 5: 授权流程
1. 系统重定向到授权页面:`http://localhost:3001/oauth/authorize`
2. 用户看到应用信息和请求的权限
3. 用户选择"同意授权"或"拒绝授权"
### 步骤 6: 获取访问令牌
1. 如果用户同意授权,系统会重定向回第三方应用
2. 第三方应用使用授权码交换访问令牌
3. 使用访问令牌获取用户信息
## API 端点说明
### 1. 获取授权信息
```
GET /api/oauth/authorize
```
- 验证OAuth请求参数
- 返回应用信息和请求的权限
- 不直接生成授权码
### 2. 用户同意/拒绝授权
```
POST /api/oauth/authorize/consent
```
- 处理用户的授权决定
- 如果同意,生成授权码
- 如果拒绝,返回错误信息
### 3. 交换访问令牌
```
POST /api/oauth/token
```
- 使用授权码交换访问令牌
- 返回访问令牌和刷新令牌
### 4. 获取用户信息
```
GET /api/oauth/userinfo
```
- 使用访问令牌获取用户信息
## 安全特性
### 1. 用户同意机制
- ✅ 用户必须明确同意才能获得授权码
- ✅ 显示详细的应用信息和权限范围
- ✅ 用户可以拒绝授权
### 2. 令牌安全
- ✅ 访问令牌有过期时间
- ✅ 支持刷新令牌机制
- ✅ 令牌可以撤销
### 3. 客户端验证
- ✅ 验证客户端ID和密钥
- ✅ 验证重定向URI
- ✅ 防止重放攻击
## 测试方法
### 1. 自动化测试
```bash
npm run test:oauth-flow
```
### 2. 手动测试
1. 使用前端界面测试
2. 使用第三方应用示例测试
3. 使用curl命令测试
### 3. 错误测试
- 测试无效的客户端ID
- 测试无效的重定向URI
- 测试过期的授权码
- 测试用户拒绝授权
## 常见问题
### Q: 为什么需要用户同意?
A: 这是OAuth 2.0的核心安全特性,确保用户对第三方应用的授权是明确和可控的。
### Q: 授权码可以重复使用吗?
A: 不可以,授权码只能使用一次,使用后立即失效。
### Q: 访问令牌过期怎么办?
A: 使用刷新令牌获取新的访问令牌,无需用户重新授权。
### Q: 如何撤销授权?
A: 可以通过撤销刷新令牌来撤销授权,用户需要重新授权。
## 扩展功能
### 1. 权限范围管理
- 支持细粒度的权限控制
- 用户可以部分授权某些权限
### 2. 应用管理
- 用户可以查看和管理已授权的应用
- 支持撤销特定应用的授权
### 3. 审计日志
- 记录所有授权和撤销操作
- 提供安全审计功能
## 总结
这个OAuth 2.0实现提供了完整的授权码流程,包括:
1. ✅ 用户同意机制
2. ✅ 安全的令牌管理
3. ✅ 完整的API端点
4. ✅ 友好的用户界面
5. ✅ 详细的测试和文档
这确保了OAuth授权的安全性和用户体验的平衡。

View File

@ -8,6 +8,8 @@
"dev": "nodemon index.js",
"test": "node test-api.js",
"test:oauth": "node test-oauth.js",
"test:oauth-flow": "node test-oauth-flow.js",
"test:redirect-uri": "node test-redirect-uri.js",
"dev:frontend": "vite",
"build:frontend": "vite build",
"preview:frontend": "vite preview"

289
public/third-party-app.html Normal file
View File

@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>第三方应用示例</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.app-container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.status-card {
transition: all 0.3s ease;
}
.status-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center align-items-center min-vh-100">
<div class="col-md-8 col-lg-6">
<div class="app-container p-4">
<div class="text-center mb-4">
<i class="fas fa-rocket fa-3x text-primary mb-3"></i>
<h2 class="fw-bold">第三方应用示例</h2>
<p class="text-muted">演示OAuth 2.0授权流程</p>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card status-card h-100">
<div class="card-body text-center">
<i class="fas fa-user-lock fa-2x text-warning mb-2"></i>
<h5 class="card-title">用户状态</h5>
<p class="card-text" id="userStatus">未登录</p>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card status-card h-100">
<div class="card-body text-center">
<i class="fas fa-key fa-2x text-success mb-2"></i>
<h5 class="card-title">访问令牌</h5>
<p class="card-text" id="tokenStatus">未获取</p>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-cog me-2"></i>应用配置
</h5>
<div class="row">
<div class="col-md-6">
<label class="form-label">客户端ID</label>
<input type="text" class="form-control" id="clientId" placeholder="输入客户端ID">
</div>
<div class="col-md-6">
<label class="form-label">客户端密钥</label>
<input type="password" class="form-control" id="clientSecret" placeholder="输入客户端密钥">
</div>
</div>
</div>
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary btn-lg" onclick="startOAuthFlow()">
<i class="fas fa-sign-in-alt me-2"></i>开始OAuth授权
</button>
<button class="btn btn-outline-secondary" onclick="getUserInfo()" id="getUserInfoBtn" disabled>
<i class="fas fa-user me-2"></i>获取用户信息
</button>
</div>
<div class="mt-4">
<div class="alert alert-info" role="alert">
<h6><i class="fas fa-info-circle me-2"></i>使用说明</h6>
<ol class="mb-0">
<li>在个人中心创建OAuth客户端</li>
<li>将客户端ID和密钥填入上方配置</li>
<li>点击"开始OAuth授权"按钮</li>
<li>在授权页面选择同意或拒绝</li>
<li>授权成功后可以获取用户信息</li>
</ol>
</div>
</div>
<div class="mt-3">
<div class="card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-terminal me-2"></i>API响应
</h6>
</div>
<div class="card-body">
<pre id="apiResponse" class="bg-light p-3 rounded"
style="max-height: 200px; overflow-y: auto;">等待操作...</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
let accessToken = '';
let userInfo = null;
// 检查URL参数
function checkUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const error = urlParams.get('error');
const state = urlParams.get('state');
if (code) {
logResponse('收到授权码', { code, state });
exchangeCodeForToken(code);
} else if (error) {
logResponse('授权被拒绝', { error, error_description: urlParams.get('error_description'), state });
updateStatus('用户拒绝授权', 'error');
}
}
// 开始OAuth流程
function startOAuthFlow() {
const clientId = document.getElementById('clientId').value.trim();
const clientSecret = document.getElementById('clientSecret').value.trim();
if (!clientId || !clientSecret) {
alert('请输入客户端ID和密钥');
return;
}
// 保存到 localStorage
localStorage.setItem('client_id', clientId);
localStorage.setItem('client_secret', clientSecret);
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname);
const scope = encodeURIComponent('read write');
const state = 'test123';
const authUrl = `http://localhost:3001/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}&response_type=code`;
logResponse('重定向到授权页面', { authUrl });
window.location.href = authUrl;
}
// 使用授权码交换访问令牌
async function exchangeCodeForToken(code) {
let clientId = document.getElementById('clientId').value.trim();
let clientSecret = document.getElementById('clientSecret').value.trim();
if (!clientId || !clientSecret) {
clientId = localStorage.getItem('client_id');
clientSecret = localStorage.getItem('client_secret');
}
if (!clientId || !clientSecret) {
logResponse('错误', { message: '请先配置客户端ID和密钥' });
return;
}
try {
const redirectUri = window.location.origin + window.location.pathname;
// const redirectUri = window.location.href;
console.log(redirectUri)
const response = await fetch('http://localhost:3000/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: code,
redirect_uri: redirectUri
})
});
const result = await response.json();
if (result.success) {
accessToken = result.data.access_token;
updateStatus('已登录', 'success');
updateTokenStatus('已获取');
document.getElementById('getUserInfoBtn').disabled = false;
logResponse('令牌交换成功', result.data);
} else {
logResponse('令牌交换失败', result);
updateStatus('令牌获取失败', 'error');
}
} catch (error) {
logResponse('请求错误', { error: error.message });
updateStatus('网络错误', 'error');
}
}
// 获取用户信息
async function getUserInfo() {
if (!accessToken) {
logResponse('错误', { message: '没有访问令牌' });
return;
}
try {
const response = await fetch('http://localhost:3000/api/oauth/userinfo', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
const result = await response.json();
if (result.success) {
userInfo = result.data;
updateStatus(`已登录: ${userInfo.username}`, 'success');
logResponse('用户信息获取成功', result.data);
} else {
logResponse('用户信息获取失败', result);
}
} catch (error) {
logResponse('请求错误', { error: error.message });
}
}
// 更新状态显示
function updateStatus(status, type = 'info') {
const statusElement = document.getElementById('userStatus');
statusElement.textContent = status;
statusElement.className = `card-text text-${type === 'success' ? 'success' : type === 'error' ? 'danger' : 'warning'}`;
}
function updateTokenStatus(status) {
document.getElementById('tokenStatus').textContent = status;
}
// 记录API响应
function logResponse(title, data) {
const responseElement = document.getElementById('apiResponse');
const timestamp = new Date().toLocaleTimeString();
const log = `[${timestamp}] ${title}:\n${JSON.stringify(data, null, 2)}\n\n`;
responseElement.textContent += log;
responseElement.scrollTop = responseElement.scrollHeight;
}
// 页面加载时检查URL参数
window.addEventListener('load', () => {
checkUrlParams();
// 从 localStorage 读取客户端ID和密钥
const savedClientId = localStorage.getItem('client_id');
const savedClientSecret = localStorage.getItem('client_secret');
if (savedClientId) {
document.getElementById('clientId').value = savedClientId;
}
if (savedClientSecret) {
document.getElementById('clientSecret').value = savedClientSecret;
}
});
</script>
</body>
</html>

View File

@ -8,6 +8,39 @@ const { authenticateOAuthToken, requireScope } = require('../middleware/oauth');
const router = express.Router();
// 验证重定向URI的通用函数
const isValidRedirectUri = (storedUris, requestedUri) => {
try {
const requestedUrl = new URL(requestedUri);
for (const storedUri of storedUris) {
const storedUrl = new URL(storedUri);
// 检查协议是否匹配
if (requestedUrl.protocol !== storedUrl.protocol) {
continue;
}
// 获取文件名(路径的最后一部分)
const requestedFilename = requestedUrl.pathname.split('/').pop();
const storedFilename = storedUrl.pathname.split('/').pop();
// 检查文件名是否匹配,忽略路径前缀
if (requestedFilename === storedFilename) {
return true;
}
// 如果文件名不匹配,检查完整路径是否匹配
if (requestedUrl.pathname === storedUrl.pathname) {
return true;
}
}
return false;
} catch (error) {
return false;
}
};
// 验证中间件
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
@ -24,7 +57,7 @@ const handleValidationErrors = (req, res, next) => {
next();
};
// 1. 授权端点 - 用户授权第三方应用
// 1. 授权端点 - 验证参数并返回授权信息
router.get('/authorize', authenticateToken, async (req, res) => {
try {
const {
@ -61,10 +94,14 @@ router.get('/authorize', authenticateToken, async (req, res) => {
}
// 验证重定向URI
if (!client.redirect_uris.includes(redirect_uri)) {
if (!isValidRedirectUri(client.redirect_uris, redirect_uri)) {
return res.status(400).json({
success: false,
message: '无效的重定向URI'
message: '无效的重定向URI',
debug: {
requested: redirect_uri,
allowed: client.redirect_uris
}
});
}
@ -77,7 +114,101 @@ router.get('/authorize', authenticateToken, async (req, res) => {
});
}
// 生成授权码
// 返回授权信息,让前端显示授权页面
const scopes = scope ? scope.split(' ') : ['read', 'write'];
res.json({
success: true,
message: '授权信息获取成功',
data: {
client: {
id: client.client_id,
name: client.client_name,
description: client.description
},
scopes: scopes,
state: state,
redirect_uri: redirect_uri
}
});
} catch (error) {
console.error('授权信息获取失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
});
}
});
// 2. 用户同意授权端点
router.post('/authorize/consent', authenticateToken, async (req, res) => {
try {
const {
client_id,
redirect_uri,
scope,
state,
approved
} = req.body;
// 验证必需参数
if (!client_id || !redirect_uri || approved === undefined) {
return res.status(400).json({
success: false,
message: '缺少必需参数'
});
}
// 验证客户端
const client = await OAuthClient.findByClientId(client_id);
if (!client) {
return res.status(400).json({
success: false,
message: '无效的客户端ID'
});
}
// 验证重定向URI
if (!isValidRedirectUri(client.redirect_uris, redirect_uri)) {
return res.status(400).json({
success: false,
message: '无效的重定向URI',
debug: {
requested: redirect_uri,
allowed: client.redirect_uris
}
});
}
// 获取用户信息
const user = await User.findByUsername(req.user.username);
if (!user) {
return res.status(401).json({
success: false,
message: '用户不存在'
});
}
if (!approved) {
// 用户拒绝授权
const errorUrl = new URL(redirect_uri);
errorUrl.searchParams.set('error', 'access_denied');
errorUrl.searchParams.set('error_description', '用户拒绝授权');
if (state) {
errorUrl.searchParams.set('state', state);
}
return res.json({
success: true,
message: '用户拒绝授权',
data: {
redirect_url: errorUrl.toString()
}
});
}
// 用户同意授权,生成授权码
const authCode = OAuthToken.generateAuthCode();
const scopes = scope ? scope.split(' ') : ['read', 'write'];
@ -96,8 +227,6 @@ router.get('/authorize', authenticateToken, async (req, res) => {
redirectUrl.searchParams.set('state', state);
}
// 对于API测试直接返回授权码
// 对于生产环境应该重定向到redirectUrl
res.json({
success: true,
message: '授权成功',
@ -109,7 +238,7 @@ router.get('/authorize', authenticateToken, async (req, res) => {
});
} catch (error) {
console.error('授权失败:', error);
console.error('授权处理失败:', error);
res.status(500).json({
success: false,
message: '服务器内部错误'
@ -117,7 +246,7 @@ router.get('/authorize', authenticateToken, async (req, res) => {
}
});
// 2. 令牌端点 - 交换授权码获取访问令牌
// 3. 令牌端点 - 交换授权码获取访问令牌
router.post('/token', [
body('grant_type').notEmpty().withMessage('grant_type不能为空'),
body('client_id').notEmpty().withMessage('client_id不能为空'),
@ -273,7 +402,7 @@ router.post('/token', [
}
});
// 3. 撤销令牌端点
// 4. 撤销令牌端点
router.post('/revoke', [
body('token').notEmpty().withMessage('token不能为空'),
body('client_id').notEmpty().withMessage('client_id不能为空'),
@ -312,7 +441,7 @@ router.post('/revoke', [
}
});
// 4. 用户信息端点
// 5. 用户信息端点
router.get('/userinfo', authenticateOAuthToken, requireScope('read'), async (req, res) => {
try {
res.json({
@ -333,7 +462,7 @@ router.get('/userinfo', authenticateOAuthToken, requireScope('read'), async (req
}
});
// 5. 令牌信息端点
// 6. 令牌信息端点
router.get('/tokeninfo', async (req, res) => {
try {
const { access_token } = req.query;

View File

@ -45,37 +45,64 @@ const OAuthAuthorize = () => {
const responseType = searchParams.get('response_type')
useEffect(() => {
if (!user) {
navigate('/login')
return
}
// 验证OAuth参数
if (!clientId || !redirectUri || responseType !== 'code') {
setError('无效的授权请求')
return
}
// 这里可以添加客户端信息获取逻辑
// 为了演示,我们使用默认信息
setClientInfo({
name: '第三方应用',
description: '请求访问您的账户信息',
scopes: scope ? scope.split(' ') : ['read', 'write']
})
}, [user, clientId, redirectUri, scope, responseType, navigate])
// 获取授权信息
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 response = await axios.get(`/api/oauth/authorize?${params}`, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
})
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 || '获取授权信息失败')
}
} catch (error) {
setError(error.response?.data?.message || '获取授权信息失败')
} finally {
setLoading(false)
}
}
fetchAuthInfo()
}, [clientId, redirectUri, scope, responseType, state])
const handleAuthorize = async () => {
setLoading(true)
try {
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: scope || 'read write',
state: state || ''
})
const response = await axios.get(`/api/oauth/authorize?${params}`, {
const response = await axios.post('/api/oauth/authorize/consent', {
client_id: clientInfo.clientId,
redirect_uri: clientInfo.redirectUri,
scope: clientInfo.scopes.join(' '),
state: clientInfo.state,
approved: true
}, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
@ -87,21 +114,37 @@ const OAuthAuthorize = () => {
}
} catch (error) {
setError(error.response?.data?.message || '授权失败')
setLoading(false)
}
setLoading(false)
}
const handleDeny = () => {
// 拒绝授权,重定向回应用
const denyUrl = new URL(redirectUri)
denyUrl.searchParams.set('error', 'access_denied')
if (state) {
denyUrl.searchParams.set('state', state)
const handleDeny = async () => {
setLoading(true)
try {
const response = await axios.post('/api/oauth/authorize/consent', {
client_id: clientInfo.clientId,
redirect_uri: clientInfo.redirectUri,
scope: clientInfo.scopes.join(' '),
state: clientInfo.state,
approved: false
}, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
})
if (response.data.success) {
const { redirect_url } = response.data.data
window.location.href = redirect_url
}
} catch (error) {
setError(error.response?.data?.message || '拒绝授权失败')
setLoading(false)
}
window.location.href = denyUrl.toString()
}
if (!user) {
// 显示加载状态
if (loading) {
return (
<Container maxWidth="sm">
<Box

177
test-oauth-flow.js Normal file
View File

@ -0,0 +1,177 @@
const axios = require('axios');
// 配置
const BASE_URL = 'http://localhost:3000/api';
const FRONTEND_URL = 'http://localhost:3001';
// 测试用户信息
const testUser = {
username: `testuser_${Date.now()}`,
email: `testuser_${Date.now()}@example.com`,
password: 'TestPassword123'
};
// 测试客户端信息
const testClient = {
client_name: '测试应用',
description: '这是一个测试OAuth流程的应用',
redirect_uris: ['http://localhost:3001/callback']
};
let userToken = '';
let clientId = '';
let clientSecret = '';
async function testOAuthFlow() {
console.log('🚀 开始测试OAuth授权流程...\n');
try {
// 1. 注册用户
console.log('1. 注册测试用户...');
const registerResponse = await axios.post(`${BASE_URL}/auth/register`, testUser);
if (registerResponse.data.success) {
console.log('✅ 用户注册成功');
} else {
console.log('❌ 用户注册失败:', registerResponse.data.message);
return;
}
// 2. 用户登录
console.log('\n2. 用户登录...');
const loginResponse = await axios.post(`${BASE_URL}/auth/login`, {
username: testUser.username,
password: testUser.password
});
if (loginResponse.data.success) {
userToken = loginResponse.data.data.token;
console.log('✅ 用户登录成功');
} else {
console.log('❌ 用户登录失败:', loginResponse.data.message);
return;
}
// 3. 创建OAuth客户端
console.log('\n3. 创建OAuth客户端...');
const clientResponse = await axios.post(`${BASE_URL}/oauth/clients`, testClient, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
if (clientResponse.data.success) {
clientId = clientResponse.data.data.client_id;
clientSecret = clientResponse.data.data.client_secret;
console.log('✅ OAuth客户端创建成功');
console.log(` 客户端ID: ${clientId}`);
console.log(` 客户端密钥: ${clientSecret}`);
} else {
console.log('❌ OAuth客户端创建失败:', clientResponse.data.message);
return;
}
// 4. 获取授权信息
console.log('\n4. 获取授权信息...');
const authParams = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: 'http://localhost:3001/callback',
scope: 'read write',
state: 'test123'
});
const authResponse = await axios.get(`${BASE_URL}/oauth/authorize?${authParams}`, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
if (authResponse.data.success) {
console.log('✅ 授权信息获取成功');
console.log(` 应用名称: ${authResponse.data.data.client.name}`);
console.log(` 应用描述: ${authResponse.data.data.client.description}`);
console.log(` 请求权限: ${authResponse.data.data.scopes.join(', ')}`);
} else {
console.log('❌ 授权信息获取失败:', authResponse.data.message);
return;
}
// 5. 用户同意授权
console.log('\n5. 用户同意授权...');
const consentResponse = await axios.post(`${BASE_URL}/oauth/authorize/consent`, {
client_id: clientId,
redirect_uri: 'http://localhost:3001/callback',
scope: 'read write',
state: 'test123',
approved: true
}, {
headers: {
'Authorization': `Bearer ${userToken}`
}
});
if (consentResponse.data.success) {
console.log('✅ 用户同意授权成功');
console.log(` 重定向URL: ${consentResponse.data.data.redirect_url}`);
console.log(` 授权码: ${consentResponse.data.data.code}`);
} else {
console.log('❌ 用户同意授权失败:', consentResponse.data.message);
return;
}
// 6. 使用授权码交换访问令牌
console.log('\n6. 使用授权码交换访问令牌...');
const tokenResponse = await axios.post(`${BASE_URL}/oauth/token`, {
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
code: consentResponse.data.data.code,
redirect_uri: 'http://localhost:3001/callback'
});
if (tokenResponse.data.success) {
console.log('✅ 访问令牌获取成功');
console.log(` 访问令牌: ${tokenResponse.data.data.access_token.substring(0, 20)}...`);
console.log(` 刷新令牌: ${tokenResponse.data.data.refresh_token.substring(0, 20)}...`);
console.log(` 过期时间: ${tokenResponse.data.data.expires_in}`);
} else {
console.log('❌ 访问令牌获取失败:', tokenResponse.data.message);
return;
}
// 7. 使用访问令牌获取用户信息
console.log('\n7. 使用访问令牌获取用户信息...');
const userInfoResponse = await axios.get(`${BASE_URL}/oauth/userinfo`, {
headers: {
'Authorization': `Bearer ${tokenResponse.data.data.access_token}`
}
});
if (userInfoResponse.data.success) {
console.log('✅ 用户信息获取成功');
console.log(` 用户名: ${userInfoResponse.data.data.username}`);
console.log(` 邮箱: ${userInfoResponse.data.data.email}`);
} else {
console.log('❌ 用户信息获取失败:', userInfoResponse.data.message);
return;
}
console.log('\n🎉 OAuth授权流程测试完成');
console.log('\n📋 测试总结:');
console.log('✅ 用户注册和登录');
console.log('✅ OAuth客户端创建');
console.log('✅ 授权信息获取');
console.log('✅ 用户同意授权');
console.log('✅ 授权码交换访问令牌');
console.log('✅ 使用访问令牌获取用户信息');
console.log('\n🔗 前端测试链接:');
console.log(`${FRONTEND_URL}/oauth/authorize?client_id=${clientId}&redirect_uri=http://localhost:3001/callback&scope=read%20write&state=test123`);
} catch (error) {
console.error('❌ 测试过程中发生错误:', error.response?.data || error.message);
}
}
// 运行测试
testOAuthFlow();