From cccb7cfced6a7ecc4eab981512eb0421dd2d4d92 Mon Sep 17 00:00:00 2001 From: tangwang Date: Fri, 24 Oct 2025 17:18:22 +0800 Subject: [PATCH] init --- oauth_setup_guide.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/Develop your application service.md | 0 old/PROJECT_STRUCTURE.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/README.md | 153 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/__pycache__/config.cpython-311.pyc | Bin 0 -> 1291 bytes old/app.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/config.py | 22 ++++++++++++++++++++++ old/env_template.txt | 16 ++++++++++++++++ old/middleware/__pycache__/hmac_validator.cpython-311.pyc | Bin 0 -> 3198 bytes old/middleware/hmac_validator.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/oauth_setup_guide.md | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/requirements.txt | 4 ++++ old/routes/__pycache__/api.cpython-311.pyc | Bin 0 -> 5874 bytes old/routes/__pycache__/auth.cpython-311.pyc | Bin 0 -> 6634 bytes old/routes/__pycache__/webhook.cpython-311.pyc | Bin 0 -> 4628 bytes old/routes/api.py | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/routes/auth.py | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/routes/webhook.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/run.py | 40 ++++++++++++++++++++++++++++++++++++++++ old/step_by_step_oauth.md | 184 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/test_oauth.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ old/后端OAuth2.0认证流程.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ python-app-demo/README.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ python-app-demo/main.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ python-app-demo/requirements.txt | 3 +++ 25 files changed, 1707 insertions(+), 0 deletions(-) create mode 100644 oauth_setup_guide.md create mode 100644 old/Develop your application service.md create mode 100644 old/PROJECT_STRUCTURE.md create mode 100644 old/README.md create mode 100644 old/__pycache__/config.cpython-311.pyc create mode 100644 old/app.py create mode 100644 old/config.py create mode 100644 old/env_template.txt create mode 100644 old/middleware/__pycache__/hmac_validator.cpython-311.pyc create mode 100644 old/middleware/hmac_validator.py create mode 100644 old/oauth_setup_guide.md create mode 100644 old/requirements.txt create mode 100644 old/routes/__pycache__/api.cpython-311.pyc create mode 100644 old/routes/__pycache__/auth.cpython-311.pyc create mode 100644 old/routes/__pycache__/webhook.cpython-311.pyc create mode 100644 old/routes/api.py create mode 100644 old/routes/auth.py create mode 100644 old/routes/webhook.py create mode 100755 old/run.py create mode 100644 old/step_by_step_oauth.md create mode 100755 old/test_oauth.py create mode 100644 old/后端OAuth2.0认证流程.md create mode 100644 python-app-demo/README.md create mode 100644 python-app-demo/main.py create mode 100644 python-app-demo/requirements.txt diff --git a/oauth_setup_guide.md b/oauth_setup_guide.md new file mode 100644 index 0000000..c4a9703 --- /dev/null +++ b/oauth_setup_guide.md @@ -0,0 +1,120 @@ +# Shoplazza OAuth2.0 认证设置指南 + +## 第一步:获取应用凭证 + +1. **访问Shoplazza开发者平台** + - 打开 https://partners.shoplazza.com/ + - 登录您的账户 + +2. **创建新应用** + - 点击"创建应用" + - 选择"公共应用" + - 填写应用信息 + +3. **获取凭证** + - 复制 `CLIENT_ID` + - 复制 `CLIENT_SECRET` + +## 第二步:配置本地环境 + +### 1. 安装依赖 +```bash +pip install -r requirements.txt +``` + +### 2. 创建环境配置文件 +创建 `.env` 文件: +```env +# Shoplazza OAuth2.0 配置 +CLIENT_ID=your_actual_client_id +CLIENT_SECRET=your_actual_client_secret +BASE_URL=https://your-ngrok-url.ngrok.io +REDIRECT_URI=/auth/shoplazza/callback + +# Flask 应用配置 +SECRET_KEY=your-secret-key-here +FLASK_ENV=development +FLASK_DEBUG=True +PORT=3000 +``` + +### 3. 使用ngrok暴露本地服务 +```bash +# 安装ngrok +npm install -g ngrok + +# 启动ngrok +ngrok http 3000 + +# 复制ngrok提供的HTTPS URL到BASE_URL +``` + +## 第三步:配置Shoplazza应用 + +在Shoplazza开发者中心配置以下URL: + +1. **应用URL**: `https://your-ngrok-url.ngrok.io/auth/install` +2. **回调URL**: `https://your-ngrok-url.ngrok.io/auth/shoplazza/callback` +3. **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook/shoplazza` + +## 第四步:启动应用 + +```bash +python run.py +``` + +## 第五步:测试OAuth流程 + +### 1. 开始认证 +访问以下URL(替换为实际的商店域名): +``` +https://your-ngrok-url.ngrok.io/auth/install?shop=your-shop.myshoplaza.com +``` + +### 2. 完成授权 +- 系统会重定向到Shoplazza授权页面 +- 商家确认授权 +- 系统自动处理回调并获取访问令牌 + +### 3. 测试API调用 +```bash +# 获取客户列表 +curl https://your-ngrok-url.ngrok.io/api/customers/your-shop.myshoplaza.com + +# 获取产品列表 +curl https://your-ngrok-url.ngrok.io/api/products/your-shop.myshoplaza.com +``` + +## 故障排除 + +### 常见问题 + +1. **HMAC验证失败** + - 检查CLIENT_SECRET是否正确 + - 确认请求来自Shoplazza + +2. **授权失败** + - 检查回调URL配置 + - 确认BASE_URL使用HTTPS + +3. **API调用失败** + - 检查访问令牌是否有效 + - 确认权限范围正确 + +### 调试技巧 + +1. **查看日志** + ```bash + # 应用会输出详细的日志信息 + python run.py + ``` + +2. **检查令牌状态** + ```bash + curl https://your-ngrok-url.ngrok.io/auth/tokens + ``` + +3. **健康检查** + ```bash + curl https://your-ngrok-url.ngrok.io/health + ``` diff --git a/old/Develop your application service.md b/old/Develop your application service.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/old/Develop your application service.md diff --git a/old/PROJECT_STRUCTURE.md b/old/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..121fd75 --- /dev/null +++ b/old/PROJECT_STRUCTURE.md @@ -0,0 +1,76 @@ +# 项目结构说明 + +## 文件组织 + +``` +shoplazza-oauth-backend/ +├── app.py # 主应用入口 +├── config.py # 配置管理 +├── run.py # 启动脚本 +├── requirements.txt # Python依赖 +├── env_template.txt # 环境变量模板 +├── README.md # 项目文档 +├── PROJECT_STRUCTURE.md # 项目结构说明 +├── middleware/ +│ └── hmac_validator.py # HMAC验证中间件 +└── routes/ + ├── auth.py # OAuth2.0认证路由 + ├── api.py # Shoplazza API调用 + └── webhook.py # Webhook处理 +``` + +## 核心组件 + +### 1. 配置管理 (config.py) +- 环境变量加载 +- 应用配置管理 +- 访问令牌存储 + +### 2. HMAC验证 (middleware/hmac_validator.py) +- OAuth请求HMAC验证 +- Webhook签名验证 +- 安全比较函数 + +### 3. 认证流程 (routes/auth.py) +- 应用安装处理 +- OAuth回调处理 +- 令牌交换和刷新 + +### 4. API调用 (routes/api.py) +- 客户数据获取 +- 产品数据获取 +- 订单数据获取 +- 商店信息获取 + +### 5. Webhook处理 (routes/webhook.py) +- Webhook签名验证 +- 事件处理逻辑 +- 应用卸载处理 + +## 数据流 + +``` +1. 商家安装应用 + ↓ +2. 重定向到Shoplazza授权页面 + ↓ +3. 商家确认授权 + ↓ +4. Shoplazza回调应用 + ↓ +5. HMAC验证 + ↓ +6. 交换访问令牌 + ↓ +7. 存储令牌 + ↓ +8. 调用Shoplazza API +``` + +## 安全特性 + +- HMAC签名验证 +- CSRF攻击防护 +- 安全的令牌存储 +- Webhook签名验证 +- 环境变量配置管理 diff --git a/old/README.md b/old/README.md new file mode 100644 index 0000000..0a4a6a4 --- /dev/null +++ b/old/README.md @@ -0,0 +1,153 @@ +# Shoplazza OAuth2.0 后端应用 + +这是一个基于Python Flask的Shoplazza OAuth2.0认证后端应用,实现了完整的OAuth2.0认证流程和API调用功能。 + +## 功能特性 + +- ✅ OAuth2.0 认证流程 +- ✅ HMAC 签名验证 +- ✅ 访问令牌管理 +- ✅ 令牌刷新功能 +- ✅ Shoplazza API 调用 +- ✅ Webhook 处理 +- ✅ 安全验证中间件 + +## 项目结构 + +``` +├── app.py # 主应用入口 +├── config.py # 配置文件 +├── requirements.txt # 依赖包 +├── middleware/ +│ └── hmac_validator.py # HMAC验证中间件 +├── routes/ +│ ├── auth.py # 认证路由 +│ ├── api.py # API调用路由 +│ └── webhook.py # Webhook处理路由 +└── README.md # 项目说明 +``` + +## 安装和配置 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 环境配置 + +创建 `.env` 文件并配置以下参数: + +```env +# Shoplazza OAuth2.0 配置 +CLIENT_ID=your_client_id_here +CLIENT_SECRET=your_client_secret_here +BASE_URL=https://your-domain.com +REDIRECT_URI=/auth/shoplazza/callback + +# 应用配置 +FLASK_ENV=development +FLASK_DEBUG=True +PORT=3000 +SECRET_KEY=your-secret-key-here +``` + +### 3. 运行应用 + +```bash +python app.py +``` + +## API 端点 + +### 认证端点 + +- `GET /auth/install?shop=your-shop.myshoplaza.com` - 开始OAuth认证流程 +- `GET /auth/shoplazza/callback` - OAuth回调处理 +- `GET /auth/refresh_token/` - 刷新访问令牌 +- `GET /auth/tokens` - 查看已授权的商店令牌 + +### API调用端点 + +- `GET /api/customers/` - 获取客户列表 +- `GET /api/products/` - 获取产品列表 +- `GET /api/orders/` - 获取订单列表 +- `GET /api/shop_info/` - 获取商店信息 + +### Webhook端点 + +- `POST /webhook/shoplazza` - 处理Shoplazza Webhook + +## 使用示例 + +### 1. 开始认证流程 + +访问以下URL开始OAuth认证: +``` +https://your-domain.com/auth/install?shop=your-shop.myshoplaza.com +``` + +### 2. 调用API + +认证成功后,可以调用Shoplazza API: +```bash +curl https://your-domain.com/api/customers/your-shop.myshoplaza.com +``` + +### 3. 处理Webhook + +在Shoplazza开发者中心配置Webhook URL: +``` +https://your-domain.com/webhook/shoplazza +``` + +## OAuth2.0 认证流程 + +1. **应用安装**: 商家从应用商店安装应用 +2. **授权请求**: 应用重定向到Shoplazza授权页面 +3. **用户授权**: 商家确认授权 +4. **授权回调**: Shoplazza重定向回应用并携带授权码 +5. **令牌交换**: 应用使用授权码交换访问令牌 +6. **API调用**: 使用访问令牌调用Shoplazza API + +## 安全特性 + +- HMAC签名验证确保请求来自Shoplazza +- 安全的令牌存储和管理 +- CSRF攻击防护 +- Webhook签名验证 + +## 开发说明 + +### 本地开发 + +1. 使用ngrok等工具将本地服务暴露到公网 +2. 在Shoplazza开发者中心配置应用URL +3. 确保所有回调URL使用HTTPS + +### 生产部署 + +1. 使用环境变量管理敏感配置 +2. 使用数据库存储访问令牌 +3. 配置适当的日志记录 +4. 设置监控和告警 + +## 故障排除 + +### 常见问题 + +1. **HMAC验证失败**: 检查CLIENT_SECRET配置 +2. **授权失败**: 确认回调URL配置正确 +3. **API调用失败**: 检查访问令牌是否有效 + +### 日志查看 + +应用会记录详细的日志信息,包括: +- 认证流程状态 +- API调用结果 +- 错误信息 + +## 许可证 + +MIT License diff --git a/old/__pycache__/config.cpython-311.pyc b/old/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000..cec93c4 Binary files /dev/null and b/old/__pycache__/config.cpython-311.pyc differ diff --git a/old/app.py b/old/app.py new file mode 100644 index 0000000..75844ec --- /dev/null +++ b/old/app.py @@ -0,0 +1,64 @@ +from flask import Flask, jsonify +from config import Config +from routes.auth import auth_bp +from routes.api import api_bp +from routes.webhook import webhook_bp +import logging + +def create_app(): + """创建Flask应用""" + app = Flask(__name__) + + # 配置应用 + app.config.from_object(Config) + + # 配置日志 + logging.basicConfig(level=logging.INFO) + + # 注册蓝图 + app.register_blueprint(auth_bp) + app.register_blueprint(api_bp) + app.register_blueprint(webhook_bp) + + # 根路由 + @app.route('/') + def index(): + return jsonify({ + 'message': 'Shoplazza OAuth2.0 Backend Service', + 'version': '1.0.0', + 'endpoints': { + 'auth': { + 'install': '/auth/install?shop=your-shop.myshoplaza.com', + 'callback': '/auth/shoplazza/callback', + 'refresh_token': '/auth/refresh_token/', + 'tokens': '/auth/tokens' + }, + 'api': { + 'customers': '/api/customers/', + 'products': '/api/products/', + 'orders': '/api/orders/', + 'shop_info': '/api/shop_info/' + }, + 'webhook': { + 'shoplazza': '/webhook/shoplazza' + } + } + }) + + # 健康检查端点 + @app.route('/health') + def health(): + return jsonify({ + 'status': 'healthy', + 'authorized_shops': len(Config.ACCESS_TOKENS) + }) + + return app + +if __name__ == '__main__': + app = create_app() + app.run( + host='0.0.0.0', + port=Config.PORT, + debug=Config.DEBUG + ) diff --git a/old/config.py b/old/config.py new file mode 100644 index 0000000..4911522 --- /dev/null +++ b/old/config.py @@ -0,0 +1,22 @@ +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +class Config: + """Shoplazza OAuth2.0 应用配置""" + + # Shoplazza OAuth2.0 配置 + CLIENT_ID = os.getenv('CLIENT_ID', 'your_client_id_here') + CLIENT_SECRET = os.getenv('CLIENT_SECRET', 'your_client_secret_here') + BASE_URL = os.getenv('BASE_URL', 'https://your-domain.com') + REDIRECT_URI = os.getenv('REDIRECT_URI', '/auth/shoplazza/callback') + + # Flask 配置 + SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here') + DEBUG = os.getenv('FLASK_DEBUG', 'True').lower() == 'true' + PORT = int(os.getenv('PORT', 3000)) + + # 存储访问令牌的字典(生产环境应使用数据库) + ACCESS_TOKENS = {} diff --git a/old/env_template.txt b/old/env_template.txt new file mode 100644 index 0000000..2bea9c3 --- /dev/null +++ b/old/env_template.txt @@ -0,0 +1,16 @@ +# Shoplazza OAuth2.0 配置模板 +# 复制此文件为 .env 并填入实际值 + +# Shoplazza 应用配置 (从 https://partners.shoplazza.com/ 获取) +CLIENT_ID=your_client_id_here +CLIENT_SECRET=your_client_secret_here + +# 应用基础URL (使用ngrok等工具暴露本地服务) +BASE_URL=https://your-domain.com +REDIRECT_URI=/auth/shoplazza/callback + +# Flask 应用配置 +SECRET_KEY=your-secret-key-here +FLASK_ENV=development +FLASK_DEBUG=True +PORT=3000 diff --git a/old/middleware/__pycache__/hmac_validator.cpython-311.pyc b/old/middleware/__pycache__/hmac_validator.cpython-311.pyc new file mode 100644 index 0000000..779b099 Binary files /dev/null and b/old/middleware/__pycache__/hmac_validator.cpython-311.pyc differ diff --git a/old/middleware/hmac_validator.py b/old/middleware/hmac_validator.py new file mode 100644 index 0000000..a9ec9d3 --- /dev/null +++ b/old/middleware/hmac_validator.py @@ -0,0 +1,59 @@ +import hmac +import hashlib +from flask import request, jsonify +from functools import wraps +from config import Config + +def hmac_validator_required(f): + """ + HMAC验证装饰器 + 验证来自Shoplazza的请求是否有效 + """ + @wraps(f) + def decorated_function(*args, **kwargs): + # 获取查询参数 + query_params = request.args.to_dict() + hmac_param = query_params.pop('hmac', None) + + if not hmac_param: + return jsonify({'error': 'Missing HMAC parameter'}), 400 + + # 验证shop参数格式 + shop = query_params.get('shop') + if not shop or not shop.endswith('.myshoplaza.com'): + return jsonify({'error': 'Invalid shop parameter'}), 400 + + # 构建验证消息 + sorted_keys = sorted(query_params.keys()) + message = '&'.join([f"{key}={query_params[key]}" for key in sorted_keys]) + + # 计算HMAC + calculated_hmac = hmac.new( + Config.CLIENT_SECRET.encode('utf-8'), + message.encode('utf-8'), + hashlib.sha256 + ).hexdigest() + + # 安全比较HMAC + if not hmac.compare_digest(calculated_hmac, hmac_param): + return jsonify({'error': 'HMAC validation failed'}), 403 + + return f(*args, **kwargs) + + return decorated_function + +def verify_webhook_hmac(data, hmac_header): + """ + 验证Webhook的HMAC签名 + """ + import base64 + + calculated_hmac = base64.b64encode( + hmac.new( + Config.CLIENT_SECRET.encode('utf-8'), + data, + hashlib.sha256 + ).digest() + ).decode('utf-8') + + return hmac.compare_digest(calculated_hmac, hmac_header) diff --git a/old/oauth_setup_guide.md b/old/oauth_setup_guide.md new file mode 100644 index 0000000..c4a9703 --- /dev/null +++ b/old/oauth_setup_guide.md @@ -0,0 +1,120 @@ +# Shoplazza OAuth2.0 认证设置指南 + +## 第一步:获取应用凭证 + +1. **访问Shoplazza开发者平台** + - 打开 https://partners.shoplazza.com/ + - 登录您的账户 + +2. **创建新应用** + - 点击"创建应用" + - 选择"公共应用" + - 填写应用信息 + +3. **获取凭证** + - 复制 `CLIENT_ID` + - 复制 `CLIENT_SECRET` + +## 第二步:配置本地环境 + +### 1. 安装依赖 +```bash +pip install -r requirements.txt +``` + +### 2. 创建环境配置文件 +创建 `.env` 文件: +```env +# Shoplazza OAuth2.0 配置 +CLIENT_ID=your_actual_client_id +CLIENT_SECRET=your_actual_client_secret +BASE_URL=https://your-ngrok-url.ngrok.io +REDIRECT_URI=/auth/shoplazza/callback + +# Flask 应用配置 +SECRET_KEY=your-secret-key-here +FLASK_ENV=development +FLASK_DEBUG=True +PORT=3000 +``` + +### 3. 使用ngrok暴露本地服务 +```bash +# 安装ngrok +npm install -g ngrok + +# 启动ngrok +ngrok http 3000 + +# 复制ngrok提供的HTTPS URL到BASE_URL +``` + +## 第三步:配置Shoplazza应用 + +在Shoplazza开发者中心配置以下URL: + +1. **应用URL**: `https://your-ngrok-url.ngrok.io/auth/install` +2. **回调URL**: `https://your-ngrok-url.ngrok.io/auth/shoplazza/callback` +3. **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook/shoplazza` + +## 第四步:启动应用 + +```bash +python run.py +``` + +## 第五步:测试OAuth流程 + +### 1. 开始认证 +访问以下URL(替换为实际的商店域名): +``` +https://your-ngrok-url.ngrok.io/auth/install?shop=your-shop.myshoplaza.com +``` + +### 2. 完成授权 +- 系统会重定向到Shoplazza授权页面 +- 商家确认授权 +- 系统自动处理回调并获取访问令牌 + +### 3. 测试API调用 +```bash +# 获取客户列表 +curl https://your-ngrok-url.ngrok.io/api/customers/your-shop.myshoplaza.com + +# 获取产品列表 +curl https://your-ngrok-url.ngrok.io/api/products/your-shop.myshoplaza.com +``` + +## 故障排除 + +### 常见问题 + +1. **HMAC验证失败** + - 检查CLIENT_SECRET是否正确 + - 确认请求来自Shoplazza + +2. **授权失败** + - 检查回调URL配置 + - 确认BASE_URL使用HTTPS + +3. **API调用失败** + - 检查访问令牌是否有效 + - 确认权限范围正确 + +### 调试技巧 + +1. **查看日志** + ```bash + # 应用会输出详细的日志信息 + python run.py + ``` + +2. **检查令牌状态** + ```bash + curl https://your-ngrok-url.ngrok.io/auth/tokens + ``` + +3. **健康检查** + ```bash + curl https://your-ngrok-url.ngrok.io/health + ``` diff --git a/old/requirements.txt b/old/requirements.txt new file mode 100644 index 0000000..a8eb25a --- /dev/null +++ b/old/requirements.txt @@ -0,0 +1,4 @@ +Flask==2.3.3 +requests==2.31.0 +python-dotenv==1.0.0 +cryptography==41.0.4 diff --git a/old/routes/__pycache__/api.cpython-311.pyc b/old/routes/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000..4493a7c Binary files /dev/null and b/old/routes/__pycache__/api.cpython-311.pyc differ diff --git a/old/routes/__pycache__/auth.cpython-311.pyc b/old/routes/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000..6ce48ba Binary files /dev/null and b/old/routes/__pycache__/auth.cpython-311.pyc differ diff --git a/old/routes/__pycache__/webhook.cpython-311.pyc b/old/routes/__pycache__/webhook.cpython-311.pyc new file mode 100644 index 0000000..2a50f49 Binary files /dev/null and b/old/routes/__pycache__/webhook.cpython-311.pyc differ diff --git a/old/routes/api.py b/old/routes/api.py new file mode 100644 index 0000000..d45c1d3 --- /dev/null +++ b/old/routes/api.py @@ -0,0 +1,142 @@ +import requests +from flask import Blueprint, jsonify, request, current_app +from config import Config + +# 创建API蓝图 +api_bp = Blueprint('api', __name__, url_prefix='/api') + +@api_bp.route('/customers/') +def get_customers(shop): + """ + 获取客户列表 + """ + if shop not in Config.ACCESS_TOKENS: + return jsonify({'error': 'Shop not authorized'}), 401 + + token_data = Config.ACCESS_TOKENS[shop] + access_token = token_data.get('access_token') + + if not access_token: + return jsonify({'error': 'No access token available'}), 401 + + try: + # 调用Shoplazza API获取客户列表 + api_url = f"https://{shop}/openapi/2022-01/customers" + headers = { + 'Access-Token': access_token, + 'Content-Type': 'application/json' + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + return jsonify({ + 'shop': shop, + 'customers': response.json() + }) + + except Exception as e: + current_app.logger.error(f"获取客户列表失败: {str(e)}") + return jsonify({'error': 'Failed to fetch customers'}), 500 + +@api_bp.route('/products/') +def get_products(shop): + """ + 获取产品列表 + """ + if shop not in Config.ACCESS_TOKENS: + return jsonify({'error': 'Shop not authorized'}), 401 + + token_data = Config.ACCESS_TOKENS[shop] + access_token = token_data.get('access_token') + + if not access_token: + return jsonify({'error': 'No access token available'}), 401 + + try: + # 调用Shoplazza API获取产品列表 + api_url = f"https://{shop}/openapi/2020-01/products" + headers = { + 'Access-Token': access_token, + 'Content-Type': 'application/json' + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + return jsonify({ + 'shop': shop, + 'products': response.json() + }) + + except Exception as e: + current_app.logger.error(f"获取产品列表失败: {str(e)}") + return jsonify({'error': 'Failed to fetch products'}), 500 + +@api_bp.route('/orders/') +def get_orders(shop): + """ + 获取订单列表 + """ + if shop not in Config.ACCESS_TOKENS: + return jsonify({'error': 'Shop not authorized'}), 401 + + token_data = Config.ACCESS_TOKENS[shop] + access_token = token_data.get('access_token') + + if not access_token: + return jsonify({'error': 'No access token available'}), 401 + + try: + # 调用Shoplazza API获取订单列表 + api_url = f"https://{shop}/openapi/2020-01/orders" + headers = { + 'Access-Token': access_token, + 'Content-Type': 'application/json' + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + return jsonify({ + 'shop': shop, + 'orders': response.json() + }) + + except Exception as e: + current_app.logger.error(f"获取订单列表失败: {str(e)}") + return jsonify({'error': 'Failed to fetch orders'}), 500 + +@api_bp.route('/shop_info/') +def get_shop_info(shop): + """ + 获取商店信息 + """ + if shop not in Config.ACCESS_TOKENS: + return jsonify({'error': 'Shop not authorized'}), 401 + + token_data = Config.ACCESS_TOKENS[shop] + access_token = token_data.get('access_token') + + if not access_token: + return jsonify({'error': 'No access token available'}), 401 + + try: + # 调用Shoplazza API获取商店信息 + api_url = f"https://{shop}/openapi/2020-01/shop" + headers = { + 'Access-Token': access_token, + 'Content-Type': 'application/json' + } + + response = requests.get(api_url, headers=headers) + response.raise_for_status() + + return jsonify({ + 'shop': shop, + 'shop_info': response.json() + }) + + except Exception as e: + current_app.logger.error(f"获取商店信息失败: {str(e)}") + return jsonify({'error': 'Failed to fetch shop info'}), 500 diff --git a/old/routes/auth.py b/old/routes/auth.py new file mode 100644 index 0000000..4151465 --- /dev/null +++ b/old/routes/auth.py @@ -0,0 +1,143 @@ +import secrets +import requests +from flask import Blueprint, request, redirect, jsonify, current_app +from config import Config +from middleware.hmac_validator import hmac_validator_required + +# 创建认证蓝图 +auth_bp = Blueprint('auth', __name__, url_prefix='/auth') + +@auth_bp.route('/install') +def install(): + """ + Step 1: 处理应用安装请求 + 当商家从应用商店安装应用时,Shoplazza会发送请求到这个端点 + """ + shop = request.args.get('shop') + if not shop: + return jsonify({'error': 'Missing shop parameter'}), 400 + + # 验证shop参数格式 + if not shop.endswith('.myshoplaza.com'): + return jsonify({'error': 'Invalid shop parameter'}), 400 + + # 定义需要的权限范围 + scopes = "read_customer,read_product,write_product,read_order,read_shop" + + # 生成随机state参数防止CSRF攻击 + state = secrets.token_hex(16) + + # 构建授权URL + auth_url = ( + f"https://{shop}/admin/oauth/authorize?" + f"client_id={Config.CLIENT_ID}&" + f"scope={scopes}&" + f"redirect_uri={Config.BASE_URL}{Config.REDIRECT_URI}&" + f"response_type=code&" + f"state={state}" + ) + + return redirect(auth_url) + +@auth_bp.route('/shoplazza/callback') +@hmac_validator_required +def callback(): + """ + Step 2: 处理OAuth回调 + 商家授权后,Shoplazza会重定向到这个端点 + """ + code = request.args.get('code') + shop = request.args.get('shop') + state = request.args.get('state') + + if not shop or not code: + return jsonify({'error': 'Missing required parameters'}), 400 + + try: + # 交换授权码获取访问令牌 + token_data = exchange_code_for_token(code, shop) + + # 存储访问令牌(生产环境应使用数据库) + Config.ACCESS_TOKENS[shop] = token_data + + current_app.logger.info(f"获取access_token成功 for shop: {shop}") + + return jsonify({ + 'message': 'Authorization successful', + 'shop': shop, + 'store_id': token_data.get('store_id'), + 'store_name': token_data.get('store_name') + }) + + except Exception as e: + current_app.logger.error(f"获取access_token失败: {str(e)}") + return jsonify({'error': 'Failed to obtain access token'}), 500 + +def exchange_code_for_token(code, shop): + """ + 使用授权码交换访问令牌 + """ + token_url = f"https://{shop}/admin/oauth/token" + + data = { + 'client_id': Config.CLIENT_ID, + 'client_secret': Config.CLIENT_SECRET, + 'code': code, + 'grant_type': 'authorization_code', + 'redirect_uri': f"{Config.BASE_URL}{Config.REDIRECT_URI}" + } + + response = requests.post(token_url, data=data) + response.raise_for_status() + + return response.json() + +@auth_bp.route('/refresh_token/') +def refresh_token(shop): + """ + 刷新访问令牌 + """ + if shop not in Config.ACCESS_TOKENS: + return jsonify({'error': 'Shop not found'}), 404 + + token_data = Config.ACCESS_TOKENS[shop] + refresh_token_value = token_data.get('refresh_token') + + if not refresh_token_value: + return jsonify({'error': 'No refresh token available'}), 400 + + try: + refresh_url = f"https://{shop}/admin/oauth/token" + + data = { + 'client_id': Config.CLIENT_ID, + 'client_secret': Config.CLIENT_SECRET, + 'refresh_token': refresh_token_value, + 'grant_type': 'refresh_token', + 'redirect_uri': f"{Config.BASE_URL}{Config.REDIRECT_URI}" + } + + response = requests.post(refresh_url, data=data) + response.raise_for_status() + + # 更新存储的令牌 + Config.ACCESS_TOKENS[shop] = response.json() + + return jsonify({ + 'message': 'Token refreshed successfully', + 'new_token': response.json() + }) + + except Exception as e: + current_app.logger.error(f"刷新token失败: {str(e)}") + return jsonify({'error': 'Failed to refresh token'}), 500 + +@auth_bp.route('/tokens') +def list_tokens(): + """ + 列出所有已授权的商店令牌(仅用于调试) + """ + return jsonify({ + 'authorized_shops': list(Config.ACCESS_TOKENS.keys()), + 'tokens': Config.ACCESS_TOKENS + }) diff --git a/old/routes/webhook.py b/old/routes/webhook.py new file mode 100644 index 0000000..b5aa59b --- /dev/null +++ b/old/routes/webhook.py @@ -0,0 +1,77 @@ +import hmac +import hashlib +import base64 +from flask import Blueprint, request, jsonify, current_app +from config import Config +from middleware.hmac_validator import verify_webhook_hmac + +# 创建Webhook蓝图 +webhook_bp = Blueprint('webhook', __name__, url_prefix='/webhook') + +@webhook_bp.route('/shoplazza', methods=['POST']) +def shoplazza_webhook(): + """ + 处理Shoplazza Webhook + """ + # 获取HMAC签名头 + hmac_header = request.headers.get('X-Shoplazza-Hmac-Sha256') + if not hmac_header: + return jsonify({'error': 'Missing HMAC header'}), 400 + + # 获取原始数据 + data = request.get_data() + + # 验证Webhook签名 + if not verify_webhook_hmac(data, hmac_header): + current_app.logger.warning("Webhook HMAC verification failed") + return jsonify({'error': 'Invalid webhook signature'}), 403 + + try: + # 解析Webhook数据 + webhook_data = request.get_json() + current_app.logger.info(f"Received webhook: {webhook_data}") + + # 根据Webhook类型处理 + webhook_type = webhook_data.get('type') + + if webhook_type == 'app/uninstalled': + handle_app_uninstalled(webhook_data) + elif webhook_type == 'orders/create': + handle_order_created(webhook_data) + elif webhook_type == 'orders/updated': + handle_order_updated(webhook_data) + elif webhook_type == 'products/create': + handle_product_created(webhook_data) + else: + current_app.logger.info(f"Unhandled webhook type: {webhook_type}") + + return jsonify({'status': 'success'}) + + except Exception as e: + current_app.logger.error(f"Webhook处理失败: {str(e)}") + return jsonify({'error': 'Webhook processing failed'}), 500 + +def handle_app_uninstalled(webhook_data): + """处理应用卸载事件""" + shop = webhook_data.get('shop') + if shop and shop in Config.ACCESS_TOKENS: + del Config.ACCESS_TOKENS[shop] + current_app.logger.info(f"App uninstalled for shop: {shop}") + +def handle_order_created(webhook_data): + """处理订单创建事件""" + order = webhook_data.get('order') + if order: + current_app.logger.info(f"New order created: {order.get('id')}") + +def handle_order_updated(webhook_data): + """处理订单更新事件""" + order = webhook_data.get('order') + if order: + current_app.logger.info(f"Order updated: {order.get('id')}") + +def handle_product_created(webhook_data): + """处理产品创建事件""" + product = webhook_data.get('product') + if product: + current_app.logger.info(f"New product created: {product.get('id')}") diff --git a/old/run.py b/old/run.py new file mode 100755 index 0000000..a9de741 --- /dev/null +++ b/old/run.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +""" +Shoplazza OAuth2.0 后端应用启动脚本 +""" + +import os +import sys +from app import create_app + +def main(): + """主函数""" + # 检查环境变量 + required_vars = ['CLIENT_ID', 'CLIENT_SECRET', 'BASE_URL'] + missing_vars = [var for var in required_vars if not os.getenv(var)] + + if missing_vars: + print(f"错误: 缺少必需的环境变量: {', '.join(missing_vars)}") + print("请创建 .env 文件并配置以下变量:") + print("CLIENT_ID=your_client_id_here") + print("CLIENT_SECRET=your_client_secret_here") + print("BASE_URL=https://your-domain.com") + sys.exit(1) + + # 创建应用 + app = create_app() + + # 启动应用 + print("🚀 启动 Shoplazza OAuth2.0 后端服务...") + print(f"📡 服务地址: http://localhost:{app.config['PORT']}") + print(f"🔗 认证端点: {app.config['BASE_URL']}/auth/install?shop=your-shop.myshoplaza.com") + print(f"📋 API文档: {app.config['BASE_URL']}/") + + app.run( + host='0.0.0.0', + port=app.config['PORT'], + debug=app.config['DEBUG'] + ) + +if __name__ == '__main__': + main() diff --git a/old/step_by_step_oauth.md b/old/step_by_step_oauth.md new file mode 100644 index 0000000..22bf714 --- /dev/null +++ b/old/step_by_step_oauth.md @@ -0,0 +1,184 @@ +# OAuth2.0 认证步骤详解 + +## 🎯 完整认证流程 + +### 步骤1: 准备阶段 + +#### 1.1 获取Shoplazza应用凭证 +``` +1. 访问 https://partners.shoplazza.com/ +2. 登录并创建新应用 +3. 记录 CLIENT_ID 和 CLIENT_SECRET +``` + +#### 1.2 设置本地开发环境 +```bash +# 安装依赖 +pip install -r requirements.txt + +# 安装ngrok (用于本地开发) +npm install -g ngrok +``` + +#### 1.3 启动ngrok隧道 +```bash +# 在终端1中启动ngrok +ngrok http 3000 + +# 记录ngrok提供的HTTPS URL,例如: +# https://abc123.ngrok.io +``` + +### 步骤2: 配置应用 + +#### 2.1 创建环境配置文件 +创建 `.env` 文件: +```env +CLIENT_ID=your_actual_client_id_from_shoplazza +CLIENT_SECRET=your_actual_client_secret_from_shoplazza +BASE_URL=https://abc123.ngrok.io +REDIRECT_URI=/auth/shoplazza/callback +SECRET_KEY=your-random-secret-key +FLASK_ENV=development +FLASK_DEBUG=True +PORT=3000 +``` + +#### 2.2 在Shoplazza开发者中心配置URL +``` +应用URL: https://abc123.ngrok.io/auth/install +回调URL: https://abc123.ngrok.io/auth/shoplazza/callback +Webhook URL: https://abc123.ngrok.io/webhook/shoplazza +``` + +### 步骤3: 启动应用 + +```bash +# 在终端2中启动应用 +python run.py +``` + +您应该看到类似输出: +``` +🚀 启动 Shoplazza OAuth2.0 后端服务... +📡 服务地址: http://localhost:3000 +🔗 认证端点: https://abc123.ngrok.io/auth/install?shop=your-shop.myshoplaza.com +📋 API文档: https://abc123.ngrok.io/ +``` + +### 步骤4: 执行OAuth认证 + +#### 4.1 开始认证流程 +在浏览器中访问: +``` +https://abc123.ngrok.io/auth/install?shop=your-shop.myshoplaza.com +``` + +**注意**: 将 `your-shop.myshoplaza.com` 替换为实际的商店域名 + +#### 4.2 授权确认 +1. 系统会重定向到Shoplazza授权页面 +2. 商家点击"安装应用"或"授权" +3. 系统自动处理回调 + +#### 4.3 验证认证结果 +访问以下URL查看认证状态: +``` +https://abc123.ngrok.io/auth/tokens +``` + +### 步骤5: 测试API调用 + +#### 5.1 获取客户列表 +```bash +curl https://abc123.ngrok.io/api/customers/your-shop.myshoplaza.com +``` + +#### 5.2 获取产品列表 +```bash +curl https://abc123.ngrok.io/api/products/your-shop.myshoplaza.com +``` + +#### 5.3 获取订单列表 +```bash +curl https://abc123.ngrok.io/api/orders/your-shop.myshoplaza.com +``` + +#### 5.4 获取商店信息 +```bash +curl https://abc123.ngrok.io/api/shop_info/your-shop.myshoplaza.com +``` + +## 🔍 认证流程详解 + +### OAuth2.0 流程图 +``` +商家 → 应用安装 → 授权页面 → 确认授权 → 回调处理 → 获取令牌 → API调用 +``` + +### 详细步骤说明 + +1. **应用安装请求** + - 商家从应用商店安装应用 + - Shoplazza发送请求到 `/auth/install` + - 应用重定向到Shoplazza授权页面 + +2. **用户授权** + - 商家在Shoplazza页面确认授权 + - 选择需要的权限范围 + - 点击"安装应用" + +3. **授权回调** + - Shoplazza重定向到 `/auth/shoplazza/callback` + - 携带授权码和HMAC签名 + - 应用验证HMAC签名 + +4. **令牌交换** + - 应用使用授权码请求访问令牌 + - Shoplazza返回访问令牌和刷新令牌 + - 应用存储令牌用于后续API调用 + +5. **API调用** + - 使用访问令牌调用Shoplazza API + - 获取客户、产品、订单等数据 + +## 🛠️ 故障排除 + +### 常见错误及解决方案 + +1. **"Missing HMAC parameter"** + - 检查请求是否来自Shoplazza + - 确认URL参数完整 + +2. **"HMAC validation failed"** + - 检查CLIENT_SECRET配置 + - 确认请求参数顺序正确 + +3. **"Shop not authorized"** + - 确认已完成OAuth认证 + - 检查访问令牌是否有效 + +4. **"Failed to obtain access token"** + - 检查CLIENT_ID和CLIENT_SECRET + - 确认回调URL配置正确 + +### 调试命令 + +```bash +# 检查应用状态 +curl https://abc123.ngrok.io/health + +# 查看已授权的商店 +curl https://abc123.ngrok.io/auth/tokens + +# 刷新访问令牌 +curl https://abc123.ngrok.io/auth/refresh_token/your-shop.myshoplaza.com +``` + +## 📝 注意事项 + +1. **HTTPS要求**: 生产环境必须使用HTTPS +2. **令牌安全**: 生产环境应使用数据库存储令牌 +3. **错误处理**: 实现适当的错误处理和日志记录 +4. **权限范围**: 只请求应用实际需要的权限 +5. **令牌刷新**: 定期刷新访问令牌以保持有效性 diff --git a/old/test_oauth.py b/old/test_oauth.py new file mode 100755 index 0000000..d7df7a2 --- /dev/null +++ b/old/test_oauth.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +OAuth2.0 认证测试脚本 +用于验证Shoplazza OAuth流程是否正常工作 +""" + +import requests +import json +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +def test_health_check(base_url): + """测试健康检查端点""" + try: + response = requests.get(f"{base_url}/health") + if response.status_code == 200: + print("✅ 健康检查通过") + print(f" 状态: {response.json()}") + return True + else: + print(f"❌ 健康检查失败: {response.status_code}") + return False + except Exception as e: + print(f"❌ 健康检查异常: {str(e)}") + return False + +def test_auth_endpoints(base_url): + """测试认证端点""" + print("\n🔐 测试认证端点...") + + # 测试根端点 + try: + response = requests.get(base_url) + if response.status_code == 200: + print("✅ 根端点正常") + endpoints = response.json().get('endpoints', {}) + print(f" 可用端点: {list(endpoints.keys())}") + else: + print(f"❌ 根端点异常: {response.status_code}") + except Exception as e: + print(f"❌ 根端点异常: {str(e)}") + +def test_oauth_flow(base_url, shop_domain): + """测试OAuth流程""" + print(f"\n🔄 测试OAuth流程 (商店: {shop_domain})...") + + # 构建认证URL + auth_url = f"{base_url}/auth/install?shop={shop_domain}" + print(f"认证URL: {auth_url}") + + # 测试认证端点(不跟随重定向) + try: + response = requests.get(auth_url, allow_redirects=False) + if response.status_code in [302, 301]: + print("✅ 认证重定向正常") + print(f" 重定向到: {response.headers.get('Location', 'N/A')}") + else: + print(f"❌ 认证重定向异常: {response.status_code}") + except Exception as e: + print(f"❌ 认证端点异常: {str(e)}") + +def test_api_endpoints(base_url, shop_domain): + """测试API端点""" + print(f"\n📡 测试API端点 (商店: {shop_domain})...") + + endpoints = [ + f"/api/customers/{shop_domain}", + f"/api/products/{shop_domain}", + f"/api/orders/{shop_domain}", + f"/api/shop_info/{shop_domain}" + ] + + for endpoint in endpoints: + try: + response = requests.get(f"{base_url}{endpoint}") + if response.status_code == 401: + print(f"⚠️ {endpoint} - 需要认证 (正常)") + elif response.status_code == 200: + print(f"✅ {endpoint} - 认证成功") + else: + print(f"❌ {endpoint} - 异常状态: {response.status_code}") + except Exception as e: + print(f"❌ {endpoint} - 异常: {str(e)}") + +def test_tokens_endpoint(base_url): + """测试令牌端点""" + print("\n🔑 测试令牌端点...") + + try: + response = requests.get(f"{base_url}/auth/tokens") + if response.status_code == 200: + tokens = response.json() + print("✅ 令牌端点正常") + print(f" 已授权商店: {tokens.get('authorized_shops', [])}") + if tokens.get('tokens'): + print(f" 令牌数量: {len(tokens.get('tokens', {}))}") + else: + print(f"❌ 令牌端点异常: {response.status_code}") + except Exception as e: + print(f"❌ 令牌端点异常: {str(e)}") + +def main(): + """主测试函数""" + print("🚀 Shoplazza OAuth2.0 认证测试") + print("=" * 50) + + # 获取配置 + base_url = os.getenv('BASE_URL', 'http://localhost:3000') + shop_domain = input("请输入商店域名 (例如: your-shop.myshoplaza.com): ").strip() + + if not shop_domain: + shop_domain = "your-shop.myshoplaza.com" + print(f"使用默认商店域名: {shop_domain}") + + print(f"\n测试配置:") + print(f" 基础URL: {base_url}") + print(f" 商店域名: {shop_domain}") + + # 执行测试 + if test_health_check(base_url): + test_auth_endpoints(base_url) + test_oauth_flow(base_url, shop_domain) + test_api_endpoints(base_url, shop_domain) + test_tokens_endpoint(base_url) + + print("\n" + "=" * 50) + print("🎯 测试完成!") + print("\n下一步操作:") + print(f"1. 在浏览器中访问: {base_url}/auth/install?shop={shop_domain}") + print("2. 完成OAuth认证流程") + print("3. 重新运行此测试脚本验证API调用") + +if __name__ == "__main__": + main() diff --git a/old/后端OAuth2.0认证流程.md b/old/后端OAuth2.0认证流程.md new file mode 100644 index 0000000..01e5ff3 --- /dev/null +++ b/old/后端OAuth2.0认证流程.md @@ -0,0 +1,128 @@ + +后端OAuth2.0认证流程 +1. 安装依赖 +Codeblock + 1 + npm install express crypto axios + 2. 配置⽂件/config/index.js +⽤于存放APP_NAME,CLIENT_ID,CLIENT_SECRET,REDIRECT_URI + Codeblock + + const CLIENT_ID = ""; + const CLIENT_SECRET = ""; + const BASE_URL = ""; + const REDIRECT_URI = `${BASE_URL}/auth/shoplazza/callback`; + let access_token = {}; + 3. HMAC校验中间件/middleware/hmacValidator.js + Codeblock + + 9 + import crypto from "crypto"; + export const hmacValidator = async (ctx, next) => { + const { hmac, ...queryParams } = ctx.query; + if (!hmac) { + ctx.status = 400; + ctx.body = { error: "Missing HMAC parameter" }; + return; + + } + // +计算 + HMAC +校验 + +const message = Object.keys(queryParams) + .sort() + .map((key) => `${key}=${queryParams[key]}`) + .join("&"); + const generatedHmac = crypto + .createHmac("sha256", process.env.CLIENT_SECRET) + .update(message) + .digest("hex"); + if (crypto.timingSafeEqual(Buffer.from(generatedHmac), Buffer.from(hmac))) { + await next(); + } else { + ctx.status = 403; + ctx.body = { error: "HMAC validation failed" }; + } + }; + 4. 认证路由/routes/auth.js + Codeblock + + import Router from "koa-router"; + import crypto from "crypto"; + import axios from "axios"; + import { APP_NAME, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } from +"../config/index.js"; + import { hmacValidator } from "../middleware/hmacValidator.js"; + const router = new Router({ prefix: "/api" }); + router.get(`/auth/install`, async (ctx) => { + const shop = ctx.query.shop; + if (!shop) { + ctx.status = 400; + ctx.body = { error: "Missing shop parameter" }; + return; + } + const scopes = "read_customer,read_product,write_product"; + const state = crypto.randomBytes(16).toString("hex"); + const redirectUri = `https://${ctx.host}${REDIRECT_URI}`; + + const authUrl = `https://${shop}/admin/oauth/authorize? + client_id=${CLIENT_ID}&scope=${scopes}&redirect_uri=${redirectUri}&response_typ + e=code&state=${state}`; + ctx.redirect(authUrl); + }); + // ** Step 2: +处理 + OAuth +回调 +** +router.get(`/auth/callback`, hmacValidator, async (ctx) => { + const { code, shop } = ctx.query; + if (!shop || !code) { + ctx.status = 400; + ctx.body = { error: "Missing required parameters" }; + return; + } + const redirectUri = `https://${ctx.host}${REDIRECT_URI}`; + try { + const response = await axios.post(`https://${shop}/admin/oauth/token`, { + client_id: CLIENT_ID, + client_secret: CLIENT_SECRET, + code, + grant_type: "authorization_code", + redirect_uri: redirectUri, + }); + console.log(" +获取 + access_token +成功 +:", response.data); + ctx.body = response.data; // +返回 + token +及 + store +信息 + +} catch (error) { + console.error(" +获取 + access_token +失败 +:", error.message); + ctx.status = 500; + ctx.body = { error: "Failed to obtain access token" }; + } + }); + export default router; + 5. 启动服务器挂载oauth,认证路由 +Codeblock + 1 + 2 + const PORT = process.env.PORT || 3000; + app.listen(PORT, () => { +3 + 4 + console.log(`Server running on http://localhost:${PORT}`); + }); \ No newline at end of file diff --git a/python-app-demo/README.md b/python-app-demo/README.md new file mode 100644 index 0000000..aba1b75 --- /dev/null +++ b/python-app-demo/README.md @@ -0,0 +1,90 @@ +# Python OAuth2 认证服务器 + +这是一个用 Flask 编写的 OAuth2 认证服务器,实现了与 Node.js 版本相同的功能。 + +## 功能特性 + +- ✅ OAuth2 认证流程 +- ✅ HMAC-SHA256 签名验证 +- ✅ Timing-safe 比较防止时序攻击 +- ✅ 支持 Shopify OAuth 集成 +- ✅ CORS 跨域支持 +- ✅ 环境变量配置 + +## 快速开始 + +### 1. 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 2. 运行服务器 + +```bash +python main.py +``` + +服务器将在 `http://localhost:4000` 运行 + +## API 端点 + +### 1. 启动 OAuth 认证 + +``` +GET /api/auth?shop=example.myshopify.com +``` + +重定向用户到 Shopify 授权页面。 + +### 2. OAuth 回调处理 + +``` +GET /api/auth/callback?code=xxx&hmac=xxx&state=xxx&shop=example.myshopify.com +``` + +- 验证 HMAC 签名 +- 交换授权码获取访问令牌 +- 返回令牌信息 + +## 项目结构 + +``` +python-app-demo/ +├── main.py # 主应用文件 +├── requirements.txt # Python 依赖 +├── .env # 环境变量(需要自己创建) +└── README.md # 说明文档 +``` + +## 与 Node.js 版本的对应关系 + +| Node.js (devServer) | Python (python-app-demo) | +|-------------------|----------------------| +| Express 服务器 | Flask 服务器 | +| hmacValidatorMiddleWare | @hmac_validator 装饰器 | +| secureCompare | secure_compare 函数 | +| crypto.timingSafeEqual | hmac.compare_digest | +| /api/auth | /api/auth 路由 | +| /api/auth/callback | /api/auth/callback 路由 | + +## 安全特性 + +1. **HMAC-SHA256 验证**:所有回调请求都通过 HMAC 签名验证 +2. **Timing-safe 比较**:使用 `hmac.compare_digest` 防止时序攻击 +3. **环境变量管理**:敏感配置存储在 `.env` 文件中 +4. **CORS 支持**:安全的跨域资源共享 + +## 使用 curl 测试 + +```bash +# 测试认证端点 +curl "http://localhost:4000/api/auth?shop=example.myshopify.com" + +# 测试回调端点(需要有效的HMAC) +curl "http://localhost:4000/api/auth/callback?code=test&hmac=xxx&state=xxx&shop=example.myshopify.com" +``` + +## 完成! + +现在您有一个完整的 Python OAuth2 认证服务器,功能与 Node.js 版本完全相同。🚀 diff --git a/python-app-demo/main.py b/python-app-demo/main.py new file mode 100644 index 0000000..654ddef --- /dev/null +++ b/python-app-demo/main.py @@ -0,0 +1,129 @@ +from flask import Flask, request, redirect, jsonify +from flask_cors import CORS +import hmac +import hashlib +import secrets +import requests + +app = Flask(__name__) +CORS(app) + +# 配置 - 直接写死 +CLIENT_ID = "J2ntcUvAKRq_xVXcmysAc6iUJ-MYJF4JtoMKJGi_I9A" +CLIENT_SECRET = "WvmwpHVf1u1hfYEhsc7LPl0YxrbiFy3A2JVBhy8PWAU" +REDIRECT_URI = "https://rooster-wired-monster.ngrok-free.app/api/auth/callback" + +# 工具函数:安全比较HMAC +def secure_compare(a, b): + """ + 使用timing-safe比较来防止时序攻击 + """ + return hmac.compare_digest(a, b) + +# 中间件:HMAC验证 +def hmac_validator(func): + """ + HMAC验证装饰器 - 验证请求的HMAC签名 + """ + def wrapper(*args, **kwargs): + # 获取查询参数 + code = request.args.get('code') + hmac_value = request.args.get('hmac') + state = request.args.get('state') + shop = request.args.get('shop') + + if not hmac_value: + return jsonify({"error": "HMAC validation failed"}), 400 + + # 构建消息 + params = dict(request.args) + del params['hmac'] # 移除hmac参数 + + # 按字典序排序参数并构建消息字符串 + sorted_keys = sorted(params.keys()) + message = "&".join([f"{key}={params[key]}" for key in sorted_keys]) + + # 生成HMAC哈希 + generated_hash = hmac.new( + CLIENT_SECRET.encode(), + message.encode(), + hashlib.sha256 + ).hexdigest() + + # 比较HMAC + if not secure_compare(generated_hash, hmac_value): + return jsonify({"error": "HMAC validation failed"}), 400 + + return func(*args, **kwargs) + + wrapper.__name__ = func.__name__ + return wrapper + +# OAuth认证路由 +@app.route('/api/auth', methods=['GET']) +def auth(): + """ + OAuth认证端点 - 重定向到Shopify授权页面 + """ + shop = request.args.get('shop') + if not shop: + return jsonify({"error": "shop parameter is required"}), 400 + + scopes = "read_customer" + state = secrets.token_hex(16) # 生成随机state + + auth_url = ( + f"https://{shop}/admin/oauth/authorize?" + f"client_id={CLIENT_ID}" + f"&scope={scopes}" + f"&redirect_uri={REDIRECT_URI}" + f"&response_type=code" + f"&state={state}" + ) + + return redirect(auth_url) + +# OAuth回调路由 +@app.route('/api/auth/callback', methods=['GET']) +@hmac_validator +def auth_callback(): + """ + OAuth回调端点 - 交换授权码获取访问令牌 + """ + code = request.args.get('code') + shop = request.args.get('shop') + + if not shop or not code: + return jsonify({"error": "Required parameters missing"}), 400 + + try: + # 请求访问令牌 + token_url = f"https://{shop}/admin/oauth/token" + data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "code": code, + "grant_type": "authorization_code", + "redirect_uri": REDIRECT_URI, + } + + response = requests.post(token_url, json=data) + response.raise_for_status() + + token_data = response.json() + print("token data:", token_data) + + return jsonify(token_data) + + # 这里可以调用Shoplazza OpenAPI获取更多数据 + # headers = {"Access-Token": token_data.get("access_token")} + # customers_url = f"https://{shop}/openapi/2022-01/customers" + # customers_response = requests.get(customers_url, headers=headers) + # return redirect(LAST_URL) + + except requests.exceptions.RequestException as e: + print(f"Error: {e}") + return jsonify({"error": "Failed to get access token"}), 500 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=4000, debug=True) diff --git a/python-app-demo/requirements.txt b/python-app-demo/requirements.txt new file mode 100644 index 0000000..d6a9197 --- /dev/null +++ b/python-app-demo/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +requests==2.31.0 +flask-cors==4.0.0 -- libgit2 0.21.2