Diamond_ore 3 meses atrás
commit
3e8404330f
89 arquivos alterados com 7107 adições e 0 exclusões
  1. 66 0
      .env
  2. 66 0
      .env.development
  3. 66 0
      .env.example
  4. 168 0
      TROUBLESHOOTING.md
  5. 53 0
      check_app.py
  6. 76 0
      check_backend.py
  7. 141 0
      check_config.py
  8. 60 0
      check_user.py
  9. 112 0
      create_test_app.py
  10. 77 0
      fix_password.py
  11. 2283 0
      full_server.py
  12. 72 0
      install_deps.py
  13. 31 0
      load_env.py
  14. 117 0
      quick_start.py
  15. 44 0
      requirements/base.txt
  16. 21 0
      requirements/dev.txt
  17. 25 0
      run_server.py
  18. 164 0
      scripts/init_db.py
  19. 277 0
      simple_init_db.py
  20. 4 0
      src/app/__init__.py
  21. BIN
      src/app/__pycache__/__init__.cpython-312.pyc
  22. BIN
      src/app/__pycache__/main.cpython-312.pyc
  23. 1 0
      src/app/api/__init__.py
  24. BIN
      src/app/api/__pycache__/__init__.cpython-312.pyc
  25. 1 0
      src/app/api/v1/__init__.py
  26. BIN
      src/app/api/v1/__pycache__/__init__.cpython-312.pyc
  27. BIN
      src/app/api/v1/__pycache__/api_router.cpython-312.pyc
  28. 12 0
      src/app/api/v1/api_router.py
  29. 1 0
      src/app/api/v1/auth/__init__.py
  30. BIN
      src/app/api/v1/auth/__pycache__/__init__.cpython-312.pyc
  31. BIN
      src/app/api/v1/auth/__pycache__/endpoints.cpython-312.pyc
  32. BIN
      src/app/api/v1/auth/__pycache__/router.cpython-312.pyc
  33. 246 0
      src/app/api/v1/auth/endpoints.py
  34. 10 0
      src/app/api/v1/auth/router.py
  35. 1 0
      src/app/api/v1/oauth/__init__.py
  36. BIN
      src/app/api/v1/oauth/__pycache__/__init__.cpython-312.pyc
  37. BIN
      src/app/api/v1/oauth/__pycache__/endpoints.cpython-312.pyc
  38. BIN
      src/app/api/v1/oauth/__pycache__/router.cpython-312.pyc
  39. 543 0
      src/app/api/v1/oauth/endpoints.py
  40. 10 0
      src/app/api/v1/oauth/router.py
  41. 1 0
      src/app/config/__init__.py
  42. BIN
      src/app/config/__pycache__/__init__.cpython-312.pyc
  43. BIN
      src/app/config/__pycache__/database.cpython-312.pyc
  44. BIN
      src/app/config/__pycache__/settings.cpython-312.pyc
  45. BIN
      src/app/config/__pycache__/simple_settings.cpython-312.pyc
  46. 56 0
      src/app/config/database.py
  47. 157 0
      src/app/config/settings.py
  48. 114 0
      src/app/config/simple_settings.py
  49. 1 0
      src/app/core/__init__.py
  50. BIN
      src/app/core/__pycache__/__init__.cpython-312.pyc
  51. BIN
      src/app/core/__pycache__/exceptions.cpython-312.pyc
  52. 252 0
      src/app/core/exceptions.py
  53. 170 0
      src/app/main.py
  54. 26 0
      src/app/models/__init__.py
  55. BIN
      src/app/models/__pycache__/__init__.cpython-312.pyc
  56. BIN
      src/app/models/__pycache__/app.cpython-312.pyc
  57. BIN
      src/app/models/__pycache__/base.cpython-312.pyc
  58. BIN
      src/app/models/__pycache__/log.cpython-312.pyc
  59. BIN
      src/app/models/__pycache__/token.cpython-312.pyc
  60. BIN
      src/app/models/__pycache__/user.cpython-312.pyc
  61. 45 0
      src/app/models/app.py
  62. 31 0
      src/app/models/base.py
  63. 61 0
      src/app/models/log.py
  64. 55 0
      src/app/models/token.py
  65. 103 0
      src/app/models/user.py
  66. 1 0
      src/app/schemas/__init__.py
  67. BIN
      src/app/schemas/__pycache__/__init__.cpython-312.pyc
  68. BIN
      src/app/schemas/__pycache__/auth.cpython-312.pyc
  69. BIN
      src/app/schemas/__pycache__/base.cpython-312.pyc
  70. 122 0
      src/app/schemas/auth.py
  71. 54 0
      src/app/schemas/base.py
  72. 168 0
      src/app/schemas/user.py
  73. 1 0
      src/app/services/__init__.py
  74. BIN
      src/app/services/__pycache__/__init__.cpython-312.pyc
  75. BIN
      src/app/services/__pycache__/auth_service.cpython-312.pyc
  76. BIN
      src/app/services/__pycache__/oauth_service.cpython-312.pyc
  77. 297 0
      src/app/services/auth_service.py
  78. 12 0
      src/app/services/oauth_service.py
  79. 1 0
      src/app/utils/__init__.py
  80. BIN
      src/app/utils/__pycache__/__init__.cpython-312.pyc
  81. BIN
      src/app/utils/__pycache__/security.cpython-312.pyc
  82. 150 0
      src/app/utils/security.py
  83. 47 0
      test_captcha.py
  84. 81 0
      test_db_connection.py
  85. 45 0
      test_login.py
  86. 80 0
      test_oauth.py
  87. 78 0
      test_server.py
  88. 81 0
      test_server_8001.py
  89. 69 0
      update_app_callback.py

+ 66 - 0
.env

@@ -0,0 +1,66 @@
+# 应用配置
+APP_NAME=SSO认证中心
+APP_VERSION=1.0.0
+DEBUG=True
+SECRET_KEY=dev-secret-key-change-in-production-12345678901234567890
+ALGORITHM=HS256
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8000
+RELOAD=True
+
+# 数据库配置 - 请根据实际情况修改
+DATABASE_URL=mysql+aiomysql://root:admin@localhost:3306/lq_db
+DATABASE_ECHO=False
+
+# Redis配置
+REDIS_URL=redis://localhost:6379/0
+REDIS_PASSWORD=
+
+# JWT配置
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+REFRESH_TOKEN_EXPIRE_DAYS=30
+JWT_SECRET_KEY=dev-jwt-secret-key-change-in-production-12345678901234567890
+
+# OAuth2配置
+OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10
+OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES=120
+OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS=30
+
+# 邮件配置
+SMTP_HOST=
+SMTP_PORT=587
+SMTP_USER=
+SMTP_PASSWORD=
+SMTP_TLS=True
+SMTP_SSL=False
+
+# 文件上传配置
+UPLOAD_DIR=./uploads
+MAX_FILE_SIZE=5242880
+ALLOWED_EXTENSIONS=jpg,jpeg,png,gif
+
+# 日志配置
+LOG_LEVEL=INFO
+LOG_FILE=./logs/app.log
+
+# CORS配置
+CORS_ORIGINS=http://localhost:3000,http://localhost:8080,http://localhost:3001
+CORS_CREDENTIALS=True
+CORS_METHODS=*
+CORS_HEADERS=*
+
+# 安全配置
+BCRYPT_ROUNDS=12
+PASSWORD_MIN_LENGTH=8
+MAX_LOGIN_ATTEMPTS=5
+LOCKOUT_DURATION_MINUTES=30
+
+# 缓存配置
+CACHE_TTL=3600
+SESSION_TTL=86400
+
+# Celery配置
+CELERY_BROKER_URL=redis://localhost:6379/1
+CELERY_RESULT_BACKEND=redis://localhost:6379/2

+ 66 - 0
.env.development

@@ -0,0 +1,66 @@
+# 开发环境配置
+APP_NAME=SSO认证中心
+APP_VERSION=1.0.0
+DEBUG=True
+SECRET_KEY=dev-secret-key-change-in-production
+ALGORITHM=HS256
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8000
+RELOAD=True
+
+# 数据库配置
+DATABASE_URL=mysql+aiomysql://sso_user:sso_password@localhost:3306/sso_db
+DATABASE_ECHO=False
+
+# Redis配置
+REDIS_URL=redis://localhost:6379/0
+REDIS_PASSWORD=
+
+# JWT配置
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+REFRESH_TOKEN_EXPIRE_DAYS=30
+JWT_SECRET_KEY=dev-jwt-secret-key-change-in-production
+
+# OAuth2配置
+OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10
+OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES=120
+OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS=30
+
+# 邮件配置
+SMTP_HOST=
+SMTP_PORT=587
+SMTP_USER=
+SMTP_PASSWORD=
+SMTP_TLS=True
+SMTP_SSL=False
+
+# 文件上传配置
+UPLOAD_DIR=./uploads
+MAX_FILE_SIZE=5242880
+ALLOWED_EXTENSIONS=jpg,jpeg,png,gif
+
+# 日志配置
+LOG_LEVEL=INFO
+LOG_FILE=./logs/app.log
+
+# CORS配置
+CORS_ORIGINS=http://localhost:3000,http://localhost:8080,http://localhost:3001
+CORS_CREDENTIALS=True
+CORS_METHODS=*
+CORS_HEADERS=*
+
+# 安全配置
+BCRYPT_ROUNDS=12
+PASSWORD_MIN_LENGTH=8
+MAX_LOGIN_ATTEMPTS=5
+LOCKOUT_DURATION_MINUTES=30
+
+# 缓存配置
+CACHE_TTL=3600
+SESSION_TTL=86400
+
+# Celery配置
+CELERY_BROKER_URL=redis://localhost:6379/1
+CELERY_RESULT_BACKEND=redis://localhost:6379/2

+ 66 - 0
.env.example

@@ -0,0 +1,66 @@
+# 应用配置
+APP_NAME=SSO认证中心
+APP_VERSION=1.0.0
+DEBUG=True
+SECRET_KEY=your-secret-key-here
+ALGORITHM=HS256
+
+# 服务器配置
+HOST=0.0.0.0
+PORT=8000
+RELOAD=True
+
+# 数据库配置
+DATABASE_URL=mysql+aiomysql://sso_user:sso_password@localhost:3306/sso_db
+DATABASE_ECHO=False
+
+# Redis配置
+REDIS_URL=redis://localhost:6379/0
+REDIS_PASSWORD=
+
+# JWT配置
+ACCESS_TOKEN_EXPIRE_MINUTES=30
+REFRESH_TOKEN_EXPIRE_DAYS=30
+JWT_SECRET_KEY=your-jwt-secret-key-here
+
+# OAuth2配置
+OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10
+OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES=120
+OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS=30
+
+# 邮件配置
+SMTP_HOST=smtp.gmail.com
+SMTP_PORT=587
+SMTP_USER=your-email@gmail.com
+SMTP_PASSWORD=your-email-password
+SMTP_TLS=True
+SMTP_SSL=False
+
+# 文件上传配置
+UPLOAD_DIR=./uploads
+MAX_FILE_SIZE=5242880  # 5MB
+ALLOWED_EXTENSIONS=jpg,jpeg,png,gif
+
+# 日志配置
+LOG_LEVEL=INFO
+LOG_FILE=./logs/app.log
+
+# CORS配置
+CORS_ORIGINS=["http://localhost:3000", "http://localhost:8080"]
+CORS_CREDENTIALS=True
+CORS_METHODS=["*"]
+CORS_HEADERS=["*"]
+
+# 安全配置
+BCRYPT_ROUNDS=12
+PASSWORD_MIN_LENGTH=8
+MAX_LOGIN_ATTEMPTS=5
+LOCKOUT_DURATION_MINUTES=30
+
+# 缓存配置
+CACHE_TTL=3600
+SESSION_TTL=86400
+
+# Celery配置
+CELERY_BROKER_URL=redis://localhost:6379/1
+CELERY_RESULT_BACKEND=redis://localhost:6379/2

+ 168 - 0
TROUBLESHOOTING.md

@@ -0,0 +1,168 @@
+# 故障排除指南
+
+## 当前问题:端口8000被占用
+
+### 问题描述
+运行 `python -m app.main` 或 `python run_server.py` 时,端口8000已经被占用,导致无法访问 http://localhost:8000/docs
+
+### 解决方案
+
+#### 方案1:停止占用端口的进程(推荐)
+
+1. **查看占用端口的进程**:
+```powershell
+netstat -ano | findstr "8000"
+```
+
+2. **停止进程**:
+```powershell
+# 找到PID(例如18712),然后停止它
+taskkill /PID 18712 /F
+```
+
+3. **重新启动服务器**:
+```powershell
+python test_server.py
+```
+
+#### 方案2:使用不同的端口
+
+1. **修改 `.env` 文件**:
+```env
+PORT=8001
+```
+
+2. **使用指定端口启动**:
+```powershell
+python test_server_8001.py
+```
+
+3. **访问新地址**:
+- 主页: http://localhost:8001
+- API文档: http://localhost:8001/docs
+
+#### 方案3:使用测试服务器
+
+我已经创建了一个简化的测试服务器,可以快速验证功能:
+
+```powershell
+python test_server.py
+```
+
+如果8000端口被占用,可以使用:
+
+```powershell
+python test_server_8001.py
+```
+
+### 验证服务器是否正常运行
+
+1. **检查服务器输出**:
+   - 应该看到 "Uvicorn running on http://0.0.0.0:8000"
+   - 没有错误信息
+
+2. **测试根路径**:
+```powershell
+curl http://localhost:8000
+```
+
+应该返回JSON响应。
+
+3. **访问API文档**:
+   - 打开浏览器访问: http://localhost:8000/docs
+   - 应该看到Swagger UI界面
+
+### 常见错误
+
+#### 错误1:ModuleNotFoundError: No module named 'app'
+
+**原因**:Python路径配置问题
+
+**解决**:使用提供的启动脚本
+```powershell
+python run_server.py
+# 或
+python test_server.py
+```
+
+#### 错误2:Could not parse SQLAlchemy URL from string ''
+
+**原因**:环境变量未加载
+
+**解决**:
+1. 确保 `.env` 文件存在
+2. 检查 `DATABASE_URL` 配置是否正确
+3. 运行配置检查:
+```powershell
+python load_env.py
+```
+
+#### 错误3:Address already in use
+
+**原因**:端口被占用
+
+**解决**:参考上面的方案1或方案2
+
+### 完整启动流程
+
+1. **停止所有占用8000端口的进程**
+2. **检查配置**:
+```powershell
+python load_env.py
+python check_config.py
+```
+
+3. **启动测试服务器**:
+```powershell
+python test_server.py
+```
+
+4. **验证服务**:
+   - 浏览器访问: http://localhost:8000/docs
+   - 应该看到API文档界面
+
+5. **如果测试服务器正常,启动完整服务器**:
+```powershell
+python run_server.py
+```
+
+### 获取帮助
+
+如果以上方法都无法解决问题,请提供以下信息:
+
+1. 错误信息的完整输出
+2. `python load_env.py` 的输出
+3. `netstat -ano | findstr "8000"` 的输出
+4. Python版本:`python --version`
+5. 操作系统版本
+
+---
+
+## 快速命令参考
+
+```powershell
+# 检查端口占用
+netstat -ano | findstr "8000"
+
+# 停止进程
+taskkill /PID <进程ID> /F
+
+# 检查配置
+python load_env.py
+python check_config.py
+
+# 测试数据库连接
+python test_db_connection.py
+
+# 初始化数据库
+python simple_init_db.py
+
+# 启动测试服务器
+python test_server.py
+
+# 启动完整服务器
+python run_server.py
+
+# 访问API文档
+# 浏览器打开: http://localhost:8000/docs
+```

+ 53 - 0
check_app.py

@@ -0,0 +1,53 @@
+#!/usr/bin/env python3
+"""
+检查应用状态
+"""
+import pymysql
+from urllib.parse import urlparse
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+def check_app():
+    """检查应用状态"""
+    database_url = os.getenv('DATABASE_URL', '')
+    parsed = urlparse(database_url)
+    config = {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'database': parsed.path[1:] if parsed.path else 'sso_db',
+        'charset': 'utf8mb4'
+    }
+
+    try:
+        conn = pymysql.connect(**config)
+        cursor = conn.cursor()
+        
+        cursor.execute("""
+            SELECT name, app_key, is_trusted, is_active, redirect_uris, scope 
+            FROM apps 
+            WHERE app_key = %s
+        """, ('eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY',))
+        
+        result = cursor.fetchone()
+        if result:
+            print(f'应用名称: {result[0]}')
+            print(f'App Key: {result[1]}')
+            print(f'受信任: {result[2]}')
+            print(f'激活状态: {result[3]}')
+            print(f'回调URL: {result[4]}')
+            print(f'权限范围: {result[5]}')
+        else:
+            print('❌ 未找到应用')
+            
+        cursor.close()
+        conn.close()
+        
+    except Exception as e:
+        print(f'❌ 检查失败: {e}')
+
+if __name__ == "__main__":
+    check_app()

+ 76 - 0
check_backend.py

@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+检查后端服务状态
+"""
+import requests
+import json
+
+def check_backend_status():
+    """检查后端服务状态"""
+    ports = [8000, 8001, 8002, 8003]
+    
+    for port in ports:
+        try:
+            url = f"http://localhost:{port}"
+            print(f"检查端口 {port}...")
+            
+            response = requests.get(url, timeout=5)
+            if response.status_code == 200:
+                data = response.json()
+                print(f"✅ 端口 {port} 服务正常")
+                print(f"   消息: {data.get('message', 'N/A')}")
+                print(f"   版本: {data.get('version', 'N/A')}")
+                
+                # 检查健康状态
+                try:
+                    health_response = requests.get(f"{url}/health", timeout=5)
+                    if health_response.status_code == 200:
+                        print(f"   健康检查: ✅ 正常")
+                    else:
+                        print(f"   健康检查: ❌ 异常 ({health_response.status_code})")
+                except:
+                    print(f"   健康检查: ❌ 无响应")
+                
+                # 检查API文档
+                try:
+                    docs_response = requests.get(f"{url}/docs", timeout=5)
+                    if docs_response.status_code == 200:
+                        print(f"   API文档: ✅ 可访问 ({url}/docs)")
+                    else:
+                        print(f"   API文档: ❌ 不可访问")
+                except:
+                    print(f"   API文档: ❌ 无响应")
+                
+                print(f"   建议前端API地址: {url}")
+                return port
+                
+        except requests.exceptions.ConnectionError:
+            print(f"❌ 端口 {port} 无服务")
+        except requests.exceptions.Timeout:
+            print(f"⏰ 端口 {port} 响应超时")
+        except Exception as e:
+            print(f"❌ 端口 {port} 检查失败: {e}")
+    
+    print("\n❌ 未找到可用的后端服务")
+    print("请先启动后端服务:")
+    print("  python quick_start.py")
+    print("  或")
+    print("  python test_server.py")
+    return None
+
+if __name__ == "__main__":
+    print("=" * 50)
+    print("后端服务状态检查")
+    print("=" * 50)
+    
+    available_port = check_backend_status()
+    
+    if available_port:
+        print(f"\n🎉 找到可用服务: http://localhost:{available_port}")
+        print(f"📚 API文档地址: http://localhost:{available_port}/docs")
+        print(f"\n📝 前端配置建议:")
+        print(f"   VITE_API_BASE_URL=http://localhost:{available_port}")
+    else:
+        print(f"\n🚀 启动后端服务:")
+        print(f"   cd sso-backend")
+        print(f"   python quick_start.py")

+ 141 - 0
check_config.py

@@ -0,0 +1,141 @@
+#!/usr/bin/env python3
+"""
+配置检查脚本
+"""
+import os
+import sys
+from urllib.parse import urlparse
+
+def check_database_config():
+    """检查数据库配置"""
+    print("🔍 检查数据库配置...")
+    
+    # 检查.env文件是否存在
+    if not os.path.exists('.env'):
+        print("❌ .env文件不存在,请复制.env.example并修改配置")
+        return False
+    
+    # 读取环境变量
+    from dotenv import load_dotenv
+    load_dotenv()
+    
+    database_url = os.getenv('DATABASE_URL')
+    if not database_url:
+        print("❌ DATABASE_URL未配置")
+        return False
+    
+    # 解析数据库URL
+    try:
+        parsed = urlparse(database_url)
+        print(f"✅ 数据库类型: {parsed.scheme}")
+        print(f"✅ 数据库主机: {parsed.hostname}")
+        print(f"✅ 数据库端口: {parsed.port}")
+        print(f"✅ 数据库名称: {parsed.path[1:]}")  # 去掉开头的/
+        print(f"✅ 数据库用户: {parsed.username}")
+    except Exception as e:
+        print(f"❌ 数据库URL格式错误: {e}")
+        return False
+    
+    return True
+
+def check_required_env():
+    """检查必需的环境变量"""
+    print("\n🔍 检查必需的环境变量...")
+    
+    required_vars = [
+        'SECRET_KEY',
+        'JWT_SECRET_KEY',
+        'DATABASE_URL'
+    ]
+    
+    missing_vars = []
+    for var in required_vars:
+        value = os.getenv(var)
+        if not value:
+            missing_vars.append(var)
+        else:
+            print(f"✅ {var}: {'*' * min(len(value), 10)}")
+    
+    if missing_vars:
+        print(f"❌ 缺少必需的环境变量: {', '.join(missing_vars)}")
+        return False
+    
+    return True
+
+def test_database_connection():
+    """测试数据库连接"""
+    print("\n🔍 测试数据库连接...")
+    
+    try:
+        import aiomysql
+        import asyncio
+        from urllib.parse import urlparse
+        
+        database_url = os.getenv('DATABASE_URL')
+        parsed = urlparse(database_url)
+        
+        async def test_connection():
+            try:
+                conn = await aiomysql.connect(
+                    host=parsed.hostname,
+                    port=parsed.port or 3306,
+                    user=parsed.username,
+                    password=parsed.password,
+                    db=parsed.path[1:] if parsed.path else None,
+                    autocommit=True
+                )
+                await conn.ensure_closed()
+                return True
+            except Exception as e:
+                print(f"❌ 数据库连接失败: {e}")
+                return False
+        
+        result = asyncio.run(test_connection())
+        if result:
+            print("✅ 数据库连接成功")
+            return True
+        else:
+            return False
+            
+    except ImportError:
+        print("⚠️  aiomysql未安装,跳过数据库连接测试")
+        return True
+    except Exception as e:
+        print(f"❌ 数据库连接测试失败: {e}")
+        return False
+
+def main():
+    """主函数"""
+    print("=" * 50)
+    print("SSO后端配置检查")
+    print("=" * 50)
+    
+    # 加载环境变量
+    try:
+        from dotenv import load_dotenv
+        load_dotenv()
+    except ImportError:
+        print("❌ python-dotenv未安装,请先运行: pip install python-dotenv")
+        sys.exit(1)
+    
+    checks = [
+        check_database_config,
+        check_required_env,
+        test_database_connection
+    ]
+    
+    all_passed = True
+    for check in checks:
+        if not check():
+            all_passed = False
+    
+    print("\n" + "=" * 50)
+    if all_passed:
+        print("🎉 所有检查通过!可以运行初始化脚本了")
+        print("\n下一步: python scripts/init_db.py")
+    else:
+        print("❌ 部分检查失败,请修复配置后重试")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()

+ 60 - 0
check_user.py

@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+"""
+检查用户数据
+"""
+import pymysql
+from urllib.parse import urlparse
+import os
+from dotenv import load_dotenv
+
+load_dotenv()
+
+def check_user_data():
+    """检查用户数据"""
+    database_url = os.getenv('DATABASE_URL', '')
+    parsed = urlparse(database_url)
+    config = {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'database': parsed.path[1:] if parsed.path else 'sso_db',
+        'charset': 'utf8mb4'
+    }
+
+    try:
+        conn = pymysql.connect(**config)
+        cursor = conn.cursor()
+        
+        # 查询admin用户
+        cursor.execute('SELECT username, password_hash FROM users WHERE username = %s', ('admin',))
+        result = cursor.fetchone()
+        
+        if result:
+            username, password_hash = result
+            print(f'用户名: {username}')
+            print(f'密码哈希: {password_hash}')
+            print(f'哈希格式: {password_hash[:20]}...')
+            
+            # 检查哈希格式
+            if password_hash.startswith("sha256$"):
+                parts = password_hash.split("$")
+                print(f'哈希部分数量: {len(parts)}')
+                if len(parts) == 3:
+                    print(f'盐值: {parts[1]}')
+                    print(f'哈希值: {parts[2][:20]}...')
+                else:
+                    print('❌ 哈希格式错误')
+            else:
+                print('❌ 哈希格式不正确,应该以sha256$开头')
+        else:
+            print('❌ 未找到admin用户')
+            
+        cursor.close()
+        conn.close()
+        
+    except Exception as e:
+        print(f'❌ 数据库连接失败: {e}')
+
+if __name__ == "__main__":
+    check_user_data()

+ 112 - 0
create_test_app.py

@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+"""
+创建测试应用
+"""
+import sys
+import os
+import pymysql
+from urllib.parse import urlparse
+import uuid
+import json
+from dotenv import load_dotenv
+
+load_dotenv()
+
+def get_db_connection():
+    """获取数据库连接"""
+    try:
+        database_url = os.getenv('DATABASE_URL', '')
+        if not database_url:
+            return None
+            
+        parsed = urlparse(database_url)
+        config = {
+            'host': parsed.hostname or 'localhost',
+            'port': parsed.port or 3306,
+            'user': parsed.username or 'root',
+            'password': parsed.password or '',
+            'database': parsed.path[1:] if parsed.path else 'sso_db',
+            'charset': 'utf8mb4'
+        }
+        
+        return pymysql.connect(**config)
+    except Exception as e:
+        print(f"数据库连接失败: {e}")
+        return None
+
+def create_test_app():
+    """创建测试应用"""
+    conn = get_db_connection()
+    if not conn:
+        print("❌ 数据库连接失败")
+        return
+    
+    cursor = conn.cursor()
+    
+    # 检查是否已存在测试应用
+    cursor.execute("SELECT id FROM apps WHERE app_key = %s", ("eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY",))
+    if cursor.fetchone():
+        print("✅ 测试应用已存在")
+        cursor.close()
+        conn.close()
+        return
+    
+    # 获取admin用户ID
+    cursor.execute("SELECT id FROM users WHERE username = 'admin'")
+    admin_result = cursor.fetchone()
+    if not admin_result:
+        print("❌ 未找到admin用户")
+        cursor.close()
+        conn.close()
+        return
+    
+    admin_id = admin_result[0]
+    
+    # 创建测试应用
+    app_id = str(uuid.uuid4())
+    app_key = "eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY"
+    app_secret = "LKJm5XHJFhhgxSv9nQhoQNNI3wrKyWGZCaPQ4qc43Lf5qfXdLAHoGAHhCYqApEpr"
+    
+    cursor.execute("""
+        INSERT INTO apps (
+            id, name, app_key, app_secret, description, icon_url,
+            redirect_uris, scope, is_active, is_trusted,
+            access_token_expires, refresh_token_expires, created_by,
+            created_at, updated_at, is_deleted
+        ) VALUES (
+            %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW(), %s
+        )
+    """, (
+        app_id,
+        "子系统演示应用",
+        app_key,
+        app_secret,
+        "用于演示SSO集成的子系统应用",
+        "",
+        json.dumps([
+            "http://localhost:8001/auth/callback",
+            "http://localhost:3001/auth/callback"
+        ]),
+        json.dumps(["profile", "email", "phone"]),
+        True,  # is_active
+        True,  # is_trusted
+        7200,  # access_token_expires (2小时)
+        2592000,  # refresh_token_expires (30天)
+        admin_id,
+        False  # is_deleted
+    ))
+    
+    conn.commit()
+    cursor.close()
+    conn.close()
+    
+    print("✅ 测试应用创建成功")
+    print(f"应用名称: 子系统演示应用")
+    print(f"App Key: {app_key}")
+    print(f"App Secret: {app_secret}")
+    print(f"回调URL: http://localhost:8001/auth/callback")
+    print(f"权限范围: profile, email, phone")
+    print(f"受信任应用: 是")
+
+if __name__ == "__main__":
+    create_test_app()

+ 77 - 0
fix_password.py

@@ -0,0 +1,77 @@
+#!/usr/bin/env python3
+"""
+修复密码哈希格式
+"""
+import pymysql
+from urllib.parse import urlparse
+import os
+from dotenv import load_dotenv
+import hashlib
+import secrets
+
+load_dotenv()
+
+def hash_password_simple(password):
+    """简单的密码哈希"""
+    # 生成盐值
+    salt = secrets.token_hex(16)
+    
+    # 使用SHA256哈希
+    password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
+    
+    return f"sha256${salt}${password_hash}"
+
+def fix_admin_password():
+    """修复admin用户密码"""
+    database_url = os.getenv('DATABASE_URL', '')
+    parsed = urlparse(database_url)
+    config = {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'database': parsed.path[1:] if parsed.path else 'sso_db',
+        'charset': 'utf8mb4'
+    }
+
+    try:
+        conn = pymysql.connect(**config)
+        cursor = conn.cursor()
+        
+        # 生成新的密码哈希
+        new_password = "Admin123456"
+        new_hash = hash_password_simple(new_password)
+        
+        print(f"新密码: {new_password}")
+        print(f"新哈希: {new_hash}")
+        
+        # 更新admin用户密码
+        cursor.execute(
+            'UPDATE users SET password_hash = %s WHERE username = %s',
+            (new_hash, 'admin')
+        )
+        
+        conn.commit()
+        
+        # 验证更新
+        cursor.execute('SELECT username, password_hash FROM users WHERE username = %s', ('admin',))
+        result = cursor.fetchone()
+        
+        if result:
+            username, password_hash = result
+            print(f"✅ 密码已更新")
+            print(f"用户名: {username}")
+            print(f"新哈希: {password_hash}")
+        
+        cursor.close()
+        conn.close()
+        
+        print("\n✅ admin用户密码修复完成")
+        print("用户名: admin")
+        print("密码: Admin123456")
+        
+    except Exception as e:
+        print(f'❌ 修复失败: {e}')
+
+if __name__ == "__main__":
+    fix_admin_password()

+ 2283 - 0
full_server.py

@@ -0,0 +1,2283 @@
+#!/usr/bin/env python3
+"""
+完整的SSO服务器 - 包含认证API
+"""
+import sys
+import os
+import socket
+import json
+import uuid
+
+# 添加src目录到Python路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from fastapi import FastAPI, HTTPException, Depends, Request
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from pydantic import BaseModel
+from typing import Optional
+import hashlib
+import secrets
+# 修复JWT导入 - 确保使用正确的JWT库
+try:
+    # 首先尝试使用PyJWT
+    import jwt as pyjwt
+    # 测试是否有encode方法
+    test_token = pyjwt.encode({"test": "data"}, "secret", algorithm="HS256")
+    jwt = pyjwt
+    print("✅ 使用PyJWT库")
+except (ImportError, AttributeError, TypeError) as e:
+    print(f"PyJWT导入失败: {e}")
+    try:
+        # 尝试使用python-jose
+        from jose import jwt
+        print("✅ 使用python-jose库")
+    except ImportError as e:
+        print(f"python-jose导入失败: {e}")
+        # 最后尝试安装PyJWT
+        print("尝试安装PyJWT...")
+        import subprocess
+        import sys
+        try:
+            subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"])
+            import jwt
+            print("✅ PyJWT安装成功")
+        except Exception as install_error:
+            print(f"❌ PyJWT安装失败: {install_error}")
+            raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
+
+from datetime import datetime, timedelta, timezone
+import pymysql
+from urllib.parse import urlparse
+
+# 数据模型
+class LoginRequest(BaseModel):
+    username: str
+    password: str
+    remember_me: bool = False
+
+class TokenResponse(BaseModel):
+    access_token: str
+    refresh_token: Optional[str] = None
+    token_type: str = "Bearer"
+    expires_in: int
+    scope: Optional[str] = None
+
+class UserInfo(BaseModel):
+    id: str
+    username: str
+    email: str
+    phone: Optional[str] = None
+    avatar_url: Optional[str] = None
+    is_active: bool
+    is_superuser: bool = False
+    roles: list = []
+    permissions: list = []
+
+class ApiResponse(BaseModel):
+    code: int
+    message: str
+    data: Optional[dict] = None
+    timestamp: str
+
+# 配置
+JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
+ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
+
+def check_port(port):
+    """检查端口是否可用"""
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+        try:
+            s.bind(('localhost', port))
+            return True
+        except OSError:
+            return False
+
+def find_available_port(start_port=8000, max_port=8010):
+    """查找可用端口"""
+    for port in range(start_port, max_port + 1):
+        if check_port(port):
+            return port
+    return None
+
+def get_db_connection():
+    """获取数据库连接"""
+    try:
+        database_url = os.getenv('DATABASE_URL', '')
+        if not database_url:
+            return None
+            
+        parsed = urlparse(database_url)
+        config = {
+            'host': parsed.hostname or 'localhost',
+            'port': parsed.port or 3306,
+            'user': parsed.username or 'root',
+            'password': parsed.password or '',
+            'database': parsed.path[1:] if parsed.path else 'sso_db',
+            'charset': 'utf8mb4'
+        }
+        
+        return pymysql.connect(**config)
+    except Exception as e:
+        print(f"数据库连接失败: {e}")
+        return None
+
+def verify_password_simple(password: str, stored_hash: str) -> bool:
+    """验证密码(简化版)"""
+    if stored_hash.startswith("sha256$"):
+        parts = stored_hash.split("$")
+        if len(parts) == 3:
+            salt = parts[1]
+            expected_hash = parts[2]
+            actual_hash = hashlib.sha256((password + salt).encode()).hexdigest()
+            return actual_hash == expected_hash
+    return False
+
+def create_access_token(data: dict) -> str:
+    """创建访问令牌"""
+    to_encode = data.copy()
+    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+    to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
+    
+    encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
+    return encoded_jwt
+
+def verify_token(token: str) -> Optional[dict]:
+    """验证令牌"""
+    try:
+        payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
+        return payload
+    except jwt.PyJWTError:
+        return None
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="SSO认证中心",
+    version="1.0.0",
+    description="OAuth2单点登录认证中心",
+    docs_url="/docs",
+    redoc_url="/redoc"
+)
+
+# 配置CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+security = HTTPBearer()
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return ApiResponse(
+        code=0,
+        message="欢迎使用SSO认证中心",
+        data={
+            "name": "SSO认证中心",
+            "version": "1.0.0",
+            "docs": "/docs"
+        },
+        timestamp=datetime.now(timezone.utc).isoformat()
+    ).model_dump()
+
+@app.get("/health")
+async def health_check():
+    """健康检查"""
+    return ApiResponse(
+        code=0,
+        message="服务正常运行",
+        data={
+            "status": "healthy",
+            "version": "1.0.0",
+            "timestamp": datetime.now(timezone.utc).isoformat()
+        },
+        timestamp=datetime.now(timezone.utc).isoformat()
+    ).model_dump()
+
+@app.post("/api/v1/auth/login")
+async def login(request: Request, login_data: LoginRequest):
+    """用户登录"""
+    print(f"🔐 收到登录请求: username={login_data.username}")
+    
+    try:
+        # 获取数据库连接
+        print("📊 尝试连接数据库...")
+        conn = get_db_connection()
+        if not conn:
+            print("❌ 数据库连接失败")
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        print("✅ 数据库连接成功")
+        cursor = conn.cursor()
+        
+        # 查找用户
+        print(f"🔍 查找用户: {login_data.username}")
+        cursor.execute(
+            "SELECT id, username, email, password_hash, is_active, is_superuser FROM users WHERE username = %s OR email = %s",
+            (login_data.username, login_data.username)
+        )
+        
+        user_data = cursor.fetchone()
+        print(f"👤 用户查询结果: {user_data is not None}")
+        
+        cursor.close()
+        conn.close()
+        
+        if not user_data:
+            print("❌ 用户不存在")
+            return ApiResponse(
+                code=200001,
+                message="用户名或密码错误",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id, username, email, password_hash, is_active, is_superuser = user_data
+        print(f"✅ 找到用户: {username}, 激活状态: {is_active}")
+        
+        # 检查用户状态
+        if not is_active:
+            print("❌ 用户已被禁用")
+            return ApiResponse(
+                code=200002,
+                message="用户已被禁用",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 验证密码
+        print(f"🔑 验证密码,哈希格式: {password_hash[:20]}...")
+        password_valid = verify_password_simple(login_data.password, password_hash)
+        print(f"🔑 密码验证结果: {password_valid}")
+        
+        if not password_valid:
+            print("❌ 密码验证失败")
+            return ApiResponse(
+                code=200001,
+                message="用户名或密码错误",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 生成令牌
+        print("🎫 生成访问令牌...")
+        token_data = {
+            "sub": user_id,
+            "username": username,
+            "email": email,
+            "is_superuser": is_superuser
+        }
+        
+        access_token = create_access_token(token_data)
+        print(f"✅ 令牌生成成功: {access_token[:50]}...")
+        
+        token_response = TokenResponse(
+            access_token=access_token,
+            token_type="Bearer",
+            expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
+            scope="profile email"
+        )
+        
+        print("🎉 登录成功")
+        return ApiResponse(
+            code=0,
+            message="登录成功",
+            data=token_response.model_dump(),
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"❌ 登录错误详情: {type(e).__name__}: {str(e)}")
+        import traceback
+        print(f"❌ 错误堆栈: {traceback.format_exc()}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.get("/api/v1/users/profile")
+async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """获取用户资料"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        if not user_id:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 查找用户详细信息
+        cursor.execute("""
+            SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
+                   u.last_login_at, u.created_at, u.updated_at,
+                   p.real_name, p.company, p.department, p.position
+            FROM users u
+            LEFT JOIN user_profiles p ON u.id = p.user_id
+            WHERE u.id = %s
+        """, (user_id,))
+        
+        user_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if not user_data:
+            return ApiResponse(
+                code=200001,
+                message="用户不存在",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 构建用户信息
+        user_info = {
+            "id": user_data[0],
+            "username": user_data[1],
+            "email": user_data[2],
+            "phone": user_data[3],
+            "avatar_url": user_data[4],
+            "is_active": user_data[5],
+            "is_superuser": user_data[6],
+            "last_login_at": user_data[7].isoformat() if user_data[7] else None,
+            "created_at": user_data[8].isoformat() if user_data[8] else None,
+            "updated_at": user_data[9].isoformat() if user_data[9] else None,
+            "real_name": user_data[10],
+            "company": user_data[11],
+            "department": user_data[12],
+            "position": user_data[13]
+        }
+        
+        return ApiResponse(
+            code=0,
+            message="获取用户资料成功",
+            data=user_info,
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"获取用户资料错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.put("/api/v1/users/profile")
+async def update_user_profile(
+    request: Request,
+    profile_data: dict,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """更新用户资料"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 更新用户基本信息
+        update_fields = []
+        update_values = []
+        
+        if 'email' in profile_data:
+            update_fields.append('email = %s')
+            update_values.append(profile_data['email'])
+        
+        if 'phone' in profile_data:
+            update_fields.append('phone = %s')
+            update_values.append(profile_data['phone'])
+        
+        if update_fields:
+            update_values.append(user_id)
+            cursor.execute(f"""
+                UPDATE users 
+                SET {', '.join(update_fields)}, updated_at = NOW()
+                WHERE id = %s
+            """, update_values)
+        
+        # 更新或插入用户详情
+        profile_fields = ['real_name', 'company', 'department', 'position']
+        profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
+        
+        if profile_updates:
+            # 检查是否已有记录
+            cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
+            profile_exists = cursor.fetchone()
+            
+            if profile_exists:
+                # 更新现有记录
+                update_fields = []
+                update_values = []
+                for field, value in profile_updates.items():
+                    update_fields.append(f'{field} = %s')
+                    update_values.append(value)
+                
+                update_values.append(user_id)
+                cursor.execute(f"""
+                    UPDATE user_profiles 
+                    SET {', '.join(update_fields)}, updated_at = NOW()
+                    WHERE user_id = %s
+                """, update_values)
+            else:
+                # 插入新记录
+                fields = ['user_id'] + list(profile_updates.keys())
+                values = [user_id] + list(profile_updates.values())
+                placeholders = ', '.join(['%s'] * len(values))
+                
+                cursor.execute(f"""
+                    INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
+                    VALUES ({placeholders}, NOW(), NOW())
+                """, values)
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        return ApiResponse(
+            code=0,
+            message="用户资料更新成功",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"更新用户资料错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.put("/api/v1/users/password")
+async def change_user_password(
+    request: Request,
+    password_data: dict,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """修改用户密码"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        old_password = password_data.get('old_password')
+        new_password = password_data.get('new_password')
+        
+        if not old_password or not new_password:
+            return ApiResponse(
+                code=100001,
+                message="缺少必要参数",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 验证当前密码
+        cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
+        result = cursor.fetchone()
+        
+        if not result or not verify_password_simple(old_password, result[0]):
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="当前密码错误",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 生成新密码哈希
+        new_password_hash = hash_password_simple(new_password)
+        
+        # 更新密码
+        cursor.execute("""
+            UPDATE users 
+            SET password_hash = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (new_password_hash, user_id))
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        return ApiResponse(
+            code=0,
+            message="密码修改成功",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"修改密码错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+def hash_password_simple(password):
+    """简单的密码哈希"""
+    import hashlib
+    import secrets
+    
+    # 生成盐值
+    salt = secrets.token_hex(16)
+    
+    # 使用SHA256哈希
+    password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
+    
+    return f"sha256${salt}${password_hash}"
+
+@app.post("/api/v1/auth/logout")
+async def logout():
+    """用户登出"""
+    return ApiResponse(
+        code=0,
+        message="登出成功",
+        timestamp=datetime.now(timezone.utc).isoformat()
+    ).model_dump()
+
+# OAuth2 授权端点
+@app.get("/oauth/authorize")
+async def oauth_authorize(
+    response_type: str,
+    client_id: str,
+    redirect_uri: str,
+    scope: str = "profile",
+    state: str = None
+):
+    """OAuth2授权端点"""
+    try:
+        print(f"🔐 OAuth授权请求: client_id={client_id}, redirect_uri={redirect_uri}, scope={scope}")
+        
+        # 验证必要参数
+        if not response_type or not client_id or not redirect_uri:
+            error_url = f"{redirect_uri}?error=invalid_request&error_description=Missing required parameters"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "invalid_request", "redirect_url": error_url}
+        
+        # 验证response_type
+        if response_type != "code":
+            error_url = f"{redirect_uri}?error=unsupported_response_type&error_description=Only authorization code flow is supported"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "unsupported_response_type", "redirect_url": error_url}
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "server_error", "redirect_url": error_url}
+        
+        cursor = conn.cursor()
+        
+        # 验证client_id和redirect_uri
+        cursor.execute("""
+            SELECT id, name, redirect_uris, scope, is_active, is_trusted
+            FROM apps 
+            WHERE app_key = %s AND is_active = 1
+        """, (client_id,))
+        
+        app_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if not app_data:
+            error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client_id"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "invalid_client", "redirect_url": error_url}
+        
+        app_id, app_name, redirect_uris_json, app_scope_json, is_active, is_trusted = app_data
+        
+        # 验证redirect_uri
+        redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
+        if redirect_uri not in redirect_uris:
+            error_url = f"{redirect_uri}?error=invalid_request&error_description=Invalid redirect_uri"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "invalid_request", "redirect_url": error_url}
+        
+        # 验证scope
+        app_scopes = json.loads(app_scope_json) if app_scope_json else []
+        requested_scopes = scope.split() if scope else []
+        invalid_scopes = [s for s in requested_scopes if s not in app_scopes]
+        if invalid_scopes:
+            error_url = f"{redirect_uri}?error=invalid_scope&error_description=Invalid scope: {' '.join(invalid_scopes)}"
+            if state:
+                error_url += f"&state={state}"
+            return {"error": "invalid_scope", "redirect_url": error_url}
+        
+        # TODO: 检查用户登录状态
+        # 这里应该检查用户是否已登录(通过session或cookie)
+        # 如果未登录,应该重定向到登录页面
+        
+        # 临时方案:返回登录页面,让用户先登录
+        # 生产环境应该使用session管理
+        
+        # 构建登录页面URL,登录后返回授权页面
+        login_page_url = f"/oauth/login?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
+        if state:
+            login_page_url += f"&state={state}"
+        
+        print(f"🔐 需要用户登录,重定向到登录页面: {login_page_url}")
+        
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=login_page_url, status_code=302)
+        
+        # 非受信任应用需要用户授权确认
+        # 这里返回授权页面HTML
+        authorization_html = f"""
+        <!DOCTYPE html>
+        <html>
+        <head>
+            <title>授权确认 - SSO认证中心</title>
+            <meta charset="utf-8">
+            <meta name="viewport" content="width=device-width, initial-scale=1">
+            <style>
+                body {{
+                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+                    margin: 0;
+                    padding: 20px;
+                    min-height: 100vh;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }}
+                .auth-container {{
+                    background: white;
+                    border-radius: 10px;
+                    padding: 40px;
+                    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
+                    max-width: 400px;
+                    width: 100%;
+                }}
+                .auth-header {{
+                    text-align: center;
+                    margin-bottom: 30px;
+                }}
+                .auth-header h1 {{
+                    color: #333;
+                    margin-bottom: 10px;
+                }}
+                .app-info {{
+                    background: #f8f9fa;
+                    padding: 20px;
+                    border-radius: 8px;
+                    margin-bottom: 20px;
+                }}
+                .scope-list {{
+                    list-style: none;
+                    padding: 0;
+                    margin: 10px 0;
+                }}
+                .scope-list li {{
+                    padding: 5px 0;
+                    color: #666;
+                }}
+                .scope-list li:before {{
+                    content: "✓ ";
+                    color: #28a745;
+                    font-weight: bold;
+                }}
+                .auth-buttons {{
+                    display: flex;
+                    gap: 10px;
+                    margin-top: 20px;
+                }}
+                .btn {{
+                    flex: 1;
+                    padding: 12px 20px;
+                    border: none;
+                    border-radius: 6px;
+                    font-size: 16px;
+                    cursor: pointer;
+                    text-decoration: none;
+                    text-align: center;
+                    display: inline-block;
+                }}
+                .btn-primary {{
+                    background: #007bff;
+                    color: white;
+                }}
+                .btn-secondary {{
+                    background: #6c757d;
+                    color: white;
+                }}
+                .btn:hover {{
+                    opacity: 0.9;
+                }}
+            </style>
+        </head>
+        <body>
+            <div class="auth-container">
+                <div class="auth-header">
+                    <h1>授权确认</h1>
+                    <p>应用请求访问您的账户</p>
+                </div>
+                
+                <div class="app-info">
+                    <h3>{app_name}</h3>
+                    <p>该应用请求以下权限:</p>
+                    <ul class="scope-list">
+        """
+        
+        # 添加权限列表
+        scope_descriptions = {
+            "profile": "访问您的基本信息(用户名、头像等)",
+            "email": "访问您的邮箱地址",
+            "phone": "访问您的手机号码",
+            "roles": "访问您的角色和权限信息"
+        }
+        
+        for scope_item in requested_scopes:
+            description = scope_descriptions.get(scope_item, f"访问 {scope_item} 信息")
+            authorization_html += f"<li>{description}</li>"
+        
+        authorization_html += f"""
+                    </ul>
+                </div>
+                
+                <div class="auth-buttons">
+                    <a href="/oauth/authorize/deny?client_id={client_id}&redirect_uri={redirect_uri}&state={state or ''}" class="btn btn-secondary">拒绝</a>
+                    <a href="/oauth/authorize/approve?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}" class="btn btn-primary">授权</a>
+                </div>
+            </div>
+        </body>
+        </html>
+        """
+        
+        from fastapi.responses import HTMLResponse
+        return HTMLResponse(content=authorization_html)
+        
+    except Exception as e:
+        print(f"❌ OAuth授权错误: {e}")
+        error_url = f"{redirect_uri}?error=server_error&error_description=Internal server error"
+        if state:
+            error_url += f"&state={state}"
+        return {"error": "server_error", "redirect_url": error_url}
+
+@app.get("/oauth/login")
+async def oauth_login_page(
+    response_type: str,
+    client_id: str,
+    redirect_uri: str,
+    scope: str = "profile",
+    state: str = None
+):
+    """OAuth2登录页面"""
+    try:
+        print(f"🔐 显示OAuth登录页面: client_id={client_id}")
+        
+        # 获取应用信息
+        conn = get_db_connection()
+        if not conn:
+            return {"error": "server_error", "message": "数据库连接失败"}
+        
+        cursor = conn.cursor()
+        cursor.execute("SELECT name FROM apps WHERE app_key = %s", (client_id,))
+        app_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        app_name = app_data[0] if app_data else "未知应用"
+        
+        # 构建登录页面HTML
+        login_html = f"""
+        <!DOCTYPE html>
+        <html lang="zh-CN">
+        <head>
+            <meta charset="UTF-8">
+            <meta name="viewport" content="width=device-width, initial-scale=1.0">
+            <title>SSO登录 - {app_name}</title>
+            <style>
+                body {{
+                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+                    margin: 0;
+                    padding: 20px;
+                    min-height: 100vh;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                }}
+                .login-container {{
+                    background: white;
+                    border-radius: 15px;
+                    padding: 40px;
+                    box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+                    max-width: 400px;
+                    width: 100%;
+                }}
+                .login-header {{
+                    text-align: center;
+                    margin-bottom: 30px;
+                }}
+                .login-header h1 {{
+                    color: #333;
+                    margin-bottom: 10px;
+                }}
+                .app-info {{
+                    background: #f8f9fa;
+                    padding: 15px;
+                    border-radius: 8px;
+                    margin-bottom: 20px;
+                    text-align: center;
+                }}
+                .form-group {{
+                    margin-bottom: 20px;
+                }}
+                .form-group label {{
+                    display: block;
+                    margin-bottom: 5px;
+                    font-weight: 500;
+                    color: #333;
+                }}
+                .form-group input {{
+                    width: 100%;
+                    padding: 12px;
+                    border: 1px solid #ddd;
+                    border-radius: 6px;
+                    font-size: 16px;
+                    box-sizing: border-box;
+                }}
+                .form-group input:focus {{
+                    outline: none;
+                    border-color: #007bff;
+                    box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+                }}
+                .btn {{
+                    width: 100%;
+                    padding: 12px;
+                    background: #007bff;
+                    color: white;
+                    border: none;
+                    border-radius: 6px;
+                    font-size: 16px;
+                    font-weight: 500;
+                    cursor: pointer;
+                    transition: background 0.3s;
+                }}
+                .btn:hover {{
+                    background: #0056b3;
+                }}
+                .btn:disabled {{
+                    background: #6c757d;
+                    cursor: not-allowed;
+                }}
+                .error-message {{
+                    color: #dc3545;
+                    font-size: 14px;
+                    margin-top: 10px;
+                    text-align: center;
+                }}
+                .success-message {{
+                    color: #28a745;
+                    font-size: 14px;
+                    margin-top: 10px;
+                    text-align: center;
+                }}
+            </style>
+        </head>
+        <body>
+            <div class="login-container">
+                <div class="login-header">
+                    <h1>🔐 SSO登录</h1>
+                    <p>请登录以继续访问应用</p>
+                </div>
+                
+                <div class="app-info">
+                    <strong>{app_name}</strong> 请求访问您的账户
+                </div>
+                
+                <form id="loginForm" onsubmit="handleLogin(event)">
+                    <div class="form-group">
+                        <label for="username">用户名或邮箱</label>
+                        <input type="text" id="username" name="username" required>
+                    </div>
+                    
+                    <div class="form-group">
+                        <label for="password">密码</label>
+                        <input type="password" id="password" name="password" required>
+                    </div>
+                    
+                    <button type="submit" class="btn" id="loginBtn">登录</button>
+                    
+                    <div id="message"></div>
+                </form>
+                
+                <div style="margin-top: 20px; text-align: center; font-size: 14px; color: #666;">
+                    <p>测试账号: admin / Admin123456</p>
+                </div>
+            </div>
+            
+            <script>
+                async function handleLogin(event) {{
+                    event.preventDefault();
+                    
+                    const loginBtn = document.getElementById('loginBtn');
+                    const messageDiv = document.getElementById('message');
+                    
+                    loginBtn.disabled = true;
+                    loginBtn.textContent = '登录中...';
+                    messageDiv.innerHTML = '';
+                    
+                    const formData = new FormData(event.target);
+                    const loginData = {{
+                        username: formData.get('username'),
+                        password: formData.get('password'),
+                        remember_me: false
+                    }};
+                    
+                    try {{
+                        // 调用登录API
+                        const response = await fetch('/api/v1/auth/login', {{
+                            method: 'POST',
+                            headers: {{
+                                'Content-Type': 'application/json'
+                            }},
+                            body: JSON.stringify(loginData)
+                        }});
+                        
+                        const result = await response.json();
+                        
+                        if (result.code === 0) {{
+                            messageDiv.innerHTML = '<div class="success-message">登录成功,正在跳转...</div>';
+                            
+                            // 登录成功后,重定向到授权页面
+                            const authUrl = `/oauth/authorize/authenticated?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}&access_token=${{result.data.access_token}}`;
+                            
+                            setTimeout(() => {{
+                                window.location.href = authUrl;
+                            }}, 1000);
+                        }} else {{
+                            messageDiv.innerHTML = `<div class="error-message">${{result.message}}</div>`;
+                        }}
+                    }} catch (error) {{
+                        messageDiv.innerHTML = '<div class="error-message">登录失败,请重试</div>';
+                    }} finally {{
+                        loginBtn.disabled = false;
+                        loginBtn.textContent = '登录';
+                    }}
+                }}
+            </script>
+        </body>
+        </html>
+        """
+        
+        from fastapi.responses import HTMLResponse
+        return HTMLResponse(content=login_html)
+        
+    except Exception as e:
+        print(f"❌ OAuth登录页面错误: {e}")
+        return {"error": "server_error", "message": "服务器内部错误"}
+
+@app.get("/oauth/authorize/authenticated")
+async def oauth_authorize_authenticated(
+    response_type: str,
+    client_id: str,
+    redirect_uri: str,
+    access_token: str,
+    scope: str = "profile",
+    state: str = None
+):
+    """用户已登录后的授权处理"""
+    try:
+        print(f"🔐 用户已登录,处理授权: client_id={client_id}")
+        
+        # 验证访问令牌
+        payload = verify_token(access_token)
+        if not payload:
+            error_url = f"{redirect_uri}?error=invalid_token&error_description=Invalid access token"
+            if state:
+                error_url += f"&state={state}"
+            from fastapi.responses import RedirectResponse
+            return RedirectResponse(url=error_url, status_code=302)
+        
+        user_id = payload.get("sub")
+        username = payload.get("username", "")
+        
+        print(f"✅ 用户已验证: {username} ({user_id})")
+        
+        # 获取应用信息
+        conn = get_db_connection()
+        if not conn:
+            error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
+            if state:
+                error_url += f"&state={state}"
+            from fastapi.responses import RedirectResponse
+            return RedirectResponse(url=error_url, status_code=302)
+        
+        cursor = conn.cursor()
+        cursor.execute("SELECT name, is_trusted FROM apps WHERE app_key = %s", (client_id,))
+        app_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if not app_data:
+            error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client"
+            if state:
+                error_url += f"&state={state}"
+            from fastapi.responses import RedirectResponse
+            return RedirectResponse(url=error_url, status_code=302)
+        
+        app_name, is_trusted = app_data
+        
+        # 如果是受信任应用,直接授权
+        if is_trusted:
+            # 生成授权码
+            auth_code = secrets.token_urlsafe(32)
+            
+            # TODO: 将授权码存储到数据库,关联用户和应用
+            # 这里简化处理,实际应该存储到数据库
+            
+            # 重定向回应用
+            callback_url = f"{redirect_uri}?code={auth_code}"
+            if state:
+                callback_url += f"&state={state}"
+            
+            print(f"✅ 受信任应用自动授权: {callback_url}")
+            
+            from fastapi.responses import RedirectResponse
+            return RedirectResponse(url=callback_url, status_code=302)
+        
+        # 非受信任应用,显示授权确认页面
+        # 这里可以返回授权确认页面的HTML
+        # 为简化,暂时也直接授权
+        auth_code = secrets.token_urlsafe(32)
+        callback_url = f"{redirect_uri}?code={auth_code}"
+        if state:
+            callback_url += f"&state={state}"
+        
+        print(f"✅ 用户授权完成: {callback_url}")
+        
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=callback_url, status_code=302)
+        
+    except Exception as e:
+        print(f"❌ 授权处理错误: {e}")
+        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
+        if state:
+            error_url += f"&state={state}"
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=error_url, status_code=302)
+
+async def oauth_approve(
+    client_id: str,
+    redirect_uri: str,
+    scope: str = "profile",
+    state: str = None
+):
+    """用户同意授权"""
+    try:
+        print(f"✅ 用户同意授权: client_id={client_id}")
+        
+        # 生成授权码
+        auth_code = secrets.token_urlsafe(32)
+        
+        # TODO: 将授权码存储到数据库,关联用户和应用
+        # 这里简化处理,实际应该:
+        # 1. 验证用户登录状态
+        # 2. 将授权码存储到数据库
+        # 3. 设置过期时间(通常10分钟)
+        
+        # 构建回调URL
+        callback_url = f"{redirect_uri}?code={auth_code}"
+        if state:
+            callback_url += f"&state={state}"
+        
+        print(f"🔄 重定向到: {callback_url}")
+        
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=callback_url, status_code=302)
+        
+    except Exception as e:
+        print(f"❌ 授权确认错误: {e}")
+        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
+        if state:
+            error_url += f"&state={state}"
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=error_url, status_code=302)
+
+@app.get("/oauth/authorize/deny")
+async def oauth_deny(
+    client_id: str,
+    redirect_uri: str,
+    state: str = None
+):
+    """用户拒绝授权"""
+    try:
+        print(f"❌ 用户拒绝授权: client_id={client_id}")
+        
+        # 构建错误回调URL
+        error_url = f"{redirect_uri}?error=access_denied&error_description=User denied authorization"
+        if state:
+            error_url += f"&state={state}"
+        
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=error_url, status_code=302)
+        
+    except Exception as e:
+        print(f"❌ 拒绝授权错误: {e}")
+        error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
+        if state:
+            error_url += f"&state={state}"
+        from fastapi.responses import RedirectResponse
+        return RedirectResponse(url=error_url, status_code=302)
+
+@app.post("/oauth/token")
+async def oauth_token(request: Request):
+    """OAuth2令牌端点"""
+    try:
+        # 获取请求数据
+        form_data = await request.form()
+        
+        grant_type = form_data.get("grant_type")
+        code = form_data.get("code")
+        redirect_uri = form_data.get("redirect_uri")
+        client_id = form_data.get("client_id")
+        client_secret = form_data.get("client_secret")
+        
+        print(f"🎫 令牌请求: grant_type={grant_type}, client_id={client_id}")
+        
+        # 验证grant_type
+        if grant_type != "authorization_code":
+            return {
+                "error": "unsupported_grant_type",
+                "error_description": "Only authorization_code grant type is supported"
+            }
+        
+        # 验证必要参数
+        if not code or not redirect_uri or not client_id:
+            return {
+                "error": "invalid_request",
+                "error_description": "Missing required parameters"
+            }
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return {
+                "error": "server_error",
+                "error_description": "Database connection failed"
+            }
+        
+        cursor = conn.cursor()
+        
+        # 验证客户端
+        cursor.execute("""
+            SELECT id, name, app_secret, redirect_uris, scope, is_active
+            FROM apps 
+            WHERE app_key = %s AND is_active = 1
+        """, (client_id,))
+        
+        app_data = cursor.fetchone()
+        
+        if not app_data:
+            cursor.close()
+            conn.close()
+            return {
+                "error": "invalid_client",
+                "error_description": "Invalid client credentials"
+            }
+        
+        app_id, app_name, stored_secret, redirect_uris_json, scope_json, is_active = app_data
+        
+        # 验证客户端密钥(如果提供了)
+        if client_secret and client_secret != stored_secret:
+            cursor.close()
+            conn.close()
+            return {
+                "error": "invalid_client",
+                "error_description": "Invalid client credentials"
+            }
+        
+        # 验证redirect_uri
+        redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
+        if redirect_uri not in redirect_uris:
+            cursor.close()
+            conn.close()
+            return {
+                "error": "invalid_grant",
+                "error_description": "Invalid redirect_uri"
+            }
+        
+        # TODO: 验证授权码
+        # 这里简化处理,实际应该:
+        # 1. 从数据库查找授权码
+        # 2. 验证授权码是否有效且未过期
+        # 3. 验证授权码是否已被使用
+        # 4. 获取关联的用户ID
+        
+        # 模拟用户ID(实际应该从授权码记录中获取)
+        user_id = "ed6a79d3-0083-4d81-8b48-fc522f686f74"  # admin用户ID
+        
+        # 生成访问令牌
+        token_data = {
+            "sub": user_id,
+            "client_id": client_id,
+            "scope": "profile email"
+        }
+        
+        access_token = create_access_token(token_data)
+        refresh_token = secrets.token_urlsafe(32)
+        
+        # TODO: 将令牌存储到数据库
+        
+        cursor.close()
+        conn.close()
+        
+        # 返回令牌响应
+        token_response = {
+            "access_token": access_token,
+            "token_type": "Bearer",
+            "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
+            "refresh_token": refresh_token,
+            "scope": "profile email"
+        }
+        
+        print(f"✅ 令牌生成成功: {access_token[:50]}...")
+        
+        return token_response
+        
+    except Exception as e:
+        print(f"❌ 令牌生成错误: {e}")
+        return {
+            "error": "server_error",
+            "error_description": "Internal server error"
+        }
+
+@app.get("/oauth/userinfo")
+async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
+    """OAuth2用户信息端点"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return {
+                "error": "invalid_token",
+                "error_description": "Invalid or expired access token"
+            }
+        
+        user_id = payload.get("sub")
+        client_id = payload.get("client_id")
+        scope = payload.get("scope", "").split()
+        
+        print(f"👤 用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return {
+                "error": "server_error",
+                "error_description": "Database connection failed"
+            }
+        
+        cursor = conn.cursor()
+        
+        # 查找用户信息
+        cursor.execute("""
+            SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active,
+                   p.real_name, p.company, p.department, p.position
+            FROM users u
+            LEFT JOIN user_profiles p ON u.id = p.user_id
+            WHERE u.id = %s AND u.is_active = 1
+        """, (user_id,))
+        
+        user_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if not user_data:
+            return {
+                "error": "invalid_token",
+                "error_description": "User not found or inactive"
+            }
+        
+        # 构建用户信息响应(根据scope过滤)
+        user_info = {"sub": user_data[0]}
+        
+        if "profile" in scope:
+            user_info.update({
+                "username": user_data[1],
+                "avatar_url": user_data[4],
+                "real_name": user_data[6],
+                "company": user_data[7],
+                "department": user_data[8],
+                "position": user_data[9]
+            })
+        
+        if "email" in scope:
+            user_info["email"] = user_data[2]
+        
+        if "phone" in scope:
+            user_info["phone"] = user_data[3]
+        
+        print(f"✅ 返回用户信息: {user_info}")
+        
+        return user_info
+        
+    except Exception as e:
+        print(f"❌ 获取用户信息错误: {e}")
+        return {
+            "error": "server_error",
+            "error_description": "Internal server error"
+        }
+
+@app.get("/api/v1/apps")
+async def get_apps(
+    page: int = 1,
+    page_size: int = 20,
+    keyword: str = "",
+    status: str = "",
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """获取应用列表"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 构建查询条件
+        where_conditions = ["created_by = %s"]
+        params = [user_id]
+        
+        if keyword:
+            where_conditions.append("(name LIKE %s OR description LIKE %s)")
+            params.extend([f"%{keyword}%", f"%{keyword}%"])
+        
+        if status == "active":
+            where_conditions.append("is_active = 1")
+        elif status == "inactive":
+            where_conditions.append("is_active = 0")
+        
+        where_clause = " AND ".join(where_conditions)
+        
+        # 查询总数
+        cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
+        total = cursor.fetchone()[0]
+        
+        # 查询应用列表
+        offset = (page - 1) * page_size
+        cursor.execute(f"""
+            SELECT id, name, app_key, description, icon_url, redirect_uris, scope,
+                   is_active, is_trusted, access_token_expires, refresh_token_expires,
+                   created_at, updated_at
+            FROM apps 
+            WHERE {where_clause}
+            ORDER BY created_at DESC
+            LIMIT %s OFFSET %s
+        """, params + [page_size, offset])
+        
+        apps = []
+        for row in cursor.fetchall():
+            app = {
+                "id": row[0],
+                "name": row[1],
+                "app_key": row[2],
+                "description": row[3],
+                "icon_url": row[4],
+                "redirect_uris": json.loads(row[5]) if row[5] else [],
+                "scope": json.loads(row[6]) if row[6] else [],
+                "is_active": bool(row[7]),
+                "is_trusted": bool(row[8]),
+                "access_token_expires": row[9],
+                "refresh_token_expires": row[10],
+                "created_at": row[11].isoformat() if row[11] else None,
+                "updated_at": row[12].isoformat() if row[12] else None,
+                # 模拟统计数据
+                "today_requests": secrets.randbelow(1000),
+                "active_users": secrets.randbelow(100)
+            }
+            apps.append(app)
+        
+        cursor.close()
+        conn.close()
+        
+        return ApiResponse(
+            code=0,
+            message="获取应用列表成功",
+            data={
+                "items": apps,
+                "total": total,
+                "page": page,
+                "page_size": page_size
+            },
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"获取应用列表错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.get("/api/v1/apps/{app_id}")
+async def get_app_detail(
+    app_id: str,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """获取应用详情(包含密钥)"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 查询应用详情(包含密钥)
+        cursor.execute("""
+            SELECT id, name, app_key, app_secret, description, icon_url, 
+                   redirect_uris, scope, is_active, is_trusted,
+                   access_token_expires, refresh_token_expires,
+                   created_at, updated_at
+            FROM apps 
+            WHERE id = %s AND created_by = %s
+        """, (app_id, user_id))
+        
+        app_data = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if not app_data:
+            return ApiResponse(
+                code=200001,
+                message="应用不存在或无权限",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        app_detail = {
+            "id": app_data[0],
+            "name": app_data[1],
+            "app_key": app_data[2],
+            "app_secret": app_data[3],
+            "description": app_data[4],
+            "icon_url": app_data[5],
+            "redirect_uris": json.loads(app_data[6]) if app_data[6] else [],
+            "scope": json.loads(app_data[7]) if app_data[7] else [],
+            "is_active": bool(app_data[8]),
+            "is_trusted": bool(app_data[9]),
+            "access_token_expires": app_data[10],
+            "refresh_token_expires": app_data[11],
+            "created_at": app_data[12].isoformat() if app_data[12] else None,
+            "updated_at": app_data[13].isoformat() if app_data[13] else None
+        }
+        
+        return ApiResponse(
+            code=0,
+            message="获取应用详情成功",
+            data=app_detail,
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"获取应用详情错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.post("/api/v1/apps")
+async def create_app(
+    request: Request,
+    app_data: dict,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """创建应用"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 验证必要字段
+        if not app_data.get('name') or not app_data.get('redirect_uris'):
+            return ApiResponse(
+                code=100001,
+                message="缺少必要参数",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 生成应用ID和密钥
+        app_id = str(uuid.uuid4())
+        app_key = generate_random_string(32)
+        app_secret = generate_random_string(64)
+        
+        # 插入应用记录
+        cursor.execute("""
+            INSERT INTO apps (
+                id, name, app_key, app_secret, description, icon_url,
+                redirect_uris, scope, is_active, is_trusted,
+                access_token_expires, refresh_token_expires, created_by,
+                created_at, updated_at
+            ) VALUES (
+                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
+            )
+        """, (
+            app_id,
+            app_data['name'],
+            app_key,
+            app_secret,
+            app_data.get('description', ''),
+            app_data.get('icon_url', ''),
+            json.dumps(app_data['redirect_uris']),
+            json.dumps(app_data.get('scope', ['profile'])),
+            True,
+            app_data.get('is_trusted', False),
+            app_data.get('access_token_expires', 7200),
+            app_data.get('refresh_token_expires', 2592000),
+            user_id
+        ))
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        # 返回创建的应用信息
+        app_info = {
+            "id": app_id,
+            "name": app_data['name'],
+            "app_key": app_key,
+            "app_secret": app_secret,
+            "description": app_data.get('description', ''),
+            "redirect_uris": app_data['redirect_uris'],
+            "scope": app_data.get('scope', ['profile']),
+            "is_active": True,
+            "is_trusted": app_data.get('is_trusted', False)
+        }
+        
+        return ApiResponse(
+            code=0,
+            message="应用创建成功",
+            data=app_info,
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"创建应用错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.put("/api/v1/apps/{app_id}/status")
+async def toggle_app_status(
+    app_id: str,
+    status_data: dict,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """切换应用状态"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        is_active = status_data.get('is_active')
+        
+        if is_active is None:
+            return ApiResponse(
+                code=100001,
+                message="缺少必要参数",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 检查应用是否存在且属于当前用户
+        cursor.execute("""
+            SELECT id, name FROM apps 
+            WHERE id = %s AND created_by = %s
+        """, (app_id, user_id))
+        
+        app_data = cursor.fetchone()
+        
+        if not app_data:
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="应用不存在或无权限",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 更新应用状态
+        cursor.execute("""
+            UPDATE apps 
+            SET is_active = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (is_active, app_id))
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        action = "启用" if is_active else "禁用"
+        print(f"✅ 应用状态已更新: {app_data[1]} -> {action}")
+        
+        return ApiResponse(
+            code=0,
+            message=f"应用已{action}",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"切换应用状态错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.put("/api/v1/apps/{app_id}")
+async def update_app(
+    app_id: str,
+    app_data: dict,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """更新应用信息"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 验证必要参数
+        name = app_data.get('name', '').strip()
+        if not name:
+            return ApiResponse(
+                code=100001,
+                message="应用名称不能为空",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 检查应用是否存在且属于当前用户
+        cursor.execute("""
+            SELECT id, name FROM apps 
+            WHERE id = %s AND created_by = %s
+        """, (app_id, user_id))
+        
+        existing_app = cursor.fetchone()
+        
+        if not existing_app:
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="应用不存在或无权限",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 检查应用名称是否已被其他应用使用
+        cursor.execute("""
+            SELECT id FROM apps 
+            WHERE name = %s AND created_by = %s AND id != %s
+        """, (name, user_id, app_id))
+        
+        if cursor.fetchone():
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="应用名称已存在",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 准备更新数据
+        description = (app_data.get('description') or '').strip()
+        icon_url = (app_data.get('icon_url') or '').strip()
+        redirect_uris = app_data.get('redirect_uris', [])
+        scope = app_data.get('scope', ['profile', 'email'])
+        is_trusted = app_data.get('is_trusted', False)
+        access_token_expires = app_data.get('access_token_expires', 7200)
+        refresh_token_expires = app_data.get('refresh_token_expires', 2592000)
+        
+        # 验证回调URL
+        if not redirect_uris or not isinstance(redirect_uris, list):
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=100001,
+                message="至少需要一个回调URL",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 验证权限范围
+        if not scope or not isinstance(scope, list):
+            scope = ['profile', 'email']
+        
+        # 更新应用信息
+        cursor.execute("""
+            UPDATE apps 
+            SET name = %s, description = %s, icon_url = %s, 
+                redirect_uris = %s, scope = %s, is_trusted = %s,
+                access_token_expires = %s, refresh_token_expires = %s,
+                updated_at = NOW()
+            WHERE id = %s
+        """, (
+            name, description, icon_url,
+            json.dumps(redirect_uris), json.dumps(scope), is_trusted,
+            access_token_expires, refresh_token_expires, app_id
+        ))
+        
+        conn.commit()
+        
+        # 获取更新后的应用信息
+        cursor.execute("""
+            SELECT id, name, app_key, description, icon_url, 
+                   redirect_uris, scope, is_active, is_trusted,
+                   access_token_expires, refresh_token_expires,
+                   created_at, updated_at
+            FROM apps 
+            WHERE id = %s
+        """, (app_id,))
+        
+        app_info = cursor.fetchone()
+        cursor.close()
+        conn.close()
+        
+        if app_info:
+            app_result = {
+                "id": app_info[0],
+                "name": app_info[1],
+                "app_key": app_info[2],
+                "description": app_info[3],
+                "icon_url": app_info[4],
+                "redirect_uris": json.loads(app_info[5]) if app_info[5] else [],
+                "scope": json.loads(app_info[6]) if app_info[6] else [],
+                "is_active": bool(app_info[7]),
+                "is_trusted": bool(app_info[8]),
+                "access_token_expires": app_info[9],
+                "refresh_token_expires": app_info[10],
+                "created_at": app_info[11].isoformat() if app_info[11] else None,
+                "updated_at": app_info[12].isoformat() if app_info[12] else None
+            }
+            
+            print(f"✅ 应用已更新: {name}")
+            
+            return ApiResponse(
+                code=0,
+                message="应用更新成功",
+                data=app_result,
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        else:
+            return ApiResponse(
+                code=500001,
+                message="获取更新后的应用信息失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+    except Exception as e:
+        print(f"更新应用错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.delete("/api/v1/apps/{app_id}")
+async def delete_app(
+    app_id: str,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """删除应用"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 检查应用是否存在且属于当前用户
+        cursor.execute("""
+            SELECT id, name FROM apps 
+            WHERE id = %s AND created_by = %s
+        """, (app_id, user_id))
+        
+        app_data = cursor.fetchone()
+        
+        if not app_data:
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="应用不存在或无权限",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 删除应用(级联删除相关数据)
+        cursor.execute("DELETE FROM apps WHERE id = %s", (app_id,))
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        print(f"✅ 应用已删除: {app_data[1]}")
+        
+        return ApiResponse(
+            code=0,
+            message="应用已删除",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"删除应用错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+@app.post("/api/v1/apps/{app_id}/reset-secret")
+async def reset_app_secret(
+    app_id: str,
+    credentials: HTTPAuthorizationCredentials = Depends(security)
+):
+    """重置应用密钥"""
+    try:
+        # 验证令牌
+        payload = verify_token(credentials.credentials)
+        if not payload:
+            return ApiResponse(
+                code=200002,
+                message="无效的访问令牌",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        user_id = payload.get("sub")
+        
+        # 获取数据库连接
+        conn = get_db_connection()
+        if not conn:
+            return ApiResponse(
+                code=500001,
+                message="数据库连接失败",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        cursor = conn.cursor()
+        
+        # 检查应用是否存在且属于当前用户
+        cursor.execute("""
+            SELECT id, name FROM apps 
+            WHERE id = %s AND created_by = %s
+        """, (app_id, user_id))
+        
+        app_data = cursor.fetchone()
+        
+        if not app_data:
+            cursor.close()
+            conn.close()
+            return ApiResponse(
+                code=200001,
+                message="应用不存在或无权限",
+                timestamp=datetime.now(timezone.utc).isoformat()
+            ).model_dump()
+        
+        # 生成新的应用密钥
+        new_secret = generate_random_string(64)
+        
+        # 更新应用密钥
+        cursor.execute("""
+            UPDATE apps 
+            SET app_secret = %s, updated_at = NOW()
+            WHERE id = %s
+        """, (new_secret, app_id))
+        
+        conn.commit()
+        cursor.close()
+        conn.close()
+        
+        print(f"✅ 应用密钥已重置: {app_data[1]}")
+        
+        return ApiResponse(
+            code=0,
+            message="应用密钥已重置",
+            data={"app_secret": new_secret},
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"重置应用密钥错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="服务器内部错误",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+def generate_random_string(length=32):
+    """生成随机字符串"""
+    import secrets
+    import string
+    alphabet = string.ascii_letters + string.digits
+    return ''.join(secrets.choice(alphabet) for _ in range(length))
+    """获取验证码"""
+    try:
+        # 生成验证码
+        captcha_text, captcha_image = generate_captcha()
+        
+        # 这里应该将验证码文本存储到缓存中(Redis或内存)
+        # 为了简化,我们暂时返回固定的验证码
+        captcha_id = secrets.token_hex(16)
+        
+        return ApiResponse(
+            code=0,
+            message="获取验证码成功",
+            data={
+                "captcha_id": captcha_id,
+                "captcha_image": captcha_image,
+                "captcha_text": captcha_text  # 生产环境中不应该返回这个
+            },
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"生成验证码错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="生成验证码失败",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+def generate_captcha():
+    """生成验证码"""
+    try:
+        from PIL import Image, ImageDraw, ImageFont
+        import io
+        import base64
+        import random
+        import string
+        
+        # 生成随机验证码文本
+        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
+        
+        # 创建图片
+        width, height = 120, 40
+        image = Image.new('RGB', (width, height), color='white')
+        draw = ImageDraw.Draw(image)
+        
+        # 尝试使用系统字体,如果失败则使用默认字体
+        try:
+            # Windows系统字体
+            font = ImageFont.truetype("arial.ttf", 20)
+        except:
+            try:
+                # 备用字体
+                font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20)
+            except:
+                # 使用默认字体
+                font = ImageFont.load_default()
+        
+        # 绘制验证码文本
+        text_width = draw.textlength(captcha_text, font=font)
+        text_height = 20
+        x = (width - text_width) // 2
+        y = (height - text_height) // 2
+        
+        # 添加一些随机颜色
+        colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
+        text_color = random.choice(colors)
+        
+        draw.text((x, y), captcha_text, fill=text_color, font=font)
+        
+        # 添加一些干扰线
+        for _ in range(3):
+            x1 = random.randint(0, width)
+            y1 = random.randint(0, height)
+            x2 = random.randint(0, width)
+            y2 = random.randint(0, height)
+            draw.line([(x1, y1), (x2, y2)], fill=random.choice(colors), width=1)
+        
+        # 添加一些干扰点
+        for _ in range(20):
+            x = random.randint(0, width)
+            y = random.randint(0, height)
+            draw.point((x, y), fill=random.choice(colors))
+        
+        # 转换为base64
+        buffer = io.BytesIO()
+        image.save(buffer, format='PNG')
+        image_data = buffer.getvalue()
+        image_base64 = base64.b64encode(image_data).decode('utf-8')
+        
+        return captcha_text, f"data:image/png;base64,{image_base64}"
+        
+    except ImportError:
+        # 如果PIL不可用,返回简单的文本验证码
+        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
+        # 创建一个简单的SVG验证码
+        svg_captcha = f"""
+        <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
+            <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
+            <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
+        </svg>
+        """
+        svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
+        return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
+    except Exception as e:
+        print(f"生成验证码图片失败: {e}")
+        # 返回默认验证码
+        return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
+
+if __name__ == "__main__":
+    import uvicorn
+    
+    # 查找可用端口
+    port = find_available_port()
+    
+    if port is None:
+        print("❌ 无法找到可用端口 (8000-8010)")
+        print("请手动停止占用这些端口的进程")
+        sys.exit(1)
+    
+    print("=" * 60)
+    print("🚀 SSO认证中心完整服务器")
+    print("=" * 60)
+    print(f"✅ 找到可用端口: {port}")
+    print(f"🌐 访问地址: http://localhost:{port}")
+    print(f"📚 API文档: http://localhost:{port}/docs")
+    print(f"❤️  健康检查: http://localhost:{port}/health")
+    print(f"🔐 登录API: http://localhost:{port}/api/v1/auth/login")
+    print("=" * 60)
+    print("📝 前端配置:")
+    print(f"   VITE_API_BASE_URL=http://localhost:{port}")
+    print("=" * 60)
+    print("👤 测试账号:")
+    print("   用户名: admin")
+    print("   密码: Admin123456")
+    print("=" * 60)
+    print("按 Ctrl+C 停止服务器")
+    print()
+    
+    try:
+        uvicorn.run(
+            app,
+            host="0.0.0.0",
+            port=port,
+            log_level="info"
+        )
+    except KeyboardInterrupt:
+        print("\n👋 服务器已停止")
+    except Exception as e:
+        print(f"❌ 启动失败: {e}")
+        sys.exit(1)
+
+@app.get("/api/v1/auth/captcha")
+async def get_captcha():
+    """获取验证码"""
+    try:
+        # 生成验证码
+        captcha_text, captcha_image = generate_captcha()
+        
+        # 这里应该将验证码文本存储到缓存中(Redis或内存)
+        # 为了简化,我们暂时返回固定的验证码
+        captcha_id = secrets.token_hex(16)
+        
+        return ApiResponse(
+            code=0,
+            message="获取验证码成功",
+            data={
+                "captcha_id": captcha_id,
+                "captcha_image": captcha_image,
+                "captcha_text": captcha_text  # 生产环境中不应该返回这个
+            },
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+        
+    except Exception as e:
+        print(f"生成验证码错误: {e}")
+        return ApiResponse(
+            code=500001,
+            message="生成验证码失败",
+            timestamp=datetime.now(timezone.utc).isoformat()
+        ).model_dump()
+
+def generate_captcha():
+    """生成验证码"""
+    try:
+        import random
+        import string
+        import base64
+        
+        # 生成随机验证码文本
+        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
+        
+        # 创建一个简单的SVG验证码
+        svg_captcha = f"""
+        <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
+            <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
+            <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
+        </svg>
+        """
+        svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
+        return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
+        
+    except Exception as e:
+        print(f"生成验证码失败: {e}")
+        # 返回默认验证码
+        return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="

+ 72 - 0
install_deps.py

@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""
+依赖安装脚本
+"""
+import subprocess
+import sys
+import os
+
+def run_command(command):
+    """运行命令"""
+    print(f"执行命令: {command}")
+    result = subprocess.run(command, shell=True, capture_output=True, text=True)
+    if result.returncode != 0:
+        print(f"命令执行失败: {result.stderr}")
+        return False
+    print(f"命令执行成功: {result.stdout}")
+    return True
+
+def install_dependencies():
+    """安装依赖"""
+    print("🚀 开始安装Python依赖...")
+    
+    # 升级pip
+    if not run_command(f"{sys.executable} -m pip install --upgrade pip"):
+        print("❌ 升级pip失败")
+        return False
+    
+    # 安装基础依赖
+    dependencies = [
+        "fastapi==0.104.1",
+        "uvicorn[standard]==0.24.0",
+        "sqlalchemy==2.0.23",
+        "aiomysql==0.2.0",
+        "python-jose[cryptography]==3.3.0",
+        "passlib[bcrypt]==1.7.4",
+        "pydantic==2.5.0",
+        "pydantic-settings==2.1.0",
+        "python-multipart==0.0.6",
+        "python-dotenv==1.0.0",
+        "email-validator==2.1.0",
+        "httpx==0.25.2",
+        "Pillow==10.1.0",
+        "python-dateutil==2.8.2"
+    ]
+    
+    for dep in dependencies:
+        print(f"安装 {dep}...")
+        if not run_command(f"{sys.executable} -m pip install {dep}"):
+            print(f"❌ 安装 {dep} 失败")
+            return False
+    
+    print("✅ 所有依赖安装成功!")
+    return True
+
+def main():
+    """主函数"""
+    print("=" * 50)
+    print("SSO后端依赖安装脚本")
+    print("=" * 50)
+    
+    if install_dependencies():
+        print("\n🎉 依赖安装完成!")
+        print("\n下一步:")
+        print("1. 配置数据库连接 (编辑 .env 文件)")
+        print("2. 初始化数据库: python scripts/init_db.py")
+        print("3. 启动服务: cd src && python -m app.main")
+    else:
+        print("\n❌ 依赖安装失败,请检查错误信息")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()

+ 31 - 0
load_env.py

@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+"""
+加载环境变量脚本
+"""
+import os
+from dotenv import load_dotenv
+
+def load_environment():
+    """加载环境变量"""
+    # 尝试加载.env文件
+    env_files = ['.env', '.env.development', '.env.local']
+    
+    for env_file in env_files:
+        if os.path.exists(env_file):
+            print(f"加载环境变量文件: {env_file}")
+            load_dotenv(env_file)
+            return True
+    
+    print("未找到环境变量文件,使用默认配置")
+    return False
+
+if __name__ == "__main__":
+    load_environment()
+    
+    # 显示关键配置
+    print("\n关键配置:")
+    print(f"DATABASE_URL: {os.getenv('DATABASE_URL', '未设置')}")
+    print(f"SECRET_KEY: {'已设置' if os.getenv('SECRET_KEY') else '未设置'}")
+    print(f"JWT_SECRET_KEY: {'已设置' if os.getenv('JWT_SECRET_KEY') else '未设置'}")
+    print(f"DEBUG: {os.getenv('DEBUG', 'True')}")
+    print(f"PORT: {os.getenv('PORT', '8000')}")

+ 117 - 0
quick_start.py

@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+"""
+快速启动脚本 - 自动检测可用端口并启动服务器
+"""
+import sys
+import os
+import socket
+
+# 添加src目录到Python路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+def check_port(port):
+    """检查端口是否可用"""
+    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
+        try:
+            s.bind(('localhost', port))
+            return True
+        except OSError:
+            return False
+
+def find_available_port(start_port=8000, max_port=8010):
+    """查找可用端口"""
+    for port in range(start_port, max_port + 1):
+        if check_port(port):
+            return port
+    return None
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="SSO认证中心",
+    version="1.0.0",
+    description="OAuth2单点登录认证中心",
+    docs_url="/docs",
+    redoc_url="/redoc"
+)
+
+# 配置CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {
+        "message": "SSO认证中心",
+        "version": "1.0.0",
+        "status": "running",
+        "docs": "/docs",
+        "health": "/health"
+    }
+
+@app.get("/health")
+async def health_check():
+    """健康检查"""
+    return {
+        "status": "healthy",
+        "message": "服务正常运行"
+    }
+
+@app.get("/test")
+async def test_endpoint():
+    """测试端点"""
+    return {
+        "message": "测试成功",
+        "data": {
+            "server": "FastAPI",
+            "python_version": sys.version,
+            "working_directory": os.getcwd()
+        }
+    }
+
+if __name__ == "__main__":
+    import uvicorn
+    
+    # 查找可用端口
+    port = find_available_port()
+    
+    if port is None:
+        print("❌ 无法找到可用端口 (8000-8010)")
+        print("请手动停止占用这些端口的进程")
+        sys.exit(1)
+    
+    print("=" * 50)
+    print("🚀 SSO认证中心测试服务器")
+    print("=" * 50)
+    print(f"✅ 找到可用端口: {port}")
+    print(f"🌐 访问地址: http://localhost:{port}")
+    print(f"📚 API文档: http://localhost:{port}/docs")
+    print(f"❤️  健康检查: http://localhost:{port}/health")
+    print("=" * 50)
+    print("按 Ctrl+C 停止服务器")
+    print()
+    
+    try:
+        uvicorn.run(
+            app,
+            host="0.0.0.0",
+            port=port,
+            log_level="info"
+        )
+    except KeyboardInterrupt:
+        print("\n👋 服务器已停止")
+    except Exception as e:
+        print(f"❌ 启动失败: {e}")
+        sys.exit(1)

+ 44 - 0
requirements/base.txt

@@ -0,0 +1,44 @@
+# FastAPI核心依赖
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+python-multipart==0.0.6
+
+# 数据库相关
+sqlalchemy==2.0.23
+alembic==1.12.1
+aiomysql==0.2.0
+asyncpg==0.29.0
+
+# 认证和安全
+python-jose[cryptography]==3.3.0
+passlib[bcrypt]==1.7.4
+python-multipart==0.0.6
+cryptography==41.0.7
+
+# Redis缓存
+redis==5.0.1
+aioredis==2.0.1
+
+# 配置管理
+pydantic==2.5.0
+pydantic-settings==2.1.0
+
+# 工具库
+python-dateutil==2.8.2
+email-validator==2.1.0
+phonenumbers==8.13.25
+Pillow==10.1.0
+
+# HTTP客户端
+httpx==0.25.2
+aiofiles==23.2.1
+
+# 日志
+loguru==0.7.2
+
+# 任务队列
+celery==5.3.4
+flower==2.0.1
+
+# 开发工具
+python-dotenv==1.0.0

+ 21 - 0
requirements/dev.txt

@@ -0,0 +1,21 @@
+-r base.txt
+
+# 开发工具
+pytest==7.4.3
+pytest-asyncio==0.21.1
+pytest-cov==4.1.0
+httpx==0.25.2
+
+# 代码质量
+black==23.11.0
+isort==5.12.0
+flake8==6.1.0
+mypy==1.7.1
+
+# 调试工具
+ipython==8.17.2
+ipdb==0.13.13
+
+# 文档生成
+mkdocs==1.5.3
+mkdocs-material==9.4.8

+ 25 - 0
run_server.py

@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+"""
+服务器启动脚本
+"""
+import sys
+import os
+
+# 添加src目录到Python路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+if __name__ == "__main__":
+    import uvicorn
+    
+    # 启动服务器
+    uvicorn.run(
+        "app.main:app",
+        host="0.0.0.0",
+        port=8000,
+        reload=True,
+        log_level="info"
+    )

+ 164 - 0
scripts/init_db.py

@@ -0,0 +1,164 @@
+"""
+数据库初始化脚本
+"""
+import asyncio
+import sys
+import os
+
+# 添加项目根目录到Python路径
+sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from app.config.database import init_db, engine
+from app.models import *  # 导入所有模型
+from app.utils.security import hash_password, generate_app_key, generate_app_secret
+from sqlalchemy.ext.asyncio import AsyncSession
+from datetime import datetime
+import uuid
+
+
+async def create_default_data():
+    """创建默认数据"""
+    async with AsyncSession(engine) as session:
+        try:
+            # 创建默认超级管理员
+            admin_password = "Admin123456"  # 使用较短的密码
+            admin_user = User(
+                id=str(uuid.uuid4()),
+                username="admin",
+                email="admin@example.com",
+                password_hash=hash_password(admin_password),
+                is_active=True,
+                is_superuser=True,
+                created_at=datetime.utcnow(),
+                updated_at=datetime.utcnow()
+            )
+            session.add(admin_user)
+            
+            # 创建默认角色
+            admin_role = Role(
+                id=str(uuid.uuid4()),
+                name="超级管理员",
+                code="super_admin",
+                description="系统超级管理员角色",
+                is_active=True,
+                created_at=datetime.utcnow(),
+                updated_at=datetime.utcnow()
+            )
+            session.add(admin_role)
+            
+            user_role = Role(
+                id=str(uuid.uuid4()),
+                name="普通用户",
+                code="user",
+                description="普通用户角色",
+                is_active=True,
+                created_at=datetime.utcnow(),
+                updated_at=datetime.utcnow()
+            )
+            session.add(user_role)
+            
+            # 创建默认权限
+            permissions = [
+                ("用户管理", "user:manage", "管理用户", "user", "manage"),
+                ("用户查看", "user:view", "查看用户", "user", "view"),
+                ("应用管理", "app:manage", "管理应用", "app", "manage"),
+                ("应用查看", "app:view", "查看应用", "app", "view"),
+                ("系统管理", "system:manage", "系统管理", "system", "manage"),
+            ]
+            
+            permission_objects = []
+            for name, code, desc, resource, action in permissions:
+                permission = Permission(
+                    id=str(uuid.uuid4()),
+                    name=name,
+                    code=code,
+                    description=desc,
+                    resource=resource,
+                    action=action,
+                    is_active=True,
+                    created_at=datetime.utcnow(),
+                    updated_at=datetime.utcnow()
+                )
+                session.add(permission)
+                permission_objects.append(permission)
+            
+            await session.flush()  # 获取ID
+            
+            # 分配用户角色
+            user_role_rel = UserRole(
+                id=str(uuid.uuid4()),
+                user_id=admin_user.id,
+                role_id=admin_role.id,
+                created_at=datetime.utcnow(),
+                updated_at=datetime.utcnow()
+            )
+            session.add(user_role_rel)
+            
+            # 分配角色权限
+            for permission in permission_objects:
+                role_permission = RolePermission(
+                    id=str(uuid.uuid4()),
+                    role_id=admin_role.id,
+                    permission_id=permission.id,
+                    created_at=datetime.utcnow(),
+                    updated_at=datetime.utcnow()
+                )
+                session.add(role_permission)
+            
+            # 创建默认测试应用
+            test_app = App(
+                id=str(uuid.uuid4()),
+                name="测试应用",
+                app_key=generate_app_key(),
+                app_secret=generate_app_secret(),
+                description="用于测试的默认应用",
+                redirect_uris=["http://localhost:3000/callback", "http://localhost:8080/callback"],
+                scope=["profile", "email"],
+                is_active=True,
+                is_trusted=True,
+                access_token_expires=7200,
+                refresh_token_expires=2592000,
+                created_by=admin_user.id,
+                created_at=datetime.utcnow(),
+                updated_at=datetime.utcnow()
+            )
+            session.add(test_app)
+            
+            await session.commit()
+            print("✅ 默认数据创建成功")
+            print(f"管理员账号: admin")
+            print(f"管理员密码: {admin_password}")
+            print(f"测试应用Key: {test_app.app_key}")
+            print(f"测试应用Secret: {test_app.app_secret}")
+            
+        except Exception as e:
+            await session.rollback()
+            print(f"❌ 创建默认数据失败: {e}")
+            raise
+
+
+async def main():
+    """主函数"""
+    print("🚀 开始初始化数据库...")
+    
+    try:
+        # 创建所有表
+        await init_db()
+        print("✅ 数据库表创建成功")
+        
+        # 创建默认数据
+        await create_default_data()
+        
+        print("🎉 数据库初始化完成!")
+        
+    except Exception as e:
+        print(f"❌ 数据库初始化失败: {e}")
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())

+ 277 - 0
simple_init_db.py

@@ -0,0 +1,277 @@
+#!/usr/bin/env python3
+"""
+简化的数据库初始化脚本
+"""
+import sys
+import os
+
+# 添加项目根目录到Python路径
+sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+import pymysql
+from urllib.parse import urlparse
+import uuid
+from datetime import datetime
+
+def get_db_config():
+    """获取数据库配置"""
+    database_url = os.getenv('DATABASE_URL', '')
+    parsed = urlparse(database_url)
+    
+    return {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'database': parsed.path[1:] if parsed.path else 'sso_db',
+        'charset': 'utf8mb4'
+    }
+
+def hash_password_simple(password):
+    """简单的密码哈希"""
+    import hashlib
+    import secrets
+    
+    # 生成盐值
+    salt = secrets.token_hex(16)
+    
+    # 使用SHA256哈希
+    password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
+    
+    return f"sha256${salt}${password_hash}"
+
+def generate_random_string(length=32):
+    """生成随机字符串"""
+    import secrets
+    import string
+    alphabet = string.ascii_letters + string.digits
+    return ''.join(secrets.choice(alphabet) for _ in range(length))
+
+def create_tables(connection):
+    """创建数据库表"""
+    cursor = connection.cursor()
+    
+    # 用户表
+    cursor.execute("""
+        CREATE TABLE IF NOT EXISTS users (
+            id VARCHAR(36) PRIMARY KEY COMMENT '用户ID',
+            username VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
+            email VARCHAR(100) UNIQUE NOT NULL COMMENT '邮箱',
+            phone VARCHAR(20) UNIQUE COMMENT '手机号',
+            password_hash VARCHAR(255) NOT NULL COMMENT '密码哈希',
+            avatar_url VARCHAR(500) COMMENT '头像URL',
+            is_active BOOLEAN DEFAULT TRUE COMMENT '是否激活',
+            is_superuser BOOLEAN DEFAULT FALSE COMMENT '是否超级管理员',
+            last_login_at TIMESTAMP NULL COMMENT '最后登录时间',
+            last_login_ip VARCHAR(45) COMMENT '最后登录IP',
+            failed_login_attempts INT DEFAULT 0 COMMENT '失败登录次数',
+            locked_until TIMESTAMP NULL COMMENT '锁定直到时间',
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+            is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
+            
+            INDEX idx_username (username),
+            INDEX idx_email (email),
+            INDEX idx_phone (phone),
+            INDEX idx_created_at (created_at)
+        ) COMMENT='用户表'
+    """)
+    
+    # 应用表
+    cursor.execute("""
+        CREATE TABLE IF NOT EXISTS apps (
+            id VARCHAR(36) PRIMARY KEY COMMENT '应用ID',
+            name VARCHAR(100) NOT NULL COMMENT '应用名称',
+            app_key VARCHAR(100) UNIQUE NOT NULL COMMENT '应用Key',
+            app_secret VARCHAR(255) NOT NULL COMMENT '应用Secret',
+            description TEXT COMMENT '应用描述',
+            icon_url VARCHAR(500) COMMENT '应用图标',
+            redirect_uris JSON NOT NULL COMMENT '回调URL列表',
+            scope JSON COMMENT '权限范围',
+            is_active BOOLEAN DEFAULT TRUE COMMENT '是否激活',
+            is_trusted BOOLEAN DEFAULT FALSE COMMENT '是否受信任应用',
+            access_token_expires INT DEFAULT 7200 COMMENT '访问令牌过期时间(秒)',
+            refresh_token_expires INT DEFAULT 2592000 COMMENT '刷新令牌过期时间(秒)',
+            created_by VARCHAR(36) COMMENT '创建者ID',
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+            is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
+            
+            INDEX idx_app_key (app_key),
+            INDEX idx_created_at (created_at)
+        ) COMMENT='应用表'
+    """)
+    
+    # 访问令牌表
+    cursor.execute("""
+        CREATE TABLE IF NOT EXISTS oauth_access_tokens (
+            id VARCHAR(36) PRIMARY KEY COMMENT '令牌ID',
+            user_id VARCHAR(36) NOT NULL COMMENT '用户ID',
+            app_id VARCHAR(36) COMMENT '应用ID',
+            token VARCHAR(512) UNIQUE NOT NULL COMMENT '访问令牌',
+            refresh_token VARCHAR(512) UNIQUE COMMENT '刷新令牌',
+            token_type VARCHAR(50) DEFAULT 'Bearer' COMMENT '令牌类型',
+            scope VARCHAR(500) COMMENT '权限范围',
+            expires_at TIMESTAMP NOT NULL COMMENT '过期时间',
+            revoked BOOLEAN DEFAULT FALSE COMMENT '是否撤销',
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+            last_used_at TIMESTAMP NULL COMMENT '最后使用时间',
+            is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
+            
+            INDEX idx_token (token),
+            INDEX idx_refresh_token (refresh_token),
+            INDEX idx_user_app (user_id, app_id),
+            INDEX idx_expires_at (expires_at)
+        ) COMMENT='访问令牌表'
+    """)
+    
+    # 授权码表
+    cursor.execute("""
+        CREATE TABLE IF NOT EXISTS oauth_authorization_codes (
+            id VARCHAR(36) PRIMARY KEY COMMENT '授权码ID',
+            user_id VARCHAR(36) NOT NULL COMMENT '用户ID',
+            app_id VARCHAR(36) NOT NULL COMMENT '应用ID',
+            code VARCHAR(100) UNIQUE NOT NULL COMMENT '授权码',
+            redirect_uri VARCHAR(500) NOT NULL COMMENT '回调URL',
+            scope VARCHAR(500) COMMENT '权限范围',
+            expires_at TIMESTAMP NOT NULL COMMENT '过期时间',
+            used BOOLEAN DEFAULT FALSE COMMENT '是否已使用',
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+            is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
+            
+            INDEX idx_code (code),
+            INDEX idx_user_app (user_id, app_id),
+            INDEX idx_expires_at (expires_at)
+        ) COMMENT='授权码表'
+    """)
+    
+    # 登录日志表
+    cursor.execute("""
+        CREATE TABLE IF NOT EXISTS login_logs (
+            id VARCHAR(36) PRIMARY KEY COMMENT '日志ID',
+            user_id VARCHAR(36) COMMENT '用户ID',
+            username VARCHAR(50) NOT NULL COMMENT '用户名',
+            login_type VARCHAR(20) COMMENT '登录方式',
+            ip_address VARCHAR(45) COMMENT 'IP地址',
+            user_agent TEXT COMMENT '用户代理',
+            success BOOLEAN DEFAULT FALSE COMMENT '是否成功',
+            failure_reason VARCHAR(200) COMMENT '失败原因',
+            login_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '登录时间',
+            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+            is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否删除',
+            
+            INDEX idx_user_id (user_id),
+            INDEX idx_username (username),
+            INDEX idx_login_at (login_at),
+            INDEX idx_ip_address (ip_address)
+        ) COMMENT='登录日志表'
+    """)
+    
+    connection.commit()
+    print("✅ 数据库表创建成功")
+
+def create_default_data(connection):
+    """创建默认数据"""
+    cursor = connection.cursor()
+    
+    # 检查是否已有管理员用户
+    cursor.execute("SELECT COUNT(*) FROM users WHERE username = 'admin'")
+    if cursor.fetchone()[0] > 0:
+        print("⚠️  管理员用户已存在,跳过创建")
+        
+        # 获取现有的测试应用信息
+        cursor.execute("SELECT app_key, app_secret FROM apps WHERE name = '测试应用' LIMIT 1")
+        result = cursor.fetchone()
+        if result:
+            return result[0], result[1]
+        else:
+            # 如果没有测试应用,创建一个
+            app_key = generate_random_string(32)
+            app_secret = generate_random_string(64)
+            return app_key, app_secret
+    
+    # 创建管理员用户
+    admin_id = str(uuid.uuid4())
+    admin_password = "Admin123456"
+    password_hash = hash_password_simple(admin_password)
+    
+    cursor.execute("""
+        INSERT INTO users (id, username, email, password_hash, is_active, is_superuser, created_at, updated_at, is_deleted)
+        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
+    """, (admin_id, "admin", "admin@example.com", password_hash, True, True, datetime.now(), datetime.now(), False))
+    
+    # 创建测试应用
+    app_id = str(uuid.uuid4())
+    app_key = generate_random_string(32)
+    app_secret = generate_random_string(64)
+    
+    cursor.execute("""
+        INSERT INTO apps (id, name, app_key, app_secret, description, redirect_uris, scope, is_active, is_trusted, 
+                         access_token_expires, refresh_token_expires, created_by, created_at, updated_at, is_deleted)
+        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
+    """, (
+        app_id, "测试应用", app_key, app_secret, "用于测试的默认应用",
+        '["http://localhost:3000/callback", "http://localhost:8080/callback", "http://localhost:8001/auth/callback"]',
+        '["profile", "email"]', True, True, 7200, 2592000, admin_id,
+        datetime.now(), datetime.now(), False
+    ))
+    
+    connection.commit()
+    
+    print("✅ 默认数据创建成功")
+    print(f"管理员账号: admin")
+    print(f"管理员密码: {admin_password}")
+    print(f"测试应用Key: {app_key}")
+    print(f"测试应用Secret: {app_secret}")
+    
+    return app_key, app_secret
+
+def main():
+    """主函数"""
+    print("=" * 50)
+    print("SSO数据库初始化")
+    print("=" * 50)
+    
+    try:
+        # 获取数据库配置
+        config = get_db_config()
+        print(f"连接数据库: {config['host']}:{config['port']}/{config['database']}")
+        
+        # 连接数据库
+        connection = pymysql.connect(**config)
+        print("✅ 数据库连接成功")
+        
+        # 创建表
+        create_tables(connection)
+        
+        # 创建默认数据
+        result = create_default_data(connection)
+        if result:
+            app_key, app_secret = result
+        else:
+            app_key, app_secret = "demo_key", "demo_secret"
+        
+        # 关闭连接
+        connection.close()
+        
+        print("\n" + "=" * 50)
+        print("🎉 数据库初始化完成!")
+        print("\n下一步:")
+        print("1. 启动后端服务: cd src && python -m app.main")
+        print("2. 启动前端服务: cd ../sso-frontend && npm run dev")
+        print(f"3. 配置子系统: CLIENT_ID={app_key}")
+        print(f"   CLIENT_SECRET={app_secret}")
+        
+    except Exception as e:
+        print(f"❌ 数据库初始化失败: {e}")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()

+ 4 - 0
src/app/__init__.py

@@ -0,0 +1,4 @@
+"""
+SSO认证中心应用包
+"""
+__version__ = "1.0.0"

BIN
src/app/__pycache__/__init__.cpython-312.pyc


BIN
src/app/__pycache__/main.cpython-312.pyc


+ 1 - 0
src/app/api/__init__.py

@@ -0,0 +1 @@
+"""API模块"""

BIN
src/app/api/__pycache__/__init__.cpython-312.pyc


+ 1 - 0
src/app/api/v1/__init__.py

@@ -0,0 +1 @@
+"""API v1版本"""

BIN
src/app/api/v1/__pycache__/__init__.cpython-312.pyc


BIN
src/app/api/v1/__pycache__/api_router.cpython-312.pyc


+ 12 - 0
src/app/api/v1/api_router.py

@@ -0,0 +1,12 @@
+"""
+API路由聚合模块
+"""
+from fastapi import APIRouter
+from .auth.router import router as auth_router
+from .oauth.router import router as oauth_router
+
+api_router = APIRouter()
+
+# 包含各个模块的路由
+api_router.include_router(auth_router, prefix="/auth")
+api_router.include_router(oauth_router, prefix="/oauth")

+ 1 - 0
src/app/api/v1/auth/__init__.py

@@ -0,0 +1 @@
+"""认证API模块"""

BIN
src/app/api/v1/auth/__pycache__/__init__.cpython-312.pyc


BIN
src/app/api/v1/auth/__pycache__/endpoints.cpython-312.pyc


BIN
src/app/api/v1/auth/__pycache__/router.cpython-312.pyc


+ 246 - 0
src/app/api/v1/auth/endpoints.py

@@ -0,0 +1,246 @@
+"""
+认证API端点
+"""
+from fastapi import APIRouter, Depends, HTTPException, Request, Response
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+from sqlalchemy.ext.asyncio import AsyncSession
+from typing import Optional
+from app.config.database import get_db
+from app.schemas.auth import (
+    LoginRequest, 
+    TokenResponse, 
+    RefreshTokenRequest,
+    LogoutRequest,
+    UserInfoResponse,
+    CaptchaResponse
+)
+from app.services.auth_service import AuthService
+from app.core.exceptions import AuthenticationError, ValidationError
+from app.schemas.base import ResponseSchema
+import base64
+import io
+from PIL import Image, ImageDraw, ImageFont
+import random
+import string
+
+router = APIRouter()
+security = HTTPBearer()
+
+
+def get_client_ip(request: Request) -> str:
+    """获取客户端IP地址"""
+    forwarded = request.headers.get("X-Forwarded-For")
+    if forwarded:
+        return forwarded.split(",")[0].strip()
+    return request.client.host
+
+
+def get_user_agent(request: Request) -> str:
+    """获取用户代理"""
+    return request.headers.get("User-Agent", "")
+
+
+@router.post("/login", response_model=ResponseSchema)
+async def login(
+    request: Request,
+    login_data: LoginRequest,
+    db: AsyncSession = Depends(get_db)
+):
+    """用户登录"""
+    try:
+        auth_service = AuthService(db)
+        
+        user, token_response = await auth_service.authenticate_user(
+            username=login_data.username,
+            password=login_data.password,
+            ip_address=get_client_ip(request),
+            user_agent=get_user_agent(request)
+        )
+        
+        return ResponseSchema(
+            code=0,
+            message="登录成功",
+            data=token_response.dict()
+        )
+        
+    except AuthenticationError as e:
+        return ResponseSchema(
+            code=e.code,
+            message=e.message,
+            data=None
+        )
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )
+
+
+@router.post("/refresh", response_model=ResponseSchema)
+async def refresh_token(
+    refresh_data: RefreshTokenRequest,
+    db: AsyncSession = Depends(get_db)
+):
+    """刷新访问令牌"""
+    try:
+        auth_service = AuthService(db)
+        
+        token_response = await auth_service.refresh_access_token(
+            refresh_token=refresh_data.refresh_token
+        )
+        
+        return ResponseSchema(
+            code=0,
+            message="令牌刷新成功",
+            data=token_response.dict()
+        )
+        
+    except AuthenticationError as e:
+        return ResponseSchema(
+            code=e.code,
+            message=e.message,
+            data=None
+        )
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )
+
+
+@router.post("/logout", response_model=ResponseSchema)
+async def logout(
+    logout_data: LogoutRequest,
+    credentials: HTTPAuthorizationCredentials = Depends(security),
+    db: AsyncSession = Depends(get_db)
+):
+    """用户登出"""
+    try:
+        auth_service = AuthService(db)
+        
+        token = credentials.credentials if credentials else logout_data.token
+        
+        await auth_service.logout(
+            token=token,
+            refresh_token=logout_data.refresh_token
+        )
+        
+        return ResponseSchema(
+            code=0,
+            message="登出成功",
+            data=None
+        )
+        
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )
+
+
+@router.get("/userinfo", response_model=ResponseSchema)
+async def get_user_info(
+    credentials: HTTPAuthorizationCredentials = Depends(security),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取用户信息"""
+    try:
+        auth_service = AuthService(db)
+        
+        user = await auth_service.get_current_user(credentials.credentials)
+        user_info = await auth_service.get_user_info(user)
+        
+        return ResponseSchema(
+            code=0,
+            message="获取用户信息成功",
+            data=user_info.dict()
+        )
+        
+    except AuthenticationError as e:
+        return ResponseSchema(
+            code=e.code,
+            message=e.message,
+            data=None
+        )
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )
+
+
+@router.get("/captcha", response_model=ResponseSchema)
+async def get_captcha():
+    """获取验证码"""
+    try:
+        # 生成随机验证码
+        captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
+        
+        # 创建图片
+        width, height = 120, 40
+        image = Image.new('RGB', (width, height), color='white')
+        draw = ImageDraw.Draw(image)
+        
+        # 绘制验证码文字
+        try:
+            # 尝试使用系统字体
+            font = ImageFont.truetype("arial.ttf", 20)
+        except:
+            # 使用默认字体
+            font = ImageFont.load_default()
+        
+        # 计算文字位置
+        text_width = draw.textlength(captcha_text, font=font)
+        text_height = 20
+        x = (width - text_width) // 2
+        y = (height - text_height) // 2
+        
+        # 绘制文字
+        draw.text((x, y), captcha_text, fill='black', font=font)
+        
+        # 添加干扰线
+        for _ in range(5):
+            x1 = random.randint(0, width)
+            y1 = random.randint(0, height)
+            x2 = random.randint(0, width)
+            y2 = random.randint(0, height)
+            draw.line([(x1, y1), (x2, y2)], fill='gray', width=1)
+        
+        # 转换为base64
+        buffer = io.BytesIO()
+        image.save(buffer, format='PNG')
+        image_base64 = base64.b64encode(buffer.getvalue()).decode()
+        
+        # 生成验证码ID(实际应用中应该存储到Redis等缓存中)
+        captcha_id = ''.join(random.choices(string.ascii_letters + string.digits, k=32))
+        
+        captcha_response = CaptchaResponse(
+            captcha_id=captcha_id,
+            captcha_image=f"data:image/png;base64,{image_base64}"
+        )
+        
+        return ResponseSchema(
+            code=0,
+            message="获取验证码成功",
+            data=captcha_response.dict()
+        )
+        
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="生成验证码失败",
+            data=None
+        )
+
+
+@router.get("/me", response_model=ResponseSchema)
+async def get_current_user_info(
+    credentials: HTTPAuthorizationCredentials = Depends(security),
+    db: AsyncSession = Depends(get_db)
+):
+    """获取当前用户信息"""
+    return await get_user_info(credentials, db)

+ 10 - 0
src/app/api/v1/auth/router.py

@@ -0,0 +1,10 @@
+"""
+认证路由模块
+"""
+from fastapi import APIRouter
+from .endpoints import router as auth_endpoints
+
+router = APIRouter()
+
+# 包含认证端点
+router.include_router(auth_endpoints, tags=["认证"])

+ 1 - 0
src/app/api/v1/oauth/__init__.py

@@ -0,0 +1 @@
+"""OAuth API模块"""

BIN
src/app/api/v1/oauth/__pycache__/__init__.cpython-312.pyc


BIN
src/app/api/v1/oauth/__pycache__/endpoints.cpython-312.pyc


BIN
src/app/api/v1/oauth/__pycache__/router.cpython-312.pyc


+ 543 - 0
src/app/api/v1/oauth/endpoints.py

@@ -0,0 +1,543 @@
+"""
+OAuth 2.0 API端点
+"""
+from fastapi import APIRouter, Depends, Request, Form, Query, HTTPException
+from fastapi.responses import RedirectResponse, HTMLResponse
+from fastapi.security import HTTPBasic, HTTPBasicCredentials
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, and_, or_
+from typing import Optional
+from datetime import datetime, timedelta
+from urllib.parse import urlencode, parse_qs, urlparse
+from app.config.database import get_db
+from app.models import App, User, OAuthAuthorizationCode, OAuthAccessToken
+from app.services.oauth_service import OAuthService
+from app.utils.security import (
+    generate_authorization_code,
+    create_access_token,
+    create_refresh_token,
+    decode_basic_auth,
+    verify_password
+)
+from app.core.exceptions import (
+    ValidationError,
+    AuthenticationError,
+    AppNotFoundError,
+    InvalidRedirectURIError
+)
+from app.schemas.base import ResponseSchema
+from app.config.simple_settings import settings
+
+router = APIRouter()
+security = HTTPBasic()
+
+
+@router.get("/authorize")
+async def authorize(
+    request: Request,
+    response_type: str = Query(..., description="响应类型"),
+    client_id: str = Query(..., description="客户端ID"),
+    redirect_uri: str = Query(..., description="重定向URI"),
+    scope: Optional[str] = Query(None, description="权限范围"),
+    state: Optional[str] = Query(None, description="状态参数"),
+    db: AsyncSession = Depends(get_db)
+):
+    """OAuth2授权端点"""
+    
+    try:
+        # 验证响应类型
+        if response_type != "code":
+            error_params = {
+                "error": "unsupported_response_type",
+                "error_description": "不支持的响应类型"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 查找应用
+        stmt = select(App).where(and_(App.app_key == client_id, App.is_deleted == False))
+        result = await db.execute(stmt)
+        app = result.scalar_one_or_none()
+        
+        if not app:
+            error_params = {
+                "error": "invalid_client",
+                "error_description": "无效的客户端"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 验证应用状态
+        if not app.is_active:
+            error_params = {
+                "error": "invalid_client",
+                "error_description": "客户端已被禁用"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 验证重定向URI
+        if redirect_uri not in app.redirect_uris:
+            raise HTTPException(
+                status_code=400,
+                detail="无效的重定向URI"
+            )
+        
+        # 检查用户是否已登录(这里简化处理,实际应该检查session)
+        # 如果用户未登录,显示登录页面
+        # 如果用户已登录,显示授权确认页面
+        
+        # 这里返回一个简单的授权页面HTML
+        html_content = f"""
+        <!DOCTYPE html>
+        <html>
+        <head>
+            <title>授权确认</title>
+            <meta charset="utf-8">
+            <style>
+                body {{ font-family: Arial, sans-serif; margin: 50px; }}
+                .container {{ max-width: 500px; margin: 0 auto; }}
+                .app-info {{ background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }}
+                .form-group {{ margin-bottom: 15px; }}
+                .btn {{ padding: 10px 20px; margin: 5px; border: none; border-radius: 3px; cursor: pointer; }}
+                .btn-primary {{ background: #007bff; color: white; }}
+                .btn-secondary {{ background: #6c757d; color: white; }}
+            </style>
+        </head>
+        <body>
+            <div class="container">
+                <h2>授权确认</h2>
+                <div class="app-info">
+                    <h3>{app.name}</h3>
+                    <p>{app.description or '该应用请求访问您的账户信息'}</p>
+                    <p><strong>权限范围:</strong> {scope or 'profile email'}</p>
+                </div>
+                
+                <form method="post" action="/oauth/authorize">
+                    <input type="hidden" name="response_type" value="{response_type}">
+                    <input type="hidden" name="client_id" value="{client_id}">
+                    <input type="hidden" name="redirect_uri" value="{redirect_uri}">
+                    <input type="hidden" name="scope" value="{scope or ''}">
+                    <input type="hidden" name="state" value="{state or ''}">
+                    
+                    <div class="form-group">
+                        <label>用户名或邮箱:</label>
+                        <input type="text" name="username" required style="width: 100%; padding: 8px;">
+                    </div>
+                    
+                    <div class="form-group">
+                        <label>密码:</label>
+                        <input type="password" name="password" required style="width: 100%; padding: 8px;">
+                    </div>
+                    
+                    <button type="submit" name="action" value="authorize" class="btn btn-primary">
+                        授权
+                    </button>
+                    <button type="submit" name="action" value="deny" class="btn btn-secondary">
+                        拒绝
+                    </button>
+                </form>
+            </div>
+        </body>
+        </html>
+        """
+        
+        return HTMLResponse(content=html_content)
+        
+    except Exception as e:
+        error_params = {
+            "error": "server_error",
+            "error_description": "服务器内部错误"
+        }
+        if state:
+            error_params["state"] = state
+        
+        return RedirectResponse(
+            url=f"{redirect_uri}?{urlencode(error_params)}",
+            status_code=302
+        )
+
+
+@router.post("/authorize")
+async def authorize_post(
+    request: Request,
+    response_type: str = Form(...),
+    client_id: str = Form(...),
+    redirect_uri: str = Form(...),
+    scope: Optional[str] = Form(None),
+    state: Optional[str] = Form(None),
+    username: str = Form(...),
+    password: str = Form(...),
+    action: str = Form(...),
+    db: AsyncSession = Depends(get_db)
+):
+    """处理授权确认"""
+    
+    try:
+        # 如果用户拒绝授权
+        if action == "deny":
+            error_params = {
+                "error": "access_denied",
+                "error_description": "用户拒绝授权"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 验证用户凭据
+        from sqlalchemy import or_
+        stmt = select(User).where(
+            and_(
+                or_(User.username == username, User.email == username),
+                User.is_deleted == False,
+                User.is_active == True
+            )
+        )
+        result = await db.execute(stmt)
+        user = result.scalar_one_or_none()
+        
+        if not user or not verify_password(password, user.password_hash):
+            error_params = {
+                "error": "access_denied",
+                "error_description": "用户名或密码错误"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 查找应用
+        stmt = select(App).where(and_(App.app_key == client_id, App.is_deleted == False))
+        result = await db.execute(stmt)
+        app = result.scalar_one_or_none()
+        
+        if not app or not app.is_active:
+            error_params = {
+                "error": "invalid_client",
+                "error_description": "无效的客户端"
+            }
+            if state:
+                error_params["state"] = state
+            
+            return RedirectResponse(
+                url=f"{redirect_uri}?{urlencode(error_params)}",
+                status_code=302
+            )
+        
+        # 生成授权码
+        authorization_code = generate_authorization_code()
+        
+        # 保存授权码
+        oauth_code = OAuthAuthorizationCode(
+            user_id=user.id,
+            app_id=app.id,
+            code=authorization_code,
+            redirect_uri=redirect_uri,
+            scope=scope,
+            expires_at=datetime.utcnow() + timedelta(
+                minutes=settings.OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES
+            )
+        )
+        db.add(oauth_code)
+        await db.commit()
+        
+        # 重定向回应用
+        success_params = {
+            "code": authorization_code
+        }
+        if state:
+            success_params["state"] = state
+        
+        return RedirectResponse(
+            url=f"{redirect_uri}?{urlencode(success_params)}",
+            status_code=302
+        )
+        
+    except Exception as e:
+        error_params = {
+            "error": "server_error",
+            "error_description": "服务器内部错误"
+        }
+        if state:
+            error_params["state"] = state
+        
+        return RedirectResponse(
+            url=f"{redirect_uri}?{urlencode(error_params)}",
+            status_code=302
+        )
+
+
+@router.post("/token", response_model=ResponseSchema)
+async def token(
+    request: Request,
+    grant_type: str = Form(..., description="授权类型"),
+    code: Optional[str] = Form(None, description="授权码"),
+    redirect_uri: Optional[str] = Form(None, description="重定向URI"),
+    refresh_token: Optional[str] = Form(None, description="刷新令牌"),
+    client_id: Optional[str] = Form(None, description="客户端ID"),
+    db: AsyncSession = Depends(get_db)
+):
+    """OAuth2令牌端点"""
+    
+    try:
+        # 获取客户端认证信息
+        auth_header = request.headers.get("Authorization")
+        if auth_header:
+            client_credentials = decode_basic_auth(auth_header)
+            if client_credentials:
+                client_id, client_secret = client_credentials
+            else:
+                return ResponseSchema(
+                    code=300002,
+                    message="无效的客户端认证",
+                    data=None
+                )
+        else:
+            return ResponseSchema(
+                code=300002,
+                message="缺少客户端认证",
+                data=None
+            )
+        
+        # 查找应用
+        stmt = select(App).where(and_(App.app_key == client_id, App.is_deleted == False))
+        result = await db.execute(stmt)
+        app = result.scalar_one_or_none()
+        
+        if not app or not app.is_active:
+            return ResponseSchema(
+                code=300001,
+                message="无效的客户端",
+                data=None
+            )
+        
+        # 验证客户端密钥(这里简化处理,实际应该使用哈希比较)
+        if client_secret != app.app_secret:
+            return ResponseSchema(
+                code=300002,
+                message="客户端密钥错误",
+                data=None
+            )
+        
+        if grant_type == "authorization_code":
+            # 授权码模式
+            if not code or not redirect_uri:
+                return ResponseSchema(
+                    code=100001,
+                    message="缺少必要参数",
+                    data=None
+                )
+            
+            # 查找授权码
+            stmt = select(OAuthAuthorizationCode).where(
+                and_(
+                    OAuthAuthorizationCode.code == code,
+                    OAuthAuthorizationCode.app_id == app.id,
+                    OAuthAuthorizationCode.used == False,
+                    OAuthAuthorizationCode.expires_at > datetime.utcnow()
+                )
+            )
+            result = await db.execute(stmt)
+            oauth_code = result.scalar_one_or_none()
+            
+            if not oauth_code:
+                return ResponseSchema(
+                    code=100001,
+                    message="无效的授权码",
+                    data=None
+                )
+            
+            # 验证重定向URI
+            if oauth_code.redirect_uri != redirect_uri:
+                return ResponseSchema(
+                    code=300003,
+                    message="重定向URI不匹配",
+                    data=None
+                )
+            
+            # 标记授权码为已使用
+            oauth_code.used = True
+            
+            # 获取用户信息
+            stmt = select(User).where(User.id == oauth_code.user_id)
+            result = await db.execute(stmt)
+            user = result.scalar_one_or_none()
+            
+            if not user:
+                return ResponseSchema(
+                    code=200001,
+                    message="用户不存在",
+                    data=None
+                )
+            
+            # 生成访问令牌
+            token_data = {
+                "sub": user.id,
+                "username": user.username,
+                "email": user.email,
+                "client_id": client_id,
+                "scope": oauth_code.scope
+            }
+            
+            access_token = create_access_token(
+                token_data,
+                timedelta(seconds=app.access_token_expires)
+            )
+            refresh_token_str = create_refresh_token(
+                {"sub": user.id, "client_id": client_id},
+                timedelta(seconds=app.refresh_token_expires)
+            )
+            
+            # 保存令牌
+            oauth_token = OAuthAccessToken(
+                user_id=user.id,
+                app_id=app.id,
+                token=access_token,
+                refresh_token=refresh_token_str,
+                token_type="Bearer",
+                scope=oauth_code.scope,
+                expires_at=datetime.utcnow() + timedelta(seconds=app.access_token_expires)
+            )
+            db.add(oauth_token)
+            await db.commit()
+            
+            return ResponseSchema(
+                code=0,
+                message="获取令牌成功",
+                data={
+                    "access_token": access_token,
+                    "refresh_token": refresh_token_str,
+                    "token_type": "Bearer",
+                    "expires_in": app.access_token_expires,
+                    "scope": oauth_code.scope
+                }
+            )
+        
+        elif grant_type == "refresh_token":
+            # 刷新令牌模式
+            if not refresh_token:
+                return ResponseSchema(
+                    code=100001,
+                    message="缺少刷新令牌",
+                    data=None
+                )
+            
+            # TODO: 实现刷新令牌逻辑
+            return ResponseSchema(
+                code=100001,
+                message="暂不支持刷新令牌",
+                data=None
+            )
+        
+        else:
+            return ResponseSchema(
+                code=100001,
+                message="不支持的授权类型",
+                data=None
+            )
+            
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )
+
+
+@router.get("/userinfo", response_model=ResponseSchema)
+async def userinfo(
+    request: Request,
+    db: AsyncSession = Depends(get_db)
+):
+    """获取用户信息端点"""
+    
+    try:
+        # 获取访问令牌
+        auth_header = request.headers.get("Authorization")
+        if not auth_header or not auth_header.startswith("Bearer "):
+            return ResponseSchema(
+                code=200002,
+                message="缺少访问令牌",
+                data=None
+            )
+        
+        access_token = auth_header[7:]  # 移除 "Bearer " 前缀
+        
+        # 查找令牌
+        stmt = select(OAuthAccessToken).where(
+            and_(
+                OAuthAccessToken.token == access_token,
+                OAuthAccessToken.revoked == False,
+                OAuthAccessToken.expires_at > datetime.utcnow()
+            )
+        )
+        result = await db.execute(stmt)
+        oauth_token = result.scalar_one_or_none()
+        
+        if not oauth_token:
+            return ResponseSchema(
+                code=200003,
+                message="无效的访问令牌",
+                data=None
+            )
+        
+        # 获取用户信息
+        stmt = select(User).where(User.id == oauth_token.user_id)
+        result = await db.execute(stmt)
+        user = result.scalar_one_or_none()
+        
+        if not user:
+            return ResponseSchema(
+                code=200001,
+                message="用户不存在",
+                data=None
+            )
+        
+        # 更新令牌最后使用时间
+        oauth_token.last_used_at = datetime.utcnow()
+        await db.commit()
+        
+        # 返回用户信息
+        user_info = {
+            "sub": user.id,
+            "username": user.username,
+            "email": user.email,
+            "phone": user.phone,
+            "avatar_url": user.avatar_url,
+            "is_active": user.is_active
+        }
+        
+        return ResponseSchema(
+            code=0,
+            message="获取用户信息成功",
+            data=user_info
+        )
+        
+    except Exception as e:
+        return ResponseSchema(
+            code=500001,
+            message="服务器内部错误",
+            data=None
+        )

+ 10 - 0
src/app/api/v1/oauth/router.py

@@ -0,0 +1,10 @@
+"""
+OAuth路由模块
+"""
+from fastapi import APIRouter
+from .endpoints import router as oauth_endpoints
+
+router = APIRouter()
+
+# 包含OAuth端点
+router.include_router(oauth_endpoints, tags=["OAuth2.0"])

+ 1 - 0
src/app/config/__init__.py

@@ -0,0 +1 @@
+"""配置模块"""

BIN
src/app/config/__pycache__/__init__.cpython-312.pyc


BIN
src/app/config/__pycache__/database.cpython-312.pyc


BIN
src/app/config/__pycache__/settings.cpython-312.pyc


BIN
src/app/config/__pycache__/simple_settings.cpython-312.pyc


+ 56 - 0
src/app/config/database.py

@@ -0,0 +1,56 @@
+"""
+数据库配置模块
+"""
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
+from sqlalchemy.orm import DeclarativeBase
+from typing import AsyncGenerator
+from .simple_settings import settings
+
+
+# 创建异步数据库引擎
+engine = create_async_engine(
+    settings.DATABASE_URL,
+    echo=settings.DATABASE_ECHO,
+    pool_pre_ping=True,
+    pool_recycle=300,
+    pool_size=10,
+    max_overflow=20,
+)
+
+# 创建异步会话工厂
+AsyncSessionLocal = async_sessionmaker(
+    engine,
+    class_=AsyncSession,
+    expire_on_commit=False,
+    autocommit=False,
+    autoflush=False,
+)
+
+
+class Base(DeclarativeBase):
+    """数据库模型基类"""
+    pass
+
+
+async def get_db() -> AsyncGenerator[AsyncSession, None]:
+    """获取数据库会话"""
+    async with AsyncSessionLocal() as session:
+        try:
+            yield session
+        except Exception:
+            await session.rollback()
+            raise
+        finally:
+            await session.close()
+
+
+async def init_db():
+    """初始化数据库"""
+    async with engine.begin() as conn:
+        # 创建所有表
+        await conn.run_sync(Base.metadata.create_all)
+
+
+async def close_db():
+    """关闭数据库连接"""
+    await engine.dispose()

+ 157 - 0
src/app/config/settings.py

@@ -0,0 +1,157 @@
+"""
+应用配置模块
+"""
+from typing import List, Optional
+from pydantic import field_validator
+from pydantic_settings import BaseSettings
+from functools import lru_cache
+import os
+
+
+class Settings(BaseSettings):
+    """应用配置类"""
+    
+    # 应用基础配置
+    APP_NAME: str = "SSO认证中心"
+    APP_VERSION: str = "1.0.0"
+    DEBUG: bool = False
+    SECRET_KEY: str
+    ALGORITHM: str = "HS256"
+    
+    # 服务器配置
+    HOST: str = "0.0.0.0"
+    PORT: int = 8000
+    RELOAD: bool = False
+    
+    # 数据库配置
+    DATABASE_URL: str
+    DATABASE_ECHO: bool = False
+    
+    # Redis配置
+    REDIS_URL: str = "redis://localhost:6379/0"
+    REDIS_PASSWORD: Optional[str] = None
+    
+    # JWT配置
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
+    REFRESH_TOKEN_EXPIRE_DAYS: int = 30
+    JWT_SECRET_KEY: str
+    
+    # OAuth2配置
+    OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES: int = 10
+    OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES: int = 120
+    OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS: int = 30
+    
+    # 邮件配置
+    SMTP_HOST: Optional[str] = None
+    SMTP_PORT: int = 587
+    SMTP_USER: Optional[str] = None
+    SMTP_PASSWORD: Optional[str] = None
+    SMTP_TLS: bool = True
+    SMTP_SSL: bool = False
+    
+    # 文件上传配置
+    UPLOAD_DIR: str = "./uploads"
+    MAX_FILE_SIZE: int = 5242880  # 5MB
+    ALLOWED_EXTENSIONS: str = "jpg,jpeg,png,gif"
+    
+    # 日志配置
+    LOG_LEVEL: str = "INFO"
+    LOG_FILE: str = "./logs/app.log"
+    
+    # CORS配置
+    CORS_ORIGINS: str = "http://localhost:3000,http://localhost:8080,http://localhost:3001"
+    CORS_CREDENTIALS: bool = True
+    CORS_METHODS: str = "*"
+    CORS_HEADERS: str = "*"
+    
+    # 安全配置
+    BCRYPT_ROUNDS: int = 12
+    PASSWORD_MIN_LENGTH: int = 8
+    MAX_LOGIN_ATTEMPTS: int = 5
+    LOCKOUT_DURATION_MINUTES: int = 30
+    
+    # 缓存配置
+    CACHE_TTL: int = 3600
+    SESSION_TTL: int = 86400
+    
+    # Celery配置
+    CELERY_BROKER_URL: str = "redis://localhost:6379/1"
+    CELERY_RESULT_BACKEND: str = "redis://localhost:6379/2"
+    
+    @field_validator("ALLOWED_EXTENSIONS", mode="before")
+    @classmethod
+    def parse_allowed_extensions(cls, v):
+        if isinstance(v, str):
+            return [ext.strip() for ext in v.split(",")]
+        return v
+    
+    @field_validator("CORS_ORIGINS", mode="before")
+    @classmethod
+    def parse_cors_origins(cls, v):
+        if isinstance(v, str):
+            return [origin.strip() for origin in v.split(",")]
+        return v
+    
+    @field_validator("CORS_METHODS", mode="before")
+    @classmethod
+    def parse_cors_methods(cls, v):
+        if isinstance(v, str):
+            if v == "*":
+                return ["*"]
+            return [method.strip() for method in v.split(",")]
+        return v
+    
+    @field_validator("CORS_HEADERS", mode="before")
+    @classmethod
+    def parse_cors_headers(cls, v):
+        if isinstance(v, str):
+            if v == "*":
+                return ["*"]
+            return [header.strip() for header in v.split(",")]
+        return v
+    
+    @property
+    def allowed_extensions_list(self) -> List[str]:
+        """获取允许的文件扩展名列表"""
+        if isinstance(self.ALLOWED_EXTENSIONS, str):
+            return [ext.strip() for ext in self.ALLOWED_EXTENSIONS.split(",")]
+        return self.ALLOWED_EXTENSIONS
+    
+    @property
+    def cors_origins_list(self) -> List[str]:
+        """获取CORS允许的源列表"""
+        if isinstance(self.CORS_ORIGINS, str):
+            return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
+        return self.CORS_ORIGINS
+    
+    @property
+    def cors_methods_list(self) -> List[str]:
+        """获取CORS允许的方法列表"""
+        if isinstance(self.CORS_METHODS, str):
+            if self.CORS_METHODS == "*":
+                return ["*"]
+            return [method.strip() for method in self.CORS_METHODS.split(",")]
+        return self.CORS_METHODS
+    
+    @property
+    def cors_headers_list(self) -> List[str]:
+        """获取CORS允许的头部列表"""
+        if isinstance(self.CORS_HEADERS, str):
+            if self.CORS_HEADERS == "*":
+                return ["*"]
+            return [header.strip() for header in self.CORS_HEADERS.split(",")]
+        return self.CORS_HEADERS
+    
+    class Config:
+        env_file = ".env"
+        case_sensitive = True
+
+
+@lru_cache()
+def get_settings() -> Settings:
+    """获取配置实例"""
+    return Settings()
+
+
+# 全局配置实例
+settings = get_settings()

+ 114 - 0
src/app/config/simple_settings.py

@@ -0,0 +1,114 @@
+"""
+简化的应用配置模块
+"""
+import os
+from typing import List
+from functools import lru_cache
+
+
+class SimpleSettings:
+    """简化的配置类"""
+    
+    def __init__(self):
+        # 从环境变量加载配置
+        self.APP_NAME = os.getenv("APP_NAME", "SSO认证中心")
+        self.APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
+        self.DEBUG = os.getenv("DEBUG", "True").lower() == "true"
+        self.SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key")
+        self.ALGORITHM = os.getenv("ALGORITHM", "HS256")
+        
+        # 服务器配置
+        self.HOST = os.getenv("HOST", "0.0.0.0")
+        self.PORT = int(os.getenv("PORT", "8000"))
+        self.RELOAD = os.getenv("RELOAD", "True").lower() == "true"
+        
+        # 数据库配置
+        self.DATABASE_URL = os.getenv("DATABASE_URL", "")
+        self.DATABASE_ECHO = os.getenv("DATABASE_ECHO", "False").lower() == "true"
+        
+        # Redis配置
+        self.REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
+        self.REDIS_PASSWORD = os.getenv("REDIS_PASSWORD")
+        
+        # JWT配置
+        self.ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
+        self.REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "30"))
+        self.JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key")
+        
+        # OAuth2配置
+        self.OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES = int(os.getenv("OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES", "10"))
+        self.OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES", "120"))
+        self.OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS", "30"))
+        
+        # 邮件配置
+        self.SMTP_HOST = os.getenv("SMTP_HOST")
+        self.SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
+        self.SMTP_USER = os.getenv("SMTP_USER")
+        self.SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
+        self.SMTP_TLS = os.getenv("SMTP_TLS", "True").lower() == "true"
+        self.SMTP_SSL = os.getenv("SMTP_SSL", "False").lower() == "true"
+        
+        # 文件上传配置
+        self.UPLOAD_DIR = os.getenv("UPLOAD_DIR", "./uploads")
+        self.MAX_FILE_SIZE = int(os.getenv("MAX_FILE_SIZE", "5242880"))
+        
+        # 日志配置
+        self.LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
+        self.LOG_FILE = os.getenv("LOG_FILE", "./logs/app.log")
+        
+        # 安全配置
+        self.BCRYPT_ROUNDS = int(os.getenv("BCRYPT_ROUNDS", "12"))
+        self.PASSWORD_MIN_LENGTH = int(os.getenv("PASSWORD_MIN_LENGTH", "8"))
+        self.MAX_LOGIN_ATTEMPTS = int(os.getenv("MAX_LOGIN_ATTEMPTS", "5"))
+        self.LOCKOUT_DURATION_MINUTES = int(os.getenv("LOCKOUT_DURATION_MINUTES", "30"))
+        
+        # 缓存配置
+        self.CACHE_TTL = int(os.getenv("CACHE_TTL", "3600"))
+        self.SESSION_TTL = int(os.getenv("SESSION_TTL", "86400"))
+        
+        # Celery配置
+        self.CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "redis://localhost:6379/1")
+        self.CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", "redis://localhost:6379/2")
+    
+    @property
+    def ALLOWED_EXTENSIONS(self) -> List[str]:
+        """获取允许的文件扩展名列表"""
+        extensions = os.getenv("ALLOWED_EXTENSIONS", "jpg,jpeg,png,gif")
+        return [ext.strip() for ext in extensions.split(",")]
+    
+    @property
+    def CORS_ORIGINS(self) -> List[str]:
+        """获取CORS允许的源列表"""
+        origins = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:8080,http://localhost:3001")
+        return [origin.strip() for origin in origins.split(",")]
+    
+    @property
+    def CORS_METHODS(self) -> List[str]:
+        """获取CORS允许的方法列表"""
+        methods = os.getenv("CORS_METHODS", "*")
+        if methods == "*":
+            return ["*"]
+        return [method.strip() for method in methods.split(",")]
+    
+    @property
+    def CORS_HEADERS(self) -> List[str]:
+        """获取CORS允许的头部列表"""
+        headers = os.getenv("CORS_HEADERS", "*")
+        if headers == "*":
+            return ["*"]
+        return [header.strip() for header in headers.split(",")]
+    
+    @property
+    def CORS_CREDENTIALS(self) -> bool:
+        """获取CORS凭据设置"""
+        return os.getenv("CORS_CREDENTIALS", "True").lower() == "true"
+
+
+@lru_cache()
+def get_settings() -> SimpleSettings:
+    """获取配置实例"""
+    return SimpleSettings()
+
+
+# 全局配置实例
+settings = get_settings()

+ 1 - 0
src/app/core/__init__.py

@@ -0,0 +1 @@
+"""核心模块"""

BIN
src/app/core/__pycache__/__init__.cpython-312.pyc


BIN
src/app/core/__pycache__/exceptions.cpython-312.pyc


+ 252 - 0
src/app/core/exceptions.py

@@ -0,0 +1,252 @@
+"""
+自定义异常模块
+"""
+from typing import Any, Dict, Optional
+
+
+class BaseAPIException(Exception):
+    """API异常基类"""
+    
+    def __init__(
+        self,
+        message: str = "服务器内部错误",
+        code: int = 500001,
+        status_code: int = 500,
+        details: Optional[Dict[str, Any]] = None
+    ):
+        self.message = message
+        self.code = code
+        self.status_code = status_code
+        self.details = details or {}
+        super().__init__(self.message)
+
+
+class ValidationError(BaseAPIException):
+    """验证错误"""
+    
+    def __init__(
+        self,
+        message: str = "参数验证失败",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=100001,
+            status_code=400,
+            details=details
+        )
+
+
+class AuthenticationError(BaseAPIException):
+    """认证错误"""
+    
+    def __init__(
+        self,
+        message: str = "认证失败",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=200001,
+            status_code=401,
+            details=details
+        )
+
+
+class AuthorizationError(BaseAPIException):
+    """授权错误"""
+    
+    def __init__(
+        self,
+        message: str = "权限不足",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=200003,
+            status_code=403,
+            details=details
+        )
+
+
+class NotFoundError(BaseAPIException):
+    """资源不存在错误"""
+    
+    def __init__(
+        self,
+        message: str = "资源不存在",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=100004,
+            status_code=404,
+            details=details
+        )
+
+
+class ConflictError(BaseAPIException):
+    """冲突错误"""
+    
+    def __init__(
+        self,
+        message: str = "资源冲突",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=400001,
+            status_code=409,
+            details=details
+        )
+
+
+class RateLimitError(BaseAPIException):
+    """频率限制错误"""
+    
+    def __init__(
+        self,
+        message: str = "请求过于频繁",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=100006,
+            status_code=429,
+            details=details
+        )
+
+
+class TokenExpiredError(AuthenticationError):
+    """令牌过期错误"""
+    
+    def __init__(
+        self,
+        message: str = "令牌已过期",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+        self.code = 200003
+
+
+class TokenInvalidError(AuthenticationError):
+    """令牌无效错误"""
+    
+    def __init__(
+        self,
+        message: str = "令牌无效",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+        self.code = 200002
+
+
+class UserNotFoundError(NotFoundError):
+    """用户不存在错误"""
+    
+    def __init__(
+        self,
+        message: str = "用户不存在",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+
+
+class UserInactiveError(AuthenticationError):
+    """用户未激活错误"""
+    
+    def __init__(
+        self,
+        message: str = "用户未激活",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+
+
+class AppNotFoundError(NotFoundError):
+    """应用不存在错误"""
+    
+    def __init__(
+        self,
+        message: str = "应用不存在",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+        self.code = 300001
+
+
+class AppInactiveError(AuthenticationError):
+    """应用未激活错误"""
+    
+    def __init__(
+        self,
+        message: str = "应用未激活",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+
+
+class InvalidRedirectURIError(ValidationError):
+    """无效回调URL错误"""
+    
+    def __init__(
+        self,
+        message: str = "回调URL不匹配",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+        self.code = 300003
+
+
+class InvalidGrantError(ValidationError):
+    """无效授权错误"""
+    
+    def __init__(
+        self,
+        message: str = "无效的授权类型",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+
+
+class InvalidScopeError(ValidationError):
+    """无效权限范围错误"""
+    
+    def __init__(
+        self,
+        message: str = "无效的权限范围",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(message=message, details=details)
+
+
+class DatabaseError(BaseAPIException):
+    """数据库错误"""
+    
+    def __init__(
+        self,
+        message: str = "数据库操作失败",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=500003,
+            status_code=500,
+            details=details
+        )
+
+
+class ExternalServiceError(BaseAPIException):
+    """外部服务错误"""
+    
+    def __init__(
+        self,
+        message: str = "外部服务调用失败",
+        details: Optional[Dict[str, Any]] = None
+    ):
+        super().__init__(
+            message=message,
+            code=500004,
+            status_code=502,
+            details=details
+        )

+ 170 - 0
src/app/main.py

@@ -0,0 +1,170 @@
+"""
+FastAPI应用主入口
+"""
+from fastapi import FastAPI, Request, status
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from fastapi.exceptions import RequestValidationError
+from contextlib import asynccontextmanager
+
+# 首先加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from app.config.simple_settings import settings
+from app.config.database import init_db, close_db
+from app.api.v1.api_router import api_router
+from app.core.exceptions import BaseAPIException
+from app.schemas.base import ResponseSchema
+import logging
+from datetime import datetime
+
+
+# 配置日志
+logging.basicConfig(
+    level=getattr(logging, settings.LOG_LEVEL),
+    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    """应用生命周期管理"""
+    # 启动时执行
+    logger.info("正在启动应用...")
+    try:
+        await init_db()
+        logger.info("数据库初始化成功")
+    except Exception as e:
+        logger.error(f"数据库初始化失败: {e}")
+    
+    yield
+    
+    # 关闭时执行
+    logger.info("正在关闭应用...")
+    try:
+        await close_db()
+        logger.info("数据库连接已关闭")
+    except Exception as e:
+        logger.error(f"关闭数据库连接失败: {e}")
+
+
+# 创建FastAPI应用实例
+app = FastAPI(
+    title=settings.APP_NAME,
+    version=settings.APP_VERSION,
+    description="OAuth2单点登录认证中心",
+    docs_url="/docs" if settings.DEBUG else None,
+    redoc_url="/redoc" if settings.DEBUG else None,
+    lifespan=lifespan
+)
+
+
+# 配置CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=settings.CORS_ORIGINS,
+    allow_credentials=settings.CORS_CREDENTIALS,
+    allow_methods=settings.CORS_METHODS,
+    allow_headers=settings.CORS_HEADERS,
+)
+
+
+# 全局异常处理
+@app.exception_handler(BaseAPIException)
+async def api_exception_handler(request: Request, exc: BaseAPIException):
+    """处理自定义API异常"""
+    return JSONResponse(
+        status_code=exc.status_code,
+        content=ResponseSchema(
+            code=exc.code,
+            message=exc.message,
+            data=exc.details,
+            timestamp=datetime.utcnow()
+        ).dict()
+    )
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(request: Request, exc: RequestValidationError):
+    """处理请求验证异常"""
+    errors = []
+    for error in exc.errors():
+        errors.append({
+            "field": ".".join(str(loc) for loc in error["loc"]),
+            "message": error["msg"],
+            "type": error["type"]
+        })
+    
+    return JSONResponse(
+        status_code=status.HTTP_400_BAD_REQUEST,
+        content=ResponseSchema(
+            code=100001,
+            message="参数验证失败",
+            data={"errors": errors},
+            timestamp=datetime.utcnow()
+        ).dict()
+    )
+
+
+@app.exception_handler(Exception)
+async def general_exception_handler(request: Request, exc: Exception):
+    """处理通用异常"""
+    logger.error(f"未处理的异常: {exc}", exc_info=True)
+    
+    return JSONResponse(
+        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+        content=ResponseSchema(
+            code=500001,
+            message="服务器内部错误" if not settings.DEBUG else str(exc),
+            data=None,
+            timestamp=datetime.utcnow()
+        ).dict()
+    )
+
+
+# 健康检查端点
+@app.get("/health", tags=["系统"])
+async def health_check():
+    """健康检查"""
+    return ResponseSchema(
+        code=0,
+        message="服务正常运行",
+        data={
+            "status": "healthy",
+            "version": settings.APP_VERSION,
+            "timestamp": datetime.utcnow()
+        }
+    )
+
+
+# 根路径
+@app.get("/", tags=["系统"])
+async def root():
+    """根路径"""
+    return ResponseSchema(
+        code=0,
+        message="欢迎使用SSO认证中心",
+        data={
+            "name": settings.APP_NAME,
+            "version": settings.APP_VERSION,
+            "docs": "/docs" if settings.DEBUG else None
+        }
+    )
+
+
+# 包含API路由
+app.include_router(api_router, prefix="/api/v1")
+
+
+if __name__ == "__main__":
+    import uvicorn
+    
+    uvicorn.run(
+        "main:app",
+        host=settings.HOST,
+        port=settings.PORT,
+        reload=settings.RELOAD,
+        log_level=settings.LOG_LEVEL.lower()
+    )

+ 26 - 0
src/app/models/__init__.py

@@ -0,0 +1,26 @@
+"""
+数据模型模块
+"""
+from .base import BaseModel
+from .user import User, UserProfile, Role, UserRole, Permission, RolePermission
+from .app import App, AppPermission
+from .token import OAuthAccessToken, OAuthAuthorizationCode, TokenBlacklist
+from .log import LoginLog, OperationLog, SyncLog
+
+__all__ = [
+    "BaseModel",
+    "User",
+    "UserProfile", 
+    "Role",
+    "UserRole",
+    "Permission",
+    "RolePermission",
+    "App",
+    "AppPermission",
+    "OAuthAccessToken",
+    "OAuthAuthorizationCode", 
+    "TokenBlacklist",
+    "LoginLog",
+    "OperationLog",
+    "SyncLog",
+]

BIN
src/app/models/__pycache__/__init__.cpython-312.pyc


BIN
src/app/models/__pycache__/app.cpython-312.pyc


BIN
src/app/models/__pycache__/base.cpython-312.pyc


BIN
src/app/models/__pycache__/log.cpython-312.pyc


BIN
src/app/models/__pycache__/token.cpython-312.pyc


BIN
src/app/models/__pycache__/user.cpython-312.pyc


+ 45 - 0
src/app/models/app.py

@@ -0,0 +1,45 @@
+"""
+应用相关数据模型
+"""
+from sqlalchemy import Column, String, Boolean, Integer, Text, JSON, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.dialects.mysql import CHAR
+from .base import BaseModel
+
+
+class App(BaseModel):
+    """应用表"""
+    __tablename__ = "apps"
+    
+    name = Column(String(100), nullable=False, comment="应用名称")
+    app_key = Column(String(100), unique=True, nullable=False, comment="应用Key")
+    app_secret = Column(String(255), nullable=False, comment="应用Secret")
+    description = Column(Text, nullable=True, comment="应用描述")
+    icon_url = Column(String(500), nullable=True, comment="应用图标")
+    redirect_uris = Column(JSON, nullable=False, comment="回调URL列表")
+    scope = Column(JSON, nullable=True, comment="权限范围")
+    is_active = Column(Boolean, default=True, comment="是否激活")
+    is_trusted = Column(Boolean, default=False, comment="是否受信任应用")
+    access_token_expires = Column(Integer, default=7200, comment="访问令牌过期时间(秒)")
+    refresh_token_expires = Column(Integer, default=2592000, comment="刷新令牌过期时间(秒)")
+    created_by = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="创建者ID")
+    
+    # 关联关系
+    creator = relationship("User", foreign_keys=[created_by])
+    permissions = relationship("AppPermission", back_populates="app")
+    tokens = relationship("OAuthAccessToken", back_populates="app")
+    authorization_codes = relationship("OAuthAuthorizationCode", back_populates="app")
+    sync_logs = relationship("SyncLog", back_populates="app")
+
+
+class AppPermission(BaseModel):
+    """应用权限表"""
+    __tablename__ = "app_permissions"
+    
+    app_id = Column(CHAR(36), ForeignKey("apps.id", ondelete="CASCADE"), nullable=False, comment="应用ID")
+    permission_code = Column(String(100), nullable=False, comment="权限代码")
+    permission_name = Column(String(100), nullable=False, comment="权限名称")
+    description = Column(String(500), nullable=True, comment="权限描述")
+    
+    # 关联关系
+    app = relationship("App", back_populates="permissions")

+ 31 - 0
src/app/models/base.py

@@ -0,0 +1,31 @@
+"""
+数据库模型基类
+"""
+from sqlalchemy import Column, String, DateTime, Boolean, func
+from sqlalchemy.dialects.mysql import CHAR
+from app.config.database import Base
+from datetime import datetime
+import uuid
+
+
+class BaseModel(Base):
+    """数据库模型基类"""
+    __abstract__ = True
+    
+    id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="主键ID")
+    created_at = Column(DateTime, default=func.now(), comment="创建时间")
+    updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")
+    is_deleted = Column(Boolean, default=False, comment="是否删除")
+    
+    def to_dict(self) -> dict:
+        """转换为字典"""
+        return {
+            column.name: getattr(self, column.name)
+            for column in self.__table__.columns
+        }
+    
+    def update_from_dict(self, data: dict):
+        """从字典更新属性"""
+        for key, value in data.items():
+            if hasattr(self, key):
+                setattr(self, key, value)

+ 61 - 0
src/app/models/log.py

@@ -0,0 +1,61 @@
+"""
+日志相关数据模型
+"""
+from sqlalchemy import Column, String, Boolean, Integer, DateTime, Text, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.dialects.mysql import CHAR
+from .base import BaseModel
+from datetime import datetime
+
+
+class LoginLog(BaseModel):
+    """登录日志表"""
+    __tablename__ = "login_logs"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="用户ID")
+    username = Column(String(50), nullable=False, comment="用户名")
+    login_type = Column(String(20), nullable=True, comment="登录方式")
+    ip_address = Column(String(45), nullable=True, comment="IP地址")
+    user_agent = Column(Text, nullable=True, comment="用户代理")
+    success = Column(Boolean, default=False, comment="是否成功")
+    failure_reason = Column(String(200), nullable=True, comment="失败原因")
+    login_at = Column(DateTime, default=datetime.utcnow, comment="登录时间")
+    
+    # 关联关系
+    user = relationship("User", back_populates="login_logs")
+
+
+class OperationLog(BaseModel):
+    """操作日志表"""
+    __tablename__ = "operation_logs"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="用户ID")
+    username = Column(String(50), nullable=False, comment="用户名")
+    operation_type = Column(String(50), nullable=True, comment="操作类型")
+    target_type = Column(String(50), nullable=True, comment="目标类型")
+    target_id = Column(CHAR(36), nullable=True, comment="目标ID")
+    operation_detail = Column(Text, nullable=True, comment="操作详情")
+    ip_address = Column(String(45), nullable=True, comment="IP地址")
+    user_agent = Column(Text, nullable=True, comment="用户代理")
+    operation_time = Column(DateTime, default=datetime.utcnow, comment="操作时间")
+    
+    # 关联关系
+    user = relationship("User")
+
+
+class SyncLog(BaseModel):
+    """同步日志表"""
+    __tablename__ = "sync_logs"
+    
+    app_id = Column(CHAR(36), ForeignKey("apps.id", ondelete="CASCADE"), nullable=False, comment="应用ID")
+    sync_type = Column(String(50), nullable=True, comment="同步类型")
+    sync_status = Column(String(20), nullable=True, comment="同步状态")
+    records_count = Column(Integer, nullable=True, comment="记录数量")
+    success_count = Column(Integer, nullable=True, comment="成功数量")
+    failure_count = Column(Integer, nullable=True, comment="失败数量")
+    error_message = Column(Text, nullable=True, comment="错误信息")
+    start_time = Column(DateTime, nullable=True, comment="开始时间")
+    end_time = Column(DateTime, nullable=True, comment="结束时间")
+    
+    # 关联关系
+    app = relationship("App", back_populates="sync_logs")

+ 55 - 0
src/app/models/token.py

@@ -0,0 +1,55 @@
+"""
+令牌相关数据模型
+"""
+from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.dialects.mysql import CHAR
+from .base import BaseModel
+from datetime import datetime
+
+
+class OAuthAccessToken(BaseModel):
+    """访问令牌表"""
+    __tablename__ = "oauth_access_tokens"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
+    app_id = Column(CHAR(36), ForeignKey("apps.id", ondelete="CASCADE"), nullable=False, comment="应用ID")
+    token = Column(String(512), unique=True, nullable=False, comment="访问令牌")
+    refresh_token = Column(String(512), unique=True, nullable=True, comment="刷新令牌")
+    token_type = Column(String(50), default="Bearer", comment="令牌类型")
+    scope = Column(String(500), nullable=True, comment="权限范围")
+    expires_at = Column(DateTime, nullable=False, comment="过期时间")
+    revoked = Column(Boolean, default=False, comment="是否撤销")
+    last_used_at = Column(DateTime, nullable=True, comment="最后使用时间")
+    
+    # 关联关系
+    user = relationship("User", back_populates="tokens")
+    app = relationship("App", back_populates="tokens")
+
+
+class OAuthAuthorizationCode(BaseModel):
+    """授权码表"""
+    __tablename__ = "oauth_authorization_codes"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
+    app_id = Column(CHAR(36), ForeignKey("apps.id", ondelete="CASCADE"), nullable=False, comment="应用ID")
+    code = Column(String(100), unique=True, nullable=False, comment="授权码")
+    redirect_uri = Column(String(500), nullable=False, comment="回调URL")
+    scope = Column(String(500), nullable=True, comment="权限范围")
+    expires_at = Column(DateTime, nullable=False, comment="过期时间")
+    used = Column(Boolean, default=False, comment="是否已使用")
+    
+    # 关联关系
+    user = relationship("User")
+    app = relationship("App", back_populates="authorization_codes")
+
+
+class TokenBlacklist(BaseModel):
+    """令牌黑名单表"""
+    __tablename__ = "token_blacklist"
+    
+    token = Column(String(512), unique=True, nullable=False, comment="令牌")
+    token_type = Column(String(50), nullable=True, comment="令牌类型")
+    expires_at = Column(DateTime, nullable=False, comment="过期时间")
+    added_at = Column(DateTime, default=datetime.utcnow, comment="加入时间")
+    reason = Column(String(200), nullable=True, comment="加入原因")

+ 103 - 0
src/app/models/user.py

@@ -0,0 +1,103 @@
+"""
+用户相关数据模型
+"""
+from sqlalchemy import Column, String, Boolean, Integer, DateTime, Text, Date, JSON, ForeignKey
+from sqlalchemy.orm import relationship
+from sqlalchemy.dialects.mysql import CHAR, TINYINT
+from .base import BaseModel
+from datetime import datetime
+from typing import Optional
+
+
+class User(BaseModel):
+    """用户表"""
+    __tablename__ = "users"
+    
+    username = Column(String(50), unique=True, nullable=False, comment="用户名")
+    email = Column(String(100), unique=True, nullable=False, comment="邮箱")
+    phone = Column(String(20), unique=True, nullable=True, comment="手机号")
+    password_hash = Column(String(255), nullable=False, comment="密码哈希")
+    avatar_url = Column(String(500), nullable=True, comment="头像URL")
+    is_active = Column(Boolean, default=True, comment="是否激活")
+    is_superuser = Column(Boolean, default=False, comment="是否超级管理员")
+    last_login_at = Column(DateTime, nullable=True, comment="最后登录时间")
+    last_login_ip = Column(String(45), nullable=True, comment="最后登录IP")
+    failed_login_attempts = Column(Integer, default=0, comment="失败登录次数")
+    locked_until = Column(DateTime, nullable=True, comment="锁定直到时间")
+    
+    # 关联关系
+    profile = relationship("UserProfile", back_populates="user", uselist=False)
+    roles = relationship("UserRole", back_populates="user")
+    tokens = relationship("OAuthAccessToken", back_populates="user")
+    login_logs = relationship("LoginLog", back_populates="user")
+
+
+class UserProfile(BaseModel):
+    """用户详情表"""
+    __tablename__ = "user_profiles"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
+    real_name = Column(String(50), nullable=True, comment="真实姓名")
+    gender = Column(TINYINT, nullable=True, comment="性别 0:未知 1:男 2:女")
+    birth_date = Column(Date, nullable=True, comment="出生日期")
+    address = Column(String(500), nullable=True, comment="地址")
+    company = Column(String(100), nullable=True, comment="公司")
+    department = Column(String(100), nullable=True, comment="部门")
+    position = Column(String(100), nullable=True, comment="职位")
+    extra_info = Column(JSON, nullable=True, comment="扩展信息")
+    
+    # 关联关系
+    user = relationship("User", back_populates="profile")
+
+
+class Role(BaseModel):
+    """角色表"""
+    __tablename__ = "roles"
+    
+    name = Column(String(50), unique=True, nullable=False, comment="角色名称")
+    code = Column(String(50), unique=True, nullable=False, comment="角色代码")
+    description = Column(String(500), nullable=True, comment="角色描述")
+    is_active = Column(Boolean, default=True, comment="是否激活")
+    
+    # 关联关系
+    users = relationship("UserRole", back_populates="role")
+    permissions = relationship("RolePermission", back_populates="role")
+
+
+class UserRole(BaseModel):
+    """用户角色关系表"""
+    __tablename__ = "user_roles"
+    
+    user_id = Column(CHAR(36), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, comment="用户ID")
+    role_id = Column(CHAR(36), ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, comment="角色ID")
+    
+    # 关联关系
+    user = relationship("User", back_populates="roles")
+    role = relationship("Role", back_populates="users")
+
+
+class Permission(BaseModel):
+    """权限表"""
+    __tablename__ = "permissions"
+    
+    name = Column(String(100), nullable=False, comment="权限名称")
+    code = Column(String(100), unique=True, nullable=False, comment="权限代码")
+    description = Column(String(500), nullable=True, comment="权限描述")
+    resource = Column(String(100), nullable=True, comment="资源")
+    action = Column(String(50), nullable=True, comment="操作")
+    is_active = Column(Boolean, default=True, comment="是否激活")
+    
+    # 关联关系
+    roles = relationship("RolePermission", back_populates="permission")
+
+
+class RolePermission(BaseModel):
+    """角色权限关系表"""
+    __tablename__ = "role_permissions"
+    
+    role_id = Column(CHAR(36), ForeignKey("roles.id", ondelete="CASCADE"), nullable=False, comment="角色ID")
+    permission_id = Column(CHAR(36), ForeignKey("permissions.id", ondelete="CASCADE"), nullable=False, comment="权限ID")
+    
+    # 关联关系
+    role = relationship("Role", back_populates="permissions")
+    permission = relationship("Permission", back_populates="roles")

+ 1 - 0
src/app/schemas/__init__.py

@@ -0,0 +1 @@
+"""Schema模块"""

BIN
src/app/schemas/__pycache__/__init__.cpython-312.pyc


BIN
src/app/schemas/__pycache__/auth.cpython-312.pyc


BIN
src/app/schemas/__pycache__/base.cpython-312.pyc


+ 122 - 0
src/app/schemas/auth.py

@@ -0,0 +1,122 @@
+"""
+认证相关Schema模型
+"""
+from pydantic import BaseModel, Field, EmailStr, field_validator
+from typing import Optional, List
+from .base import BaseSchema
+
+
+class LoginRequest(BaseSchema):
+    """登录请求Schema"""
+    username: str = Field(..., description="用户名或邮箱")
+    password: str = Field(..., description="密码")
+    grant_type: str = Field(default="password", description="授权类型")
+    client_id: Optional[str] = Field(None, description="客户端ID")
+    remember_me: bool = Field(default=False, description="记住我")
+
+
+class RegisterRequest(BaseSchema):
+    """注册请求Schema"""
+    username: str = Field(..., min_length=3, max_length=50, description="用户名")
+    email: EmailStr = Field(..., description="邮箱")
+    password: str = Field(..., min_length=8, max_length=128, description="密码")
+    phone: Optional[str] = Field(None, max_length=20, description="手机号")
+    captcha: str = Field(..., description="验证码")
+    
+    @field_validator("password")
+    @classmethod
+    def validate_password(cls, v):
+        """密码验证"""
+        if not any(c.isupper() for c in v):
+            raise ValueError("密码必须包含至少一个大写字母")
+        if not any(c.islower() for c in v):
+            raise ValueError("密码必须包含至少一个小写字母")
+        if not any(c.isdigit() for c in v):
+            raise ValueError("密码必须包含至少一个数字")
+        return v
+
+
+class TokenResponse(BaseSchema):
+    """令牌响应Schema"""
+    access_token: str = Field(..., description="访问令牌")
+    refresh_token: Optional[str] = Field(None, description="刷新令牌")
+    token_type: str = Field(default="Bearer", description="令牌类型")
+    expires_in: int = Field(..., description="过期时间(秒)")
+    scope: Optional[str] = Field(None, description="权限范围")
+
+
+class RefreshTokenRequest(BaseSchema):
+    """刷新令牌请求Schema"""
+    refresh_token: str = Field(..., description="刷新令牌")
+    client_id: Optional[str] = Field(None, description="客户端ID")
+
+
+class LogoutRequest(BaseSchema):
+    """登出请求Schema"""
+    token: Optional[str] = Field(None, description="访问令牌")
+    refresh_token: Optional[str] = Field(None, description="刷新令牌")
+
+
+class ForgotPasswordRequest(BaseSchema):
+    """忘记密码请求Schema"""
+    email: EmailStr = Field(..., description="邮箱")
+
+
+class ResetPasswordRequest(BaseSchema):
+    """重置密码请求Schema"""
+    token: str = Field(..., description="重置令牌")
+    new_password: str = Field(..., min_length=8, max_length=128, description="新密码")
+    
+    @field_validator("new_password")
+    @classmethod
+    def validate_password(cls, v):
+        """密码验证"""
+        if not any(c.isupper() for c in v):
+            raise ValueError("密码必须包含至少一个大写字母")
+        if not any(c.islower() for c in v):
+            raise ValueError("密码必须包含至少一个小写字母")
+        if not any(c.isdigit() for c in v):
+            raise ValueError("密码必须包含至少一个数字")
+        return v
+
+
+class CaptchaResponse(BaseSchema):
+    """验证码响应Schema"""
+    captcha_id: str = Field(..., description="验证码ID")
+    captcha_image: str = Field(..., description="验证码图片(base64)")
+
+
+class UserInfoResponse(BaseSchema):
+    """用户信息响应Schema"""
+    id: str = Field(..., description="用户ID")
+    username: str = Field(..., description="用户名")
+    email: str = Field(..., description="邮箱")
+    phone: Optional[str] = Field(None, description="手机号")
+    avatar_url: Optional[str] = Field(None, description="头像URL")
+    is_active: bool = Field(..., description="是否激活")
+    roles: List[str] = Field(default=[], description="角色列表")
+    permissions: List[str] = Field(default=[], description="权限列表")
+
+
+class TokenIntrospectRequest(BaseSchema):
+    """令牌内省请求Schema"""
+    token: str = Field(..., description="令牌")
+    token_type_hint: Optional[str] = Field(None, description="令牌类型提示")
+
+
+class TokenIntrospectResponse(BaseSchema):
+    """令牌内省响应Schema"""
+    active: bool = Field(..., description="是否有效")
+    client_id: Optional[str] = Field(None, description="客户端ID")
+    username: Optional[str] = Field(None, description="用户名")
+    scope: Optional[str] = Field(None, description="权限范围")
+    exp: Optional[int] = Field(None, description="过期时间戳")
+    iat: Optional[int] = Field(None, description="签发时间戳")
+    sub: Optional[str] = Field(None, description="主题(用户ID)")
+    aud: Optional[str] = Field(None, description="受众(客户端ID)")
+
+
+class RevokeTokenRequest(BaseSchema):
+    """撤销令牌请求Schema"""
+    token: str = Field(..., description="令牌")
+    token_type_hint: Optional[str] = Field(None, description="令牌类型提示")

+ 54 - 0
src/app/schemas/base.py

@@ -0,0 +1,54 @@
+"""
+基础Schema模型
+"""
+from pydantic import BaseModel, Field
+from typing import Optional, Any, Dict
+from datetime import datetime
+
+
+class BaseSchema(BaseModel):
+    """基础Schema"""
+    
+    class Config:
+        from_attributes = True
+        json_encoders = {
+            datetime: lambda v: v.isoformat() if v else None
+        }
+
+
+class ResponseSchema(BaseSchema):
+    """统一响应Schema"""
+    code: int = Field(default=0, description="响应码")
+    message: str = Field(default="success", description="响应消息")
+    data: Optional[Any] = Field(default=None, description="响应数据")
+    timestamp: datetime = Field(default_factory=datetime.utcnow, description="响应时间")
+
+
+class PaginationSchema(BaseSchema):
+    """分页Schema"""
+    page: int = Field(default=1, ge=1, description="页码")
+    page_size: int = Field(default=20, ge=1, le=100, description="每页数量")
+    total: int = Field(default=0, description="总数量")
+    total_pages: int = Field(default=0, description="总页数")
+
+
+class PaginatedResponseSchema(ResponseSchema):
+    """分页响应Schema"""
+    data: Optional[Any] = Field(default=None, description="响应数据")
+    meta: Optional[PaginationSchema] = Field(default=None, description="分页信息")
+
+
+class IDSchema(BaseSchema):
+    """ID Schema"""
+    id: str = Field(..., description="ID")
+
+
+class TimestampSchema(BaseSchema):
+    """时间戳Schema"""
+    created_at: Optional[datetime] = Field(default=None, description="创建时间")
+    updated_at: Optional[datetime] = Field(default=None, description="更新时间")
+
+
+class BaseModelSchema(IDSchema, TimestampSchema):
+    """基础模型Schema"""
+    is_deleted: bool = Field(default=False, description="是否删除")

+ 168 - 0
src/app/schemas/user.py

@@ -0,0 +1,168 @@
+"""
+用户相关Schema模型
+"""
+from pydantic import BaseModel, Field, EmailStr, field_validator
+from typing import Optional, List, Dict, Any
+from datetime import datetime, date
+from .base import BaseSchema, BaseModelSchema
+
+
+class UserBase(BaseSchema):
+    """用户基础Schema"""
+    username: str = Field(..., min_length=3, max_length=50, description="用户名")
+    email: EmailStr = Field(..., description="邮箱")
+    phone: Optional[str] = Field(None, max_length=20, description="手机号")
+    avatar_url: Optional[str] = Field(None, max_length=500, description="头像URL")
+
+
+class UserCreate(UserBase):
+    """创建用户Schema"""
+    password: str = Field(..., min_length=8, max_length=128, description="密码")
+    
+    @field_validator("password")
+    @classmethod
+    def validate_password(cls, v):
+        """密码验证"""
+        if not any(c.isupper() for c in v):
+            raise ValueError("密码必须包含至少一个大写字母")
+        if not any(c.islower() for c in v):
+            raise ValueError("密码必须包含至少一个小写字母")
+        if not any(c.isdigit() for c in v):
+            raise ValueError("密码必须包含至少一个数字")
+        return v
+
+
+class UserUpdate(BaseSchema):
+    """更新用户Schema"""
+    email: Optional[EmailStr] = Field(None, description="邮箱")
+    phone: Optional[str] = Field(None, max_length=20, description="手机号")
+    avatar_url: Optional[str] = Field(None, max_length=500, description="头像URL")
+
+
+class UserPasswordUpdate(BaseSchema):
+    """更新密码Schema"""
+    old_password: str = Field(..., description="旧密码")
+    new_password: str = Field(..., min_length=8, max_length=128, description="新密码")
+    
+    @field_validator("new_password")
+    @classmethod
+    def validate_password(cls, v):
+        """密码验证"""
+        if not any(c.isupper() for c in v):
+            raise ValueError("密码必须包含至少一个大写字母")
+        if not any(c.islower() for c in v):
+            raise ValueError("密码必须包含至少一个小写字母")
+        if not any(c.isdigit() for c in v):
+            raise ValueError("密码必须包含至少一个数字")
+        return v
+
+
+class UserStatusUpdate(BaseSchema):
+    """更新用户状态Schema"""
+    is_active: bool = Field(..., description="是否激活")
+
+
+class UserResponse(UserBase, BaseModelSchema):
+    """用户响应Schema"""
+    is_active: bool = Field(..., description="是否激活")
+    is_superuser: bool = Field(..., description="是否超级管理员")
+    last_login_at: Optional[datetime] = Field(None, description="最后登录时间")
+    last_login_ip: Optional[str] = Field(None, description="最后登录IP")
+    failed_login_attempts: int = Field(..., description="失败登录次数")
+    locked_until: Optional[datetime] = Field(None, description="锁定直到时间")
+
+
+class UserProfileBase(BaseSchema):
+    """用户详情基础Schema"""
+    real_name: Optional[str] = Field(None, max_length=50, description="真实姓名")
+    gender: Optional[int] = Field(None, ge=0, le=2, description="性别 0:未知 1:男 2:女")
+    birth_date: Optional[date] = Field(None, description="出生日期")
+    address: Optional[str] = Field(None, max_length=500, description="地址")
+    company: Optional[str] = Field(None, max_length=100, description="公司")
+    department: Optional[str] = Field(None, max_length=100, description="部门")
+    position: Optional[str] = Field(None, max_length=100, description="职位")
+    extra_info: Optional[Dict[str, Any]] = Field(None, description="扩展信息")
+
+
+class UserProfileCreate(UserProfileBase):
+    """创建用户详情Schema"""
+    user_id: str = Field(..., description="用户ID")
+
+
+class UserProfileUpdate(UserProfileBase):
+    """更新用户详情Schema"""
+    pass
+
+
+class UserProfileResponse(UserProfileBase, BaseModelSchema):
+    """用户详情响应Schema"""
+    user_id: str = Field(..., description="用户ID")
+
+
+class UserWithProfileResponse(UserResponse):
+    """用户及详情响应Schema"""
+    profile: Optional[UserProfileResponse] = Field(None, description="用户详情")
+
+
+class RoleBase(BaseSchema):
+    """角色基础Schema"""
+    name: str = Field(..., max_length=50, description="角色名称")
+    code: str = Field(..., max_length=50, description="角色代码")
+    description: Optional[str] = Field(None, max_length=500, description="角色描述")
+
+
+class RoleCreate(RoleBase):
+    """创建角色Schema"""
+    pass
+
+
+class RoleUpdate(BaseSchema):
+    """更新角色Schema"""
+    name: Optional[str] = Field(None, max_length=50, description="角色名称")
+    description: Optional[str] = Field(None, max_length=500, description="角色描述")
+    is_active: Optional[bool] = Field(None, description="是否激活")
+
+
+class RoleResponse(RoleBase, BaseModelSchema):
+    """角色响应Schema"""
+    is_active: bool = Field(..., description="是否激活")
+
+
+class PermissionBase(BaseSchema):
+    """权限基础Schema"""
+    name: str = Field(..., max_length=100, description="权限名称")
+    code: str = Field(..., max_length=100, description="权限代码")
+    description: Optional[str] = Field(None, max_length=500, description="权限描述")
+    resource: Optional[str] = Field(None, max_length=100, description="资源")
+    action: Optional[str] = Field(None, max_length=50, description="操作")
+
+
+class PermissionCreate(PermissionBase):
+    """创建权限Schema"""
+    pass
+
+
+class PermissionUpdate(BaseSchema):
+    """更新权限Schema"""
+    name: Optional[str] = Field(None, max_length=100, description="权限名称")
+    description: Optional[str] = Field(None, max_length=500, description="权限描述")
+    resource: Optional[str] = Field(None, max_length=100, description="资源")
+    action: Optional[str] = Field(None, max_length=50, description="操作")
+    is_active: Optional[bool] = Field(None, description="是否激活")
+
+
+class PermissionResponse(PermissionBase, BaseModelSchema):
+    """权限响应Schema"""
+    is_active: bool = Field(..., description="是否激活")
+
+
+class UserRoleAssign(BaseSchema):
+    """用户角色分配Schema"""
+    user_id: str = Field(..., description="用户ID")
+    role_ids: List[str] = Field(..., description="角色ID列表")
+
+
+class RolePermissionAssign(BaseSchema):
+    """角色权限分配Schema"""
+    role_id: str = Field(..., description="角色ID")
+    permission_ids: List[str] = Field(..., description="权限ID列表")

+ 1 - 0
src/app/services/__init__.py

@@ -0,0 +1 @@
+"""服务模块"""

BIN
src/app/services/__pycache__/__init__.cpython-312.pyc


BIN
src/app/services/__pycache__/auth_service.cpython-312.pyc


BIN
src/app/services/__pycache__/oauth_service.cpython-312.pyc


+ 297 - 0
src/app/services/auth_service.py

@@ -0,0 +1,297 @@
+"""
+认证服务模块
+"""
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, and_, or_
+from datetime import datetime, timedelta
+from typing import Optional, Dict, Any, Tuple
+from app.models import User, LoginLog, OAuthAccessToken, TokenBlacklist
+from app.schemas.auth import LoginRequest, TokenResponse, UserInfoResponse
+from app.utils.security import (
+    verify_password, 
+    create_access_token, 
+    create_refresh_token,
+    verify_token,
+    generate_random_string
+)
+from app.config.simple_settings import settings
+from app.core.exceptions import AuthenticationError, ValidationError
+import ipaddress
+
+
+class AuthService:
+    """认证服务类"""
+    
+    def __init__(self, db: AsyncSession):
+        self.db = db
+    
+    async def authenticate_user(
+        self, 
+        username: str, 
+        password: str,
+        ip_address: Optional[str] = None,
+        user_agent: Optional[str] = None
+    ) -> Tuple[User, TokenResponse]:
+        """用户认证"""
+        
+        # 查找用户
+        stmt = select(User).where(
+            and_(
+                or_(User.username == username, User.email == username),
+                User.is_deleted == False
+            )
+        )
+        result = await self.db.execute(stmt)
+        user = result.scalar_one_or_none()
+        
+        # 记录登录日志
+        login_log = LoginLog(
+            user_id=user.id if user else None,
+            username=username,
+            login_type="password",
+            ip_address=ip_address,
+            user_agent=user_agent,
+            success=False
+        )
+        
+        if not user:
+            login_log.failure_reason = "用户不存在"
+            self.db.add(login_log)
+            await self.db.commit()
+            raise AuthenticationError("用户名或密码错误")
+        
+        # 检查用户状态
+        if not user.is_active:
+            login_log.failure_reason = "用户已被禁用"
+            self.db.add(login_log)
+            await self.db.commit()
+            raise AuthenticationError("用户已被禁用")
+        
+        # 检查账户锁定
+        if user.locked_until and user.locked_until > datetime.utcnow():
+            login_log.failure_reason = "账户已锁定"
+            self.db.add(login_log)
+            await self.db.commit()
+            raise AuthenticationError(f"账户已锁定,请在{user.locked_until}后重试")
+        
+        # 验证密码
+        if not verify_password(password, user.password_hash):
+            # 增加失败次数
+            user.failed_login_attempts += 1
+            
+            # 检查是否需要锁定账户
+            if user.failed_login_attempts >= settings.MAX_LOGIN_ATTEMPTS:
+                user.locked_until = datetime.utcnow() + timedelta(
+                    minutes=settings.LOCKOUT_DURATION_MINUTES
+                )
+                login_log.failure_reason = f"密码错误,账户已锁定{settings.LOCKOUT_DURATION_MINUTES}分钟"
+            else:
+                remaining_attempts = settings.MAX_LOGIN_ATTEMPTS - user.failed_login_attempts
+                login_log.failure_reason = f"密码错误,还有{remaining_attempts}次尝试机会"
+            
+            self.db.add(login_log)
+            await self.db.commit()
+            raise AuthenticationError("用户名或密码错误")
+        
+        # 登录成功,重置失败次数
+        user.failed_login_attempts = 0
+        user.locked_until = None
+        user.last_login_at = datetime.utcnow()
+        user.last_login_ip = ip_address
+        
+        # 生成令牌
+        token_data = {
+            "sub": user.id,
+            "username": user.username,
+            "email": user.email,
+            "is_superuser": user.is_superuser
+        }
+        
+        access_token = create_access_token(token_data)
+        refresh_token = create_refresh_token({"sub": user.id})
+        
+        # 保存令牌到数据库
+        oauth_token = OAuthAccessToken(
+            user_id=user.id,
+            app_id=None,  # 系统内部登录
+            token=access_token,
+            refresh_token=refresh_token,
+            token_type="Bearer",
+            scope="profile email",
+            expires_at=datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+        )
+        self.db.add(oauth_token)
+        
+        # 记录成功登录日志
+        login_log.success = True
+        login_log.user_id = user.id
+        self.db.add(login_log)
+        
+        await self.db.commit()
+        
+        token_response = TokenResponse(
+            access_token=access_token,
+            refresh_token=refresh_token,
+            token_type="Bearer",
+            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
+            scope="profile email"
+        )
+        
+        return user, token_response
+    
+    async def refresh_access_token(self, refresh_token: str) -> TokenResponse:
+        """刷新访问令牌"""
+        
+        # 验证刷新令牌
+        payload = verify_token(refresh_token)
+        if not payload or payload.get("type") != "refresh":
+            raise AuthenticationError("无效的刷新令牌")
+        
+        user_id = payload.get("sub")
+        if not user_id:
+            raise AuthenticationError("无效的刷新令牌")
+        
+        # 检查令牌是否在黑名单中
+        stmt = select(TokenBlacklist).where(TokenBlacklist.token == refresh_token)
+        result = await self.db.execute(stmt)
+        if result.scalar_one_or_none():
+            raise AuthenticationError("令牌已被撤销")
+        
+        # 查找用户
+        stmt = select(User).where(and_(User.id == user_id, User.is_deleted == False))
+        result = await self.db.execute(stmt)
+        user = result.scalar_one_or_none()
+        
+        if not user or not user.is_active:
+            raise AuthenticationError("用户不存在或已被禁用")
+        
+        # 生成新的访问令牌
+        token_data = {
+            "sub": user.id,
+            "username": user.username,
+            "email": user.email,
+            "is_superuser": user.is_superuser
+        }
+        
+        access_token = create_access_token(token_data)
+        
+        # 更新数据库中的令牌
+        stmt = select(OAuthAccessToken).where(
+            OAuthAccessToken.refresh_token == refresh_token
+        )
+        result = await self.db.execute(stmt)
+        oauth_token = result.scalar_one_or_none()
+        
+        if oauth_token:
+            oauth_token.token = access_token
+            oauth_token.expires_at = datetime.utcnow() + timedelta(
+                minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
+            )
+            oauth_token.last_used_at = datetime.utcnow()
+        
+        await self.db.commit()
+        
+        return TokenResponse(
+            access_token=access_token,
+            refresh_token=refresh_token,
+            token_type="Bearer",
+            expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
+            scope="profile email"
+        )
+    
+    async def get_current_user(self, token: str) -> User:
+        """获取当前用户"""
+        
+        # 验证令牌
+        payload = verify_token(token)
+        if not payload:
+            raise AuthenticationError("无效的访问令牌")
+        
+        user_id = payload.get("sub")
+        if not user_id:
+            raise AuthenticationError("无效的访问令牌")
+        
+        # 检查令牌是否在黑名单中
+        stmt = select(TokenBlacklist).where(TokenBlacklist.token == token)
+        result = await self.db.execute(stmt)
+        if result.scalar_one_or_none():
+            raise AuthenticationError("令牌已被撤销")
+        
+        # 查找用户
+        stmt = select(User).where(and_(User.id == user_id, User.is_deleted == False))
+        result = await self.db.execute(stmt)
+        user = result.scalar_one_or_none()
+        
+        if not user or not user.is_active:
+            raise AuthenticationError("用户不存在或已被禁用")
+        
+        # 更新令牌最后使用时间
+        stmt = select(OAuthAccessToken).where(OAuthAccessToken.token == token)
+        result = await self.db.execute(stmt)
+        oauth_token = result.scalar_one_or_none()
+        if oauth_token:
+            oauth_token.last_used_at = datetime.utcnow()
+            await self.db.commit()
+        
+        return user
+    
+    async def logout(self, token: str, refresh_token: Optional[str] = None):
+        """用户登出"""
+        
+        # 将访问令牌加入黑名单
+        if token:
+            payload = verify_token(token)
+            if payload:
+                exp = payload.get("exp")
+                if exp:
+                    expires_at = datetime.fromtimestamp(exp)
+                    blacklist_token = TokenBlacklist(
+                        token=token,
+                        token_type="access_token",
+                        expires_at=expires_at,
+                        reason="用户登出"
+                    )
+                    self.db.add(blacklist_token)
+        
+        # 将刷新令牌加入黑名单
+        if refresh_token:
+            payload = verify_token(refresh_token)
+            if payload:
+                exp = payload.get("exp")
+                if exp:
+                    expires_at = datetime.fromtimestamp(exp)
+                    blacklist_token = TokenBlacklist(
+                        token=refresh_token,
+                        token_type="refresh_token",
+                        expires_at=expires_at,
+                        reason="用户登出"
+                    )
+                    self.db.add(blacklist_token)
+        
+        await self.db.commit()
+    
+    async def get_user_info(self, user: User) -> UserInfoResponse:
+        """获取用户信息"""
+        
+        # TODO: 获取用户角色和权限
+        roles = []
+        permissions = []
+        
+        return UserInfoResponse(
+            id=user.id,
+            username=user.username,
+            email=user.email,
+            phone=user.phone,
+            avatar_url=user.avatar_url,
+            is_active=user.is_active,
+            roles=roles,
+            permissions=permissions
+        )
+    
+    async def validate_ip_address(self, ip_address: str) -> bool:
+        """验证IP地址"""
+        try:
+            ipaddress.ip_address(ip_address)
+            return True
+        except ValueError:
+            return False

+ 12 - 0
src/app/services/oauth_service.py

@@ -0,0 +1,12 @@
+"""
+OAuth服务模块
+"""
+from sqlalchemy.ext.asyncio import AsyncSession
+from typing import Optional
+
+
+class OAuthService:
+    """OAuth服务类"""
+    
+    def __init__(self, db: AsyncSession):
+        self.db = db

+ 1 - 0
src/app/utils/__init__.py

@@ -0,0 +1 @@
+"""工具模块"""

BIN
src/app/utils/__pycache__/__init__.cpython-312.pyc


BIN
src/app/utils/__pycache__/security.cpython-312.pyc


+ 150 - 0
src/app/utils/security.py

@@ -0,0 +1,150 @@
+"""
+安全工具模块
+"""
+from passlib.context import CryptContext
+from jose import JWTError, jwt
+from datetime import datetime, timedelta
+from typing import Optional, Dict, Any
+from app.config.simple_settings import settings
+import secrets
+import string
+import hashlib
+import base64
+
+
+# 密码加密上下文
+pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
+
+
+def hash_password(password: str) -> str:
+    """密码哈希"""
+    # 确保密码不超过72字节
+    if len(password.encode('utf-8')) > 72:
+        password = password[:72]
+    return pwd_context.hash(password)
+
+
+def verify_password(plain_password: str, hashed_password: str) -> bool:
+    """验证密码"""
+    return pwd_context.verify(plain_password, hashed_password)
+
+
+def create_access_token(
+    data: Dict[str, Any], 
+    expires_delta: Optional[timedelta] = None
+) -> str:
+    """创建访问令牌"""
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
+    
+    to_encode.update({"exp": expire, "iat": datetime.utcnow()})
+    encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.ALGORITHM)
+    return encoded_jwt
+
+
+def create_refresh_token(
+    data: Dict[str, Any], 
+    expires_delta: Optional[timedelta] = None
+) -> str:
+    """创建刷新令牌"""
+    to_encode = data.copy()
+    if expires_delta:
+        expire = datetime.utcnow() + expires_delta
+    else:
+        expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
+    
+    to_encode.update({"exp": expire, "iat": datetime.utcnow(), "type": "refresh"})
+    encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.ALGORITHM)
+    return encoded_jwt
+
+
+def verify_token(token: str) -> Optional[Dict[str, Any]]:
+    """验证令牌"""
+    try:
+        payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.ALGORITHM])
+        return payload
+    except JWTError:
+        return None
+
+
+def generate_random_string(length: int = 32) -> str:
+    """生成随机字符串"""
+    alphabet = string.ascii_letters + string.digits
+    return ''.join(secrets.choice(alphabet) for _ in range(length))
+
+
+def generate_authorization_code() -> str:
+    """生成授权码"""
+    return generate_random_string(32)
+
+
+def generate_app_key() -> str:
+    """生成应用Key"""
+    return generate_random_string(32)
+
+
+def generate_app_secret() -> str:
+    """生成应用Secret"""
+    return generate_random_string(64)
+
+
+def generate_state() -> str:
+    """生成状态码"""
+    return generate_random_string(16)
+
+
+def hash_client_secret(secret: str) -> str:
+    """客户端密钥哈希"""
+    return hashlib.sha256(secret.encode()).hexdigest()
+
+
+def verify_client_secret(plain_secret: str, hashed_secret: str) -> bool:
+    """验证客户端密钥"""
+    return hash_client_secret(plain_secret) == hashed_secret
+
+
+def encode_basic_auth(username: str, password: str) -> str:
+    """编码Basic认证"""
+    credentials = f"{username}:{password}"
+    encoded_credentials = base64.b64encode(credentials.encode()).decode()
+    return f"Basic {encoded_credentials}"
+
+
+def decode_basic_auth(auth_header: str) -> Optional[tuple[str, str]]:
+    """解码Basic认证"""
+    try:
+        if not auth_header.startswith("Basic "):
+            return None
+        
+        encoded_credentials = auth_header[6:]
+        decoded_credentials = base64.b64decode(encoded_credentials).decode()
+        username, password = decoded_credentials.split(":", 1)
+        return username, password
+    except Exception:
+        return None
+
+
+def is_safe_url(url: str, allowed_hosts: list[str]) -> bool:
+    """检查URL是否安全"""
+    try:
+        from urllib.parse import urlparse
+        parsed = urlparse(url)
+        return parsed.netloc in allowed_hosts
+    except Exception:
+        return False
+
+
+def generate_csrf_token() -> str:
+    """生成CSRF令牌"""
+    return generate_random_string(32)
+
+
+def mask_sensitive_data(data: str, mask_char: str = "*", visible_chars: int = 4) -> str:
+    """掩码敏感数据"""
+    if len(data) <= visible_chars:
+        return mask_char * len(data)
+    
+    return data[:visible_chars] + mask_char * (len(data) - visible_chars)

+ 47 - 0
test_captcha.py

@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""
+测试验证码API
+"""
+import requests
+import json
+
+def test_captcha():
+    """测试验证码"""
+    url = "http://localhost:8000/api/v1/auth/captcha"
+    
+    print("测试验证码API...")
+    print(f"URL: {url}")
+    
+    try:
+        response = requests.get(url, timeout=10)
+        print(f"\n状态码: {response.status_code}")
+        
+        if response.status_code == 200:
+            result = response.json()
+            print(f"响应码: {result.get('code')}")
+            print(f"消息: {result.get('message')}")
+            
+            data = result.get('data', {})
+            captcha_id = data.get('captcha_id')
+            captcha_image = data.get('captcha_image', '')
+            captcha_text = data.get('captcha_text')
+            
+            print(f"验证码ID: {captcha_id}")
+            print(f"验证码文本: {captcha_text}")
+            print(f"图片格式: {captcha_image[:50]}...")
+            
+            if result.get('code') == 0:
+                print("\n✅ 验证码API正常工作!")
+            else:
+                print(f"\n❌ API返回错误: {result.get('message')}")
+        else:
+            print(f"\n❌ HTTP错误: {response.status_code}")
+            print(f"响应内容: {response.text}")
+            
+    except requests.exceptions.ConnectionError:
+        print("\n❌ 无法连接到服务器,请确保后端服务正在运行")
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+
+if __name__ == "__main__":
+    test_captcha()

+ 81 - 0
test_db_connection.py

@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""
+测试数据库连接脚本
+"""
+import os
+import pymysql
+from dotenv import load_dotenv
+
+def test_mysql_connection():
+    """测试MySQL连接"""
+    load_dotenv()
+    
+    # 从环境变量获取数据库配置
+    database_url = os.getenv('DATABASE_URL', '')
+    print(f"数据库URL: {database_url}")
+    
+    if not database_url:
+        print("❌ DATABASE_URL未设置")
+        return False
+    
+    # 解析数据库URL
+    from urllib.parse import urlparse
+    parsed = urlparse(database_url)
+    
+    config = {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'charset': 'utf8mb4'
+    }
+    
+    print(f"连接配置: host={config['host']}, port={config['port']}, user={config['user']}")
+    
+    try:
+        # 测试连接(不指定数据库)
+        connection = pymysql.connect(**config)
+        print("✅ MySQL连接成功")
+        
+        # 检查数据库是否存在
+        db_name = parsed.path[1:] if parsed.path else 'sso_db'
+        cursor = connection.cursor()
+        cursor.execute("SHOW DATABASES")
+        databases = [db[0] for db in cursor.fetchall()]
+        
+        if db_name in databases:
+            print(f"✅ 数据库 '{db_name}' 已存在")
+        else:
+            print(f"⚠️  数据库 '{db_name}' 不存在,需要创建")
+            
+            # 尝试创建数据库
+            try:
+                cursor.execute(f"CREATE DATABASE {db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci")
+                print(f"✅ 数据库 '{db_name}' 创建成功")
+            except Exception as e:
+                print(f"❌ 创建数据库失败: {e}")
+        
+        cursor.close()
+        connection.close()
+        return True
+        
+    except Exception as e:
+        print(f"❌ MySQL连接失败: {e}")
+        print("\n可能的解决方案:")
+        print("1. 检查MySQL服务是否启动")
+        print("2. 检查用户名和密码是否正确")
+        print("3. 检查主机和端口是否正确")
+        print("4. 尝试在命令行连接: mysql -h localhost -u root -p")
+        return False
+
+if __name__ == "__main__":
+    print("=" * 50)
+    print("MySQL连接测试")
+    print("=" * 50)
+    
+    if test_mysql_connection():
+        print("\n🎉 数据库连接测试通过!")
+        print("现在可以运行: python scripts/init_db.py")
+    else:
+        print("\n❌ 数据库连接测试失败")
+        print("请修复数据库连接问题后重试")

+ 45 - 0
test_login.py

@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+"""
+测试登录功能
+"""
+import requests
+import json
+
+def test_login():
+    """测试登录"""
+    url = "http://localhost:8000/api/v1/auth/login"
+    
+    data = {
+        "username": "admin",
+        "password": "Admin123456",
+        "remember_me": False
+    }
+    
+    print("测试登录...")
+    print(f"URL: {url}")
+    print(f"数据: {json.dumps(data, indent=2)}")
+    
+    try:
+        response = requests.post(url, json=data, timeout=10)
+        print(f"\n状态码: {response.status_code}")
+        print(f"响应: {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
+        
+        if response.status_code == 200:
+            result = response.json()
+            if result.get('code') == 0:
+                print("\n✅ 登录成功!")
+                token = result.get('data', {}).get('access_token')
+                if token:
+                    print(f"访问令牌: {token[:50]}...")
+            else:
+                print(f"\n❌ 登录失败: {result.get('message')}")
+        else:
+            print(f"\n❌ HTTP错误: {response.status_code}")
+            
+    except requests.exceptions.ConnectionError:
+        print("\n❌ 无法连接到服务器,请确保后端服务正在运行")
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+
+if __name__ == "__main__":
+    test_login()

+ 80 - 0
test_oauth.py

@@ -0,0 +1,80 @@
+#!/usr/bin/env python3
+"""
+测试OAuth2端点
+"""
+import requests
+import json
+
+def test_oauth_authorize():
+    """测试OAuth2授权端点"""
+    url = "http://localhost:8000/oauth/authorize"
+    
+    params = {
+        "response_type": "code",
+        "client_id": "eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY",
+        "redirect_uri": "http://localhost:8001/auth/callback",
+        "scope": "profile email",
+        "state": "test_state_123"
+    }
+    
+    print("测试OAuth2授权端点...")
+    print(f"URL: {url}")
+    print(f"参数: {json.dumps(params, indent=2)}")
+    
+    try:
+        response = requests.get(url, params=params, timeout=10, allow_redirects=False)
+        print(f"\n状态码: {response.status_code}")
+        print(f"响应头: {dict(response.headers)}")
+        
+        if response.status_code == 200:
+            print("✅ 授权端点正常工作!")
+            print("返回了授权页面HTML")
+        elif response.status_code == 302:
+            print("✅ 受信任应用自动重定向!")
+            print(f"重定向到: {response.headers.get('location')}")
+        else:
+            print(f"❌ 意外的状态码: {response.status_code}")
+            print(f"响应内容: {response.text}")
+            
+    except requests.exceptions.ConnectionError:
+        print("\n❌ 无法连接到服务器,请确保后端服务正在运行")
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+
+def test_oauth_token():
+    """测试OAuth2令牌端点"""
+    url = "http://localhost:8000/oauth/token"
+    
+    data = {
+        "grant_type": "authorization_code",
+        "code": "test_auth_code_123",
+        "redirect_uri": "http://localhost:8001/auth/callback",
+        "client_id": "eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY",
+        "client_secret": "LKJm5XHJFhhgxSv9nQhoQNNI3wrKyWGZCaPQ4qc43Lf5qfXdLAHoGAHhCYqApEpr"
+    }
+    
+    print("\n" + "="*50)
+    print("测试OAuth2令牌端点...")
+    print(f"URL: {url}")
+    print(f"数据: {json.dumps(data, indent=2)}")
+    
+    try:
+        response = requests.post(url, data=data, timeout=10)
+        print(f"\n状态码: {response.status_code}")
+        
+        if response.status_code == 200:
+            result = response.json()
+            print("✅ 令牌端点正常工作!")
+            print(f"访问令牌: {result.get('access_token', '')[:50]}...")
+            print(f"令牌类型: {result.get('token_type')}")
+            print(f"过期时间: {result.get('expires_in')} 秒")
+        else:
+            print(f"❌ 令牌请求失败: {response.status_code}")
+            print(f"响应内容: {response.text}")
+            
+    except Exception as e:
+        print(f"\n❌ 测试失败: {e}")
+
+if __name__ == "__main__":
+    test_oauth_authorize()
+    test_oauth_token()

+ 78 - 0
test_server.py

@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+"""
+测试服务器 - 最小化版本
+"""
+import sys
+import os
+
+# 添加src目录到Python路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="SSO认证中心",
+    version="1.0.0",
+    description="OAuth2单点登录认证中心",
+    docs_url="/docs",
+    redoc_url="/redoc"
+)
+
+# 配置CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {
+        "message": "SSO认证中心测试服务器",
+        "version": "1.0.0",
+        "status": "running",
+        "docs": "/docs"
+    }
+
+@app.get("/health")
+async def health_check():
+    """健康检查"""
+    return {
+        "status": "healthy",
+        "message": "服务正常运行"
+    }
+
+@app.get("/test")
+async def test_endpoint():
+    """测试端点"""
+    return {
+        "message": "测试成功",
+        "data": {
+            "server": "FastAPI",
+            "python_version": sys.version,
+            "working_directory": os.getcwd()
+        }
+    }
+
+if __name__ == "__main__":
+    import uvicorn
+    print("启动测试服务器...")
+    print("访问地址: http://localhost:8000")
+    print("API文档: http://localhost:8000/docs")
+    print("按 Ctrl+C 停止服务器")
+    
+    uvicorn.run(
+        app,
+        host="0.0.0.0",
+        port=8000,
+        log_level="info"
+    )

+ 81 - 0
test_server_8001.py

@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+"""
+测试服务器 - 使用8001端口
+"""
+import sys
+import os
+
+# 添加src目录到Python路径
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
+
+# 加载环境变量
+from dotenv import load_dotenv
+load_dotenv()
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+
+# 创建FastAPI应用
+app = FastAPI(
+    title="SSO认证中心",
+    version="1.0.0",
+    description="OAuth2单点登录认证中心",
+    docs_url="/docs",
+    redoc_url="/redoc"
+)
+
+# 配置CORS
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+@app.get("/")
+async def root():
+    """根路径"""
+    return {
+        "message": "SSO认证中心测试服务器",
+        "version": "1.0.0",
+        "status": "running",
+        "port": 8001,
+        "docs": "/docs"
+    }
+
+@app.get("/health")
+async def health_check():
+    """健康检查"""
+    return {
+        "status": "healthy",
+        "message": "服务正常运行",
+        "port": 8001
+    }
+
+@app.get("/test")
+async def test_endpoint():
+    """测试端点"""
+    return {
+        "message": "测试成功",
+        "port": 8001,
+        "data": {
+            "server": "FastAPI",
+            "python_version": sys.version,
+            "working_directory": os.getcwd()
+        }
+    }
+
+if __name__ == "__main__":
+    import uvicorn
+    print("启动测试服务器...")
+    print("访问地址: http://localhost:8001")
+    print("API文档: http://localhost:8001/docs")
+    print("按 Ctrl+C 停止服务器")
+    
+    uvicorn.run(
+        app,
+        host="0.0.0.0",
+        port=8001,
+        log_level="info"
+    )

+ 69 - 0
update_app_callback.py

@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+"""
+更新应用回调URL
+"""
+import pymysql
+from urllib.parse import urlparse
+import os
+import json
+from dotenv import load_dotenv
+
+load_dotenv()
+
+def update_app_callback():
+    """更新应用回调URL"""
+    database_url = os.getenv('DATABASE_URL', '')
+    parsed = urlparse(database_url)
+    config = {
+        'host': parsed.hostname or 'localhost',
+        'port': parsed.port or 3306,
+        'user': parsed.username or 'root',
+        'password': parsed.password or '',
+        'database': parsed.path[1:] if parsed.path else 'sso_db',
+        'charset': 'utf8mb4'
+    }
+
+    try:
+        conn = pymysql.connect(**config)
+        cursor = conn.cursor()
+        
+        # 更新回调URL
+        new_redirect_uris = [
+            "http://localhost:8001/auth/callback",  # 子系统后端回调
+            "http://localhost:3001/auth/callback",  # 子系统前端回调
+            "http://localhost:3000/callback",       # 原有的回调URL
+            "http://localhost:8080/callback"        # 原有的回调URL
+        ]
+        
+        cursor.execute("""
+            UPDATE apps 
+            SET redirect_uris = %s, updated_at = NOW()
+            WHERE app_key = %s
+        """, (json.dumps(new_redirect_uris), 'eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY'))
+        
+        conn.commit()
+        
+        # 验证更新
+        cursor.execute("""
+            SELECT name, redirect_uris 
+            FROM apps 
+            WHERE app_key = %s
+        """, ('eqhoIdAyAWbA8MsYHsNqQqNLJbCayTjY',))
+        
+        result = cursor.fetchone()
+        if result:
+            print(f"✅ 应用回调URL更新成功")
+            print(f"应用名称: {result[0]}")
+            print(f"回调URL列表:")
+            urls = json.loads(result[1])
+            for i, url in enumerate(urls, 1):
+                print(f"  {i}. {url}")
+        
+        cursor.close()
+        conn.close()
+        
+    except Exception as e:
+        print(f'❌ 更新失败: {e}')
+
+if __name__ == "__main__":
+    update_app_callback()