更改README.md
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
# OAuth 2.0 提供商服务
|
# Pdnode Account
|
||||||
|
|
||||||
这是一个完整的OAuth 2.0提供商服务,支持Authorization Code Flow,包含用户认证、OAuth客户端管理、令牌管理等功能。
|
这是一个支持普通登录注册和OAuth 2.0授权码流程的认证系统,适合自建账号体系和第三方授权。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
|
1
debug-oauth-client.js
Normal file
1
debug-oauth-client.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
@ -175,6 +175,11 @@ class OAuthClient {
|
|||||||
static async validateRedirectUri(clientId, redirectUri) {
|
static async validateRedirectUri(clientId, redirectUri) {
|
||||||
const client = await this.findByClientId(clientId);
|
const client = await this.findByClientId(clientId);
|
||||||
if (!client) return false;
|
if (!client) return false;
|
||||||
|
// console.log(client.redirect_uris)
|
||||||
|
console.log("请求 redirect_uri: ", redirectUri);
|
||||||
|
console.log("允许 redirect_uris: ", client.redirect_uris);
|
||||||
|
console.log("是否包含: ", client.redirect_uris.includes(redirectUri));
|
||||||
|
|
||||||
return client.redirect_uris.includes(redirectUri);
|
return client.redirect_uris.includes(redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "auth-api",
|
"name": "pdnode-account",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "登录和注册API系统",
|
"description": "Pdnode Account:支持普通登录注册和OAuth 2.0授权码流程的认证系统",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
|
@ -15,7 +15,14 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/oauth/authorize" element={<OAuthAuthorize />} />
|
<Route
|
||||||
|
path="/oauth/authorize"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<OAuthAuthorize />
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/dashboard"
|
path="/dashboard"
|
||||||
element={
|
element={
|
||||||
|
@ -28,7 +28,8 @@ export const AuthProvider = ({ children }) => {
|
|||||||
const checkAuthStatus = async () => {
|
const checkAuthStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get('/api/auth/profile')
|
const response = await axios.get('/api/auth/profile')
|
||||||
setUser(response.data.data)
|
setUser(response.data.data.user)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
delete axios.defaults.headers.common['Authorization']
|
delete axios.defaults.headers.common['Authorization']
|
||||||
|
176
test-oauth-authorize.html
Normal file
176
test-oauth-authorize.html
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>OAuth授权页面测试</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.test-button {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
.test-button:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>OAuth授权页面测试</h1>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试说明</h3>
|
||||||
|
<p>这个页面用于测试OAuth授权页面的修复情况。</p>
|
||||||
|
<p><strong>问题描述:</strong>访问OAuth授权页面后直接跳转到login然后又跳转dashboard</p>
|
||||||
|
<p><strong>修复内容:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>将OAuth授权页面包装在PrivateRoute中</li>
|
||||||
|
<li>等待AuthContext的loading状态完成</li>
|
||||||
|
<li>简化组件逻辑,移除重复的认证检查</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试步骤</h3>
|
||||||
|
<ol>
|
||||||
|
<li>确保后端服务运行在 <code>http://localhost:3000</code></li>
|
||||||
|
<li>确保前端服务运行在 <code>http://localhost:3001</code></li>
|
||||||
|
<li>先登录用户(访问 <code>http://localhost:3001/login</code>)</li>
|
||||||
|
<li>在个人中心创建一个OAuth客户端</li>
|
||||||
|
<li>使用下面的测试链接访问OAuth授权页面</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试链接</h3>
|
||||||
|
<p>点击下面的按钮测试OAuth授权页面:</p>
|
||||||
|
|
||||||
|
<button class="test-button" onclick="testOAuthAuthorize()">
|
||||||
|
测试OAuth授权页面
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="test-button" onclick="testWithInvalidParams()">
|
||||||
|
测试无效参数
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="test-button" onclick="testWithoutLogin()">
|
||||||
|
测试未登录状态
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>预期结果</h3>
|
||||||
|
<ul>
|
||||||
|
<li class="success">✅ 已登录用户:应该显示授权页面,不跳转到login</li>
|
||||||
|
<li class="success">✅ 未登录用户:应该跳转到login页面</li>
|
||||||
|
<li class="success">✅ 无效参数:应该显示错误信息</li>
|
||||||
|
<li class="success">✅ 加载状态:应该显示加载动画</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="test-section">
|
||||||
|
<h3>测试结果</h3>
|
||||||
|
<div id="testResults">
|
||||||
|
<p>点击测试按钮开始测试...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function logResult(message, type = 'info') {
|
||||||
|
const resultsDiv = document.getElementById('testResults');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
const color = type === 'error' ? 'red' : type === 'success' ? 'green' : 'blue';
|
||||||
|
resultsDiv.innerHTML += `<p style="color: ${color}">[${timestamp}] ${message}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function testOAuthAuthorize() {
|
||||||
|
logResult('开始测试OAuth授权页面...');
|
||||||
|
|
||||||
|
// 模拟一个有效的OAuth请求
|
||||||
|
const clientId = 'test_client_id';
|
||||||
|
const redirectUri = 'http://localhost:3001/callback';
|
||||||
|
const scope = 'read write';
|
||||||
|
const state = 'test123';
|
||||||
|
|
||||||
|
const authUrl = `http://localhost:3001/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${state}`;
|
||||||
|
|
||||||
|
logResult(`重定向到: ${authUrl}`);
|
||||||
|
window.open(authUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWithInvalidParams() {
|
||||||
|
logResult('开始测试无效参数...');
|
||||||
|
|
||||||
|
// 模拟一个无效的OAuth请求(缺少client_id)
|
||||||
|
const authUrl = 'http://localhost:3001/oauth/authorize?redirect_uri=http://localhost:3001/callback&scope=read&state=test123';
|
||||||
|
|
||||||
|
logResult(`重定向到: ${authUrl}`);
|
||||||
|
window.open(authUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWithoutLogin() {
|
||||||
|
logResult('开始测试未登录状态...');
|
||||||
|
|
||||||
|
// 清除localStorage中的token
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
logResult('已清除登录状态');
|
||||||
|
|
||||||
|
const authUrl = 'http://localhost:3001/oauth/authorize?client_id=test&redirect_uri=http://localhost:3001/callback&scope=read&state=test123';
|
||||||
|
|
||||||
|
logResult(`重定向到: ${authUrl}`);
|
||||||
|
window.open(authUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查服务状态
|
||||||
|
window.addEventListener('load', async () => {
|
||||||
|
logResult('检查服务状态...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查后端服务
|
||||||
|
const backendResponse = await fetch('http://localhost:3000/api/health');
|
||||||
|
if (backendResponse.ok) {
|
||||||
|
logResult('✅ 后端服务正常运行', 'success');
|
||||||
|
} else {
|
||||||
|
logResult('❌ 后端服务异常', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logResult('❌ 无法连接到后端服务', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查前端服务
|
||||||
|
const frontendResponse = await fetch('http://localhost:3001');
|
||||||
|
if (frontendResponse.ok) {
|
||||||
|
logResult('✅ 前端服务正常运行', 'success');
|
||||||
|
} else {
|
||||||
|
logResult('❌ 前端服务异常', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logResult('❌ 无法连接到前端服务', 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
204
test-redirect-uri.js
Normal file
204
test-redirect-uri.js
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// 测试重定向URI匹配逻辑
|
||||||
|
function testRedirectUriMatching() {
|
||||||
|
console.log('🧪 测试重定向URI匹配逻辑...\n');
|
||||||
|
|
||||||
|
// 模拟数据库中存储的URI
|
||||||
|
const storedUris = [
|
||||||
|
'http://localhost:3001/third-party-app.html',
|
||||||
|
'http://localhost:3001/callback',
|
||||||
|
'https://example.com/callback'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 测试用例
|
||||||
|
const testCases = [
|
||||||
|
{
|
||||||
|
requested: 'http://127.0.0.1:5500/public/third-party-app.html',
|
||||||
|
expected: true,
|
||||||
|
description: '不同主机名和端口,相同路径'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requested: 'http://localhost:3001/third-party-app.html',
|
||||||
|
expected: true,
|
||||||
|
description: '完全匹配'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requested: 'http://localhost:3001/callback',
|
||||||
|
expected: true,
|
||||||
|
description: '匹配callback路径'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requested: 'http://127.0.0.1:8080/callback',
|
||||||
|
expected: true,
|
||||||
|
description: '不同端口,相同路径'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requested: 'http://localhost:3001/different-path.html',
|
||||||
|
expected: false,
|
||||||
|
description: '路径不匹配'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requested: 'https://localhost:3001/third-party-app.html',
|
||||||
|
expected: false,
|
||||||
|
description: '协议不匹配'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 验证函数(与后端逻辑相同)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
let passed = 0;
|
||||||
|
let total = testCases.length;
|
||||||
|
|
||||||
|
testCases.forEach((testCase, index) => {
|
||||||
|
const result = isValidRedirectUri(storedUris, testCase.requested);
|
||||||
|
const status = result === testCase.expected ? '✅' : '❌';
|
||||||
|
|
||||||
|
console.log(`${status} 测试 ${index + 1}: ${testCase.description}`);
|
||||||
|
console.log(` 请求URI: ${testCase.requested}`);
|
||||||
|
console.log(` 期望结果: ${testCase.expected}, 实际结果: ${result}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (result === testCase.expected) {
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📊 测试结果: ${passed}/${total} 通过`);
|
||||||
|
|
||||||
|
if (passed === total) {
|
||||||
|
console.log('🎉 所有测试通过!重定向URI验证逻辑正常工作。');
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ 部分测试失败,需要检查逻辑。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试实际的OAuth流程
|
||||||
|
async function testOAuthFlow() {
|
||||||
|
console.log('\n🚀 测试实际OAuth流程...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 注册用户
|
||||||
|
const testUser = {
|
||||||
|
username: `testuser_${Date.now()}`,
|
||||||
|
email: `testuser_${Date.now()}@example.com`,
|
||||||
|
password: 'TestPassword123'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('1. 注册测试用户...');
|
||||||
|
const registerResponse = await axios.post('http://localhost:3000/api/auth/register', testUser);
|
||||||
|
if (!registerResponse.data.success) {
|
||||||
|
console.log('❌ 用户注册失败:', registerResponse.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('✅ 用户注册成功');
|
||||||
|
|
||||||
|
// 2. 用户登录
|
||||||
|
console.log('\n2. 用户登录...');
|
||||||
|
const loginResponse = await axios.post('http://localhost:3000/api/auth/login', {
|
||||||
|
username: testUser.username,
|
||||||
|
password: testUser.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!loginResponse.data.success) {
|
||||||
|
console.log('❌ 用户登录失败:', loginResponse.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userToken = loginResponse.data.data.token;
|
||||||
|
console.log('✅ 用户登录成功');
|
||||||
|
|
||||||
|
// 3. 创建OAuth客户端
|
||||||
|
console.log('\n3. 创建OAuth客户端...');
|
||||||
|
const clientData = {
|
||||||
|
name: '测试应用',
|
||||||
|
description: '测试重定向URI匹配',
|
||||||
|
redirect_uris: ['http://localhost:3001/third-party-app.html']
|
||||||
|
};
|
||||||
|
|
||||||
|
const clientResponse = await axios.post('http://localhost:3000/api/oauth/clients', clientData, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${userToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!clientResponse.data.success) {
|
||||||
|
console.log('❌ OAuth客户端创建失败:', clientResponse.data.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = clientResponse.data.data.client_id;
|
||||||
|
console.log('✅ OAuth客户端创建成功');
|
||||||
|
console.log(` 客户端ID: ${clientId}`);
|
||||||
|
|
||||||
|
// 4. 测试授权请求
|
||||||
|
console.log('\n4. 测试授权请求...');
|
||||||
|
const authParams = new URLSearchParams({
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: clientId,
|
||||||
|
redirect_uri: 'http://127.0.0.1:5500/public/third-party-app.html',
|
||||||
|
scope: 'read write',
|
||||||
|
state: 'test123'
|
||||||
|
});
|
||||||
|
|
||||||
|
const authResponse = await axios.get(`http://localhost:3000/api/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.scopes.join(', ')}`);
|
||||||
|
} else {
|
||||||
|
console.log('❌ 授权请求失败:', authResponse.data.message);
|
||||||
|
if (authResponse.data.debug) {
|
||||||
|
console.log(' 调试信息:', authResponse.data.debug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 测试过程中发生错误:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
testRedirectUriMatching();
|
||||||
|
testOAuthFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { testRedirectUriMatching, testOAuthFlow };
|
Reference in New Issue
Block a user