Commit cccb7cfced6a7ecc4eab981512eb0421dd2d4d92
0 parents
init
Showing
25 changed files
with
1707 additions
and
0 deletions
Show diff stats
| 1 | +++ a/oauth_setup_guide.md | |
| ... | ... | @@ -0,0 +1,120 @@ |
| 1 | +# Shoplazza OAuth2.0 认证设置指南 | |
| 2 | + | |
| 3 | +## 第一步:获取应用凭证 | |
| 4 | + | |
| 5 | +1. **访问Shoplazza开发者平台** | |
| 6 | + - 打开 https://partners.shoplazza.com/ | |
| 7 | + - 登录您的账户 | |
| 8 | + | |
| 9 | +2. **创建新应用** | |
| 10 | + - 点击"创建应用" | |
| 11 | + - 选择"公共应用" | |
| 12 | + - 填写应用信息 | |
| 13 | + | |
| 14 | +3. **获取凭证** | |
| 15 | + - 复制 `CLIENT_ID` | |
| 16 | + - 复制 `CLIENT_SECRET` | |
| 17 | + | |
| 18 | +## 第二步:配置本地环境 | |
| 19 | + | |
| 20 | +### 1. 安装依赖 | |
| 21 | +```bash | |
| 22 | +pip install -r requirements.txt | |
| 23 | +``` | |
| 24 | + | |
| 25 | +### 2. 创建环境配置文件 | |
| 26 | +创建 `.env` 文件: | |
| 27 | +```env | |
| 28 | +# Shoplazza OAuth2.0 配置 | |
| 29 | +CLIENT_ID=your_actual_client_id | |
| 30 | +CLIENT_SECRET=your_actual_client_secret | |
| 31 | +BASE_URL=https://your-ngrok-url.ngrok.io | |
| 32 | +REDIRECT_URI=/auth/shoplazza/callback | |
| 33 | + | |
| 34 | +# Flask 应用配置 | |
| 35 | +SECRET_KEY=your-secret-key-here | |
| 36 | +FLASK_ENV=development | |
| 37 | +FLASK_DEBUG=True | |
| 38 | +PORT=3000 | |
| 39 | +``` | |
| 40 | + | |
| 41 | +### 3. 使用ngrok暴露本地服务 | |
| 42 | +```bash | |
| 43 | +# 安装ngrok | |
| 44 | +npm install -g ngrok | |
| 45 | + | |
| 46 | +# 启动ngrok | |
| 47 | +ngrok http 3000 | |
| 48 | + | |
| 49 | +# 复制ngrok提供的HTTPS URL到BASE_URL | |
| 50 | +``` | |
| 51 | + | |
| 52 | +## 第三步:配置Shoplazza应用 | |
| 53 | + | |
| 54 | +在Shoplazza开发者中心配置以下URL: | |
| 55 | + | |
| 56 | +1. **应用URL**: `https://your-ngrok-url.ngrok.io/auth/install` | |
| 57 | +2. **回调URL**: `https://your-ngrok-url.ngrok.io/auth/shoplazza/callback` | |
| 58 | +3. **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook/shoplazza` | |
| 59 | + | |
| 60 | +## 第四步:启动应用 | |
| 61 | + | |
| 62 | +```bash | |
| 63 | +python run.py | |
| 64 | +``` | |
| 65 | + | |
| 66 | +## 第五步:测试OAuth流程 | |
| 67 | + | |
| 68 | +### 1. 开始认证 | |
| 69 | +访问以下URL(替换为实际的商店域名): | |
| 70 | +``` | |
| 71 | +https://your-ngrok-url.ngrok.io/auth/install?shop=your-shop.myshoplaza.com | |
| 72 | +``` | |
| 73 | + | |
| 74 | +### 2. 完成授权 | |
| 75 | +- 系统会重定向到Shoplazza授权页面 | |
| 76 | +- 商家确认授权 | |
| 77 | +- 系统自动处理回调并获取访问令牌 | |
| 78 | + | |
| 79 | +### 3. 测试API调用 | |
| 80 | +```bash | |
| 81 | +# 获取客户列表 | |
| 82 | +curl https://your-ngrok-url.ngrok.io/api/customers/your-shop.myshoplaza.com | |
| 83 | + | |
| 84 | +# 获取产品列表 | |
| 85 | +curl https://your-ngrok-url.ngrok.io/api/products/your-shop.myshoplaza.com | |
| 86 | +``` | |
| 87 | + | |
| 88 | +## 故障排除 | |
| 89 | + | |
| 90 | +### 常见问题 | |
| 91 | + | |
| 92 | +1. **HMAC验证失败** | |
| 93 | + - 检查CLIENT_SECRET是否正确 | |
| 94 | + - 确认请求来自Shoplazza | |
| 95 | + | |
| 96 | +2. **授权失败** | |
| 97 | + - 检查回调URL配置 | |
| 98 | + - 确认BASE_URL使用HTTPS | |
| 99 | + | |
| 100 | +3. **API调用失败** | |
| 101 | + - 检查访问令牌是否有效 | |
| 102 | + - 确认权限范围正确 | |
| 103 | + | |
| 104 | +### 调试技巧 | |
| 105 | + | |
| 106 | +1. **查看日志** | |
| 107 | + ```bash | |
| 108 | + # 应用会输出详细的日志信息 | |
| 109 | + python run.py | |
| 110 | + ``` | |
| 111 | + | |
| 112 | +2. **检查令牌状态** | |
| 113 | + ```bash | |
| 114 | + curl https://your-ngrok-url.ngrok.io/auth/tokens | |
| 115 | + ``` | |
| 116 | + | |
| 117 | +3. **健康检查** | |
| 118 | + ```bash | |
| 119 | + curl https://your-ngrok-url.ngrok.io/health | |
| 120 | + ``` | ... | ... |
| 1 | +++ a/old/Develop your application service.md | ... | ... |
| 1 | +++ a/old/PROJECT_STRUCTURE.md | |
| ... | ... | @@ -0,0 +1,76 @@ |
| 1 | +# 项目结构说明 | |
| 2 | + | |
| 3 | +## 文件组织 | |
| 4 | + | |
| 5 | +``` | |
| 6 | +shoplazza-oauth-backend/ | |
| 7 | +├── app.py # 主应用入口 | |
| 8 | +├── config.py # 配置管理 | |
| 9 | +├── run.py # 启动脚本 | |
| 10 | +├── requirements.txt # Python依赖 | |
| 11 | +├── env_template.txt # 环境变量模板 | |
| 12 | +├── README.md # 项目文档 | |
| 13 | +├── PROJECT_STRUCTURE.md # 项目结构说明 | |
| 14 | +├── middleware/ | |
| 15 | +│ └── hmac_validator.py # HMAC验证中间件 | |
| 16 | +└── routes/ | |
| 17 | + ├── auth.py # OAuth2.0认证路由 | |
| 18 | + ├── api.py # Shoplazza API调用 | |
| 19 | + └── webhook.py # Webhook处理 | |
| 20 | +``` | |
| 21 | + | |
| 22 | +## 核心组件 | |
| 23 | + | |
| 24 | +### 1. 配置管理 (config.py) | |
| 25 | +- 环境变量加载 | |
| 26 | +- 应用配置管理 | |
| 27 | +- 访问令牌存储 | |
| 28 | + | |
| 29 | +### 2. HMAC验证 (middleware/hmac_validator.py) | |
| 30 | +- OAuth请求HMAC验证 | |
| 31 | +- Webhook签名验证 | |
| 32 | +- 安全比较函数 | |
| 33 | + | |
| 34 | +### 3. 认证流程 (routes/auth.py) | |
| 35 | +- 应用安装处理 | |
| 36 | +- OAuth回调处理 | |
| 37 | +- 令牌交换和刷新 | |
| 38 | + | |
| 39 | +### 4. API调用 (routes/api.py) | |
| 40 | +- 客户数据获取 | |
| 41 | +- 产品数据获取 | |
| 42 | +- 订单数据获取 | |
| 43 | +- 商店信息获取 | |
| 44 | + | |
| 45 | +### 5. Webhook处理 (routes/webhook.py) | |
| 46 | +- Webhook签名验证 | |
| 47 | +- 事件处理逻辑 | |
| 48 | +- 应用卸载处理 | |
| 49 | + | |
| 50 | +## 数据流 | |
| 51 | + | |
| 52 | +``` | |
| 53 | +1. 商家安装应用 | |
| 54 | + ↓ | |
| 55 | +2. 重定向到Shoplazza授权页面 | |
| 56 | + ↓ | |
| 57 | +3. 商家确认授权 | |
| 58 | + ↓ | |
| 59 | +4. Shoplazza回调应用 | |
| 60 | + ↓ | |
| 61 | +5. HMAC验证 | |
| 62 | + ↓ | |
| 63 | +6. 交换访问令牌 | |
| 64 | + ↓ | |
| 65 | +7. 存储令牌 | |
| 66 | + ↓ | |
| 67 | +8. 调用Shoplazza API | |
| 68 | +``` | |
| 69 | + | |
| 70 | +## 安全特性 | |
| 71 | + | |
| 72 | +- HMAC签名验证 | |
| 73 | +- CSRF攻击防护 | |
| 74 | +- 安全的令牌存储 | |
| 75 | +- Webhook签名验证 | |
| 76 | +- 环境变量配置管理 | ... | ... |
| 1 | +++ a/old/README.md | |
| ... | ... | @@ -0,0 +1,153 @@ |
| 1 | +# Shoplazza OAuth2.0 后端应用 | |
| 2 | + | |
| 3 | +这是一个基于Python Flask的Shoplazza OAuth2.0认证后端应用,实现了完整的OAuth2.0认证流程和API调用功能。 | |
| 4 | + | |
| 5 | +## 功能特性 | |
| 6 | + | |
| 7 | +- ✅ OAuth2.0 认证流程 | |
| 8 | +- ✅ HMAC 签名验证 | |
| 9 | +- ✅ 访问令牌管理 | |
| 10 | +- ✅ 令牌刷新功能 | |
| 11 | +- ✅ Shoplazza API 调用 | |
| 12 | +- ✅ Webhook 处理 | |
| 13 | +- ✅ 安全验证中间件 | |
| 14 | + | |
| 15 | +## 项目结构 | |
| 16 | + | |
| 17 | +``` | |
| 18 | +├── app.py # 主应用入口 | |
| 19 | +├── config.py # 配置文件 | |
| 20 | +├── requirements.txt # 依赖包 | |
| 21 | +├── middleware/ | |
| 22 | +│ └── hmac_validator.py # HMAC验证中间件 | |
| 23 | +├── routes/ | |
| 24 | +│ ├── auth.py # 认证路由 | |
| 25 | +│ ├── api.py # API调用路由 | |
| 26 | +│ └── webhook.py # Webhook处理路由 | |
| 27 | +└── README.md # 项目说明 | |
| 28 | +``` | |
| 29 | + | |
| 30 | +## 安装和配置 | |
| 31 | + | |
| 32 | +### 1. 安装依赖 | |
| 33 | + | |
| 34 | +```bash | |
| 35 | +pip install -r requirements.txt | |
| 36 | +``` | |
| 37 | + | |
| 38 | +### 2. 环境配置 | |
| 39 | + | |
| 40 | +创建 `.env` 文件并配置以下参数: | |
| 41 | + | |
| 42 | +```env | |
| 43 | +# Shoplazza OAuth2.0 配置 | |
| 44 | +CLIENT_ID=your_client_id_here | |
| 45 | +CLIENT_SECRET=your_client_secret_here | |
| 46 | +BASE_URL=https://your-domain.com | |
| 47 | +REDIRECT_URI=/auth/shoplazza/callback | |
| 48 | + | |
| 49 | +# 应用配置 | |
| 50 | +FLASK_ENV=development | |
| 51 | +FLASK_DEBUG=True | |
| 52 | +PORT=3000 | |
| 53 | +SECRET_KEY=your-secret-key-here | |
| 54 | +``` | |
| 55 | + | |
| 56 | +### 3. 运行应用 | |
| 57 | + | |
| 58 | +```bash | |
| 59 | +python app.py | |
| 60 | +``` | |
| 61 | + | |
| 62 | +## API 端点 | |
| 63 | + | |
| 64 | +### 认证端点 | |
| 65 | + | |
| 66 | +- `GET /auth/install?shop=your-shop.myshoplaza.com` - 开始OAuth认证流程 | |
| 67 | +- `GET /auth/shoplazza/callback` - OAuth回调处理 | |
| 68 | +- `GET /auth/refresh_token/<shop>` - 刷新访问令牌 | |
| 69 | +- `GET /auth/tokens` - 查看已授权的商店令牌 | |
| 70 | + | |
| 71 | +### API调用端点 | |
| 72 | + | |
| 73 | +- `GET /api/customers/<shop>` - 获取客户列表 | |
| 74 | +- `GET /api/products/<shop>` - 获取产品列表 | |
| 75 | +- `GET /api/orders/<shop>` - 获取订单列表 | |
| 76 | +- `GET /api/shop_info/<shop>` - 获取商店信息 | |
| 77 | + | |
| 78 | +### Webhook端点 | |
| 79 | + | |
| 80 | +- `POST /webhook/shoplazza` - 处理Shoplazza Webhook | |
| 81 | + | |
| 82 | +## 使用示例 | |
| 83 | + | |
| 84 | +### 1. 开始认证流程 | |
| 85 | + | |
| 86 | +访问以下URL开始OAuth认证: | |
| 87 | +``` | |
| 88 | +https://your-domain.com/auth/install?shop=your-shop.myshoplaza.com | |
| 89 | +``` | |
| 90 | + | |
| 91 | +### 2. 调用API | |
| 92 | + | |
| 93 | +认证成功后,可以调用Shoplazza API: | |
| 94 | +```bash | |
| 95 | +curl https://your-domain.com/api/customers/your-shop.myshoplaza.com | |
| 96 | +``` | |
| 97 | + | |
| 98 | +### 3. 处理Webhook | |
| 99 | + | |
| 100 | +在Shoplazza开发者中心配置Webhook URL: | |
| 101 | +``` | |
| 102 | +https://your-domain.com/webhook/shoplazza | |
| 103 | +``` | |
| 104 | + | |
| 105 | +## OAuth2.0 认证流程 | |
| 106 | + | |
| 107 | +1. **应用安装**: 商家从应用商店安装应用 | |
| 108 | +2. **授权请求**: 应用重定向到Shoplazza授权页面 | |
| 109 | +3. **用户授权**: 商家确认授权 | |
| 110 | +4. **授权回调**: Shoplazza重定向回应用并携带授权码 | |
| 111 | +5. **令牌交换**: 应用使用授权码交换访问令牌 | |
| 112 | +6. **API调用**: 使用访问令牌调用Shoplazza API | |
| 113 | + | |
| 114 | +## 安全特性 | |
| 115 | + | |
| 116 | +- HMAC签名验证确保请求来自Shoplazza | |
| 117 | +- 安全的令牌存储和管理 | |
| 118 | +- CSRF攻击防护 | |
| 119 | +- Webhook签名验证 | |
| 120 | + | |
| 121 | +## 开发说明 | |
| 122 | + | |
| 123 | +### 本地开发 | |
| 124 | + | |
| 125 | +1. 使用ngrok等工具将本地服务暴露到公网 | |
| 126 | +2. 在Shoplazza开发者中心配置应用URL | |
| 127 | +3. 确保所有回调URL使用HTTPS | |
| 128 | + | |
| 129 | +### 生产部署 | |
| 130 | + | |
| 131 | +1. 使用环境变量管理敏感配置 | |
| 132 | +2. 使用数据库存储访问令牌 | |
| 133 | +3. 配置适当的日志记录 | |
| 134 | +4. 设置监控和告警 | |
| 135 | + | |
| 136 | +## 故障排除 | |
| 137 | + | |
| 138 | +### 常见问题 | |
| 139 | + | |
| 140 | +1. **HMAC验证失败**: 检查CLIENT_SECRET配置 | |
| 141 | +2. **授权失败**: 确认回调URL配置正确 | |
| 142 | +3. **API调用失败**: 检查访问令牌是否有效 | |
| 143 | + | |
| 144 | +### 日志查看 | |
| 145 | + | |
| 146 | +应用会记录详细的日志信息,包括: | |
| 147 | +- 认证流程状态 | |
| 148 | +- API调用结果 | |
| 149 | +- 错误信息 | |
| 150 | + | |
| 151 | +## 许可证 | |
| 152 | + | |
| 153 | +MIT License | ... | ... |
No preview for this file type
| 1 | +++ a/old/app.py | |
| ... | ... | @@ -0,0 +1,64 @@ |
| 1 | +from flask import Flask, jsonify | |
| 2 | +from config import Config | |
| 3 | +from routes.auth import auth_bp | |
| 4 | +from routes.api import api_bp | |
| 5 | +from routes.webhook import webhook_bp | |
| 6 | +import logging | |
| 7 | + | |
| 8 | +def create_app(): | |
| 9 | + """创建Flask应用""" | |
| 10 | + app = Flask(__name__) | |
| 11 | + | |
| 12 | + # 配置应用 | |
| 13 | + app.config.from_object(Config) | |
| 14 | + | |
| 15 | + # 配置日志 | |
| 16 | + logging.basicConfig(level=logging.INFO) | |
| 17 | + | |
| 18 | + # 注册蓝图 | |
| 19 | + app.register_blueprint(auth_bp) | |
| 20 | + app.register_blueprint(api_bp) | |
| 21 | + app.register_blueprint(webhook_bp) | |
| 22 | + | |
| 23 | + # 根路由 | |
| 24 | + @app.route('/') | |
| 25 | + def index(): | |
| 26 | + return jsonify({ | |
| 27 | + 'message': 'Shoplazza OAuth2.0 Backend Service', | |
| 28 | + 'version': '1.0.0', | |
| 29 | + 'endpoints': { | |
| 30 | + 'auth': { | |
| 31 | + 'install': '/auth/install?shop=your-shop.myshoplaza.com', | |
| 32 | + 'callback': '/auth/shoplazza/callback', | |
| 33 | + 'refresh_token': '/auth/refresh_token/<shop>', | |
| 34 | + 'tokens': '/auth/tokens' | |
| 35 | + }, | |
| 36 | + 'api': { | |
| 37 | + 'customers': '/api/customers/<shop>', | |
| 38 | + 'products': '/api/products/<shop>', | |
| 39 | + 'orders': '/api/orders/<shop>', | |
| 40 | + 'shop_info': '/api/shop_info/<shop>' | |
| 41 | + }, | |
| 42 | + 'webhook': { | |
| 43 | + 'shoplazza': '/webhook/shoplazza' | |
| 44 | + } | |
| 45 | + } | |
| 46 | + }) | |
| 47 | + | |
| 48 | + # 健康检查端点 | |
| 49 | + @app.route('/health') | |
| 50 | + def health(): | |
| 51 | + return jsonify({ | |
| 52 | + 'status': 'healthy', | |
| 53 | + 'authorized_shops': len(Config.ACCESS_TOKENS) | |
| 54 | + }) | |
| 55 | + | |
| 56 | + return app | |
| 57 | + | |
| 58 | +if __name__ == '__main__': | |
| 59 | + app = create_app() | |
| 60 | + app.run( | |
| 61 | + host='0.0.0.0', | |
| 62 | + port=Config.PORT, | |
| 63 | + debug=Config.DEBUG | |
| 64 | + ) | ... | ... |
| 1 | +++ a/old/config.py | |
| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | +import os | |
| 2 | +from dotenv import load_dotenv | |
| 3 | + | |
| 4 | +# 加载环境变量 | |
| 5 | +load_dotenv() | |
| 6 | + | |
| 7 | +class Config: | |
| 8 | + """Shoplazza OAuth2.0 应用配置""" | |
| 9 | + | |
| 10 | + # Shoplazza OAuth2.0 配置 | |
| 11 | + CLIENT_ID = os.getenv('CLIENT_ID', 'your_client_id_here') | |
| 12 | + CLIENT_SECRET = os.getenv('CLIENT_SECRET', 'your_client_secret_here') | |
| 13 | + BASE_URL = os.getenv('BASE_URL', 'https://your-domain.com') | |
| 14 | + REDIRECT_URI = os.getenv('REDIRECT_URI', '/auth/shoplazza/callback') | |
| 15 | + | |
| 16 | + # Flask 配置 | |
| 17 | + SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-here') | |
| 18 | + DEBUG = os.getenv('FLASK_DEBUG', 'True').lower() == 'true' | |
| 19 | + PORT = int(os.getenv('PORT', 3000)) | |
| 20 | + | |
| 21 | + # 存储访问令牌的字典(生产环境应使用数据库) | |
| 22 | + ACCESS_TOKENS = {} | ... | ... |
| 1 | +++ a/old/env_template.txt | |
| ... | ... | @@ -0,0 +1,16 @@ |
| 1 | +# Shoplazza OAuth2.0 配置模板 | |
| 2 | +# 复制此文件为 .env 并填入实际值 | |
| 3 | + | |
| 4 | +# Shoplazza 应用配置 (从 https://partners.shoplazza.com/ 获取) | |
| 5 | +CLIENT_ID=your_client_id_here | |
| 6 | +CLIENT_SECRET=your_client_secret_here | |
| 7 | + | |
| 8 | +# 应用基础URL (使用ngrok等工具暴露本地服务) | |
| 9 | +BASE_URL=https://your-domain.com | |
| 10 | +REDIRECT_URI=/auth/shoplazza/callback | |
| 11 | + | |
| 12 | +# Flask 应用配置 | |
| 13 | +SECRET_KEY=your-secret-key-here | |
| 14 | +FLASK_ENV=development | |
| 15 | +FLASK_DEBUG=True | |
| 16 | +PORT=3000 | ... | ... |
No preview for this file type
| 1 | +++ a/old/middleware/hmac_validator.py | |
| ... | ... | @@ -0,0 +1,59 @@ |
| 1 | +import hmac | |
| 2 | +import hashlib | |
| 3 | +from flask import request, jsonify | |
| 4 | +from functools import wraps | |
| 5 | +from config import Config | |
| 6 | + | |
| 7 | +def hmac_validator_required(f): | |
| 8 | + """ | |
| 9 | + HMAC验证装饰器 | |
| 10 | + 验证来自Shoplazza的请求是否有效 | |
| 11 | + """ | |
| 12 | + @wraps(f) | |
| 13 | + def decorated_function(*args, **kwargs): | |
| 14 | + # 获取查询参数 | |
| 15 | + query_params = request.args.to_dict() | |
| 16 | + hmac_param = query_params.pop('hmac', None) | |
| 17 | + | |
| 18 | + if not hmac_param: | |
| 19 | + return jsonify({'error': 'Missing HMAC parameter'}), 400 | |
| 20 | + | |
| 21 | + # 验证shop参数格式 | |
| 22 | + shop = query_params.get('shop') | |
| 23 | + if not shop or not shop.endswith('.myshoplaza.com'): | |
| 24 | + return jsonify({'error': 'Invalid shop parameter'}), 400 | |
| 25 | + | |
| 26 | + # 构建验证消息 | |
| 27 | + sorted_keys = sorted(query_params.keys()) | |
| 28 | + message = '&'.join([f"{key}={query_params[key]}" for key in sorted_keys]) | |
| 29 | + | |
| 30 | + # 计算HMAC | |
| 31 | + calculated_hmac = hmac.new( | |
| 32 | + Config.CLIENT_SECRET.encode('utf-8'), | |
| 33 | + message.encode('utf-8'), | |
| 34 | + hashlib.sha256 | |
| 35 | + ).hexdigest() | |
| 36 | + | |
| 37 | + # 安全比较HMAC | |
| 38 | + if not hmac.compare_digest(calculated_hmac, hmac_param): | |
| 39 | + return jsonify({'error': 'HMAC validation failed'}), 403 | |
| 40 | + | |
| 41 | + return f(*args, **kwargs) | |
| 42 | + | |
| 43 | + return decorated_function | |
| 44 | + | |
| 45 | +def verify_webhook_hmac(data, hmac_header): | |
| 46 | + """ | |
| 47 | + 验证Webhook的HMAC签名 | |
| 48 | + """ | |
| 49 | + import base64 | |
| 50 | + | |
| 51 | + calculated_hmac = base64.b64encode( | |
| 52 | + hmac.new( | |
| 53 | + Config.CLIENT_SECRET.encode('utf-8'), | |
| 54 | + data, | |
| 55 | + hashlib.sha256 | |
| 56 | + ).digest() | |
| 57 | + ).decode('utf-8') | |
| 58 | + | |
| 59 | + return hmac.compare_digest(calculated_hmac, hmac_header) | ... | ... |
| 1 | +++ a/old/oauth_setup_guide.md | |
| ... | ... | @@ -0,0 +1,120 @@ |
| 1 | +# Shoplazza OAuth2.0 认证设置指南 | |
| 2 | + | |
| 3 | +## 第一步:获取应用凭证 | |
| 4 | + | |
| 5 | +1. **访问Shoplazza开发者平台** | |
| 6 | + - 打开 https://partners.shoplazza.com/ | |
| 7 | + - 登录您的账户 | |
| 8 | + | |
| 9 | +2. **创建新应用** | |
| 10 | + - 点击"创建应用" | |
| 11 | + - 选择"公共应用" | |
| 12 | + - 填写应用信息 | |
| 13 | + | |
| 14 | +3. **获取凭证** | |
| 15 | + - 复制 `CLIENT_ID` | |
| 16 | + - 复制 `CLIENT_SECRET` | |
| 17 | + | |
| 18 | +## 第二步:配置本地环境 | |
| 19 | + | |
| 20 | +### 1. 安装依赖 | |
| 21 | +```bash | |
| 22 | +pip install -r requirements.txt | |
| 23 | +``` | |
| 24 | + | |
| 25 | +### 2. 创建环境配置文件 | |
| 26 | +创建 `.env` 文件: | |
| 27 | +```env | |
| 28 | +# Shoplazza OAuth2.0 配置 | |
| 29 | +CLIENT_ID=your_actual_client_id | |
| 30 | +CLIENT_SECRET=your_actual_client_secret | |
| 31 | +BASE_URL=https://your-ngrok-url.ngrok.io | |
| 32 | +REDIRECT_URI=/auth/shoplazza/callback | |
| 33 | + | |
| 34 | +# Flask 应用配置 | |
| 35 | +SECRET_KEY=your-secret-key-here | |
| 36 | +FLASK_ENV=development | |
| 37 | +FLASK_DEBUG=True | |
| 38 | +PORT=3000 | |
| 39 | +``` | |
| 40 | + | |
| 41 | +### 3. 使用ngrok暴露本地服务 | |
| 42 | +```bash | |
| 43 | +# 安装ngrok | |
| 44 | +npm install -g ngrok | |
| 45 | + | |
| 46 | +# 启动ngrok | |
| 47 | +ngrok http 3000 | |
| 48 | + | |
| 49 | +# 复制ngrok提供的HTTPS URL到BASE_URL | |
| 50 | +``` | |
| 51 | + | |
| 52 | +## 第三步:配置Shoplazza应用 | |
| 53 | + | |
| 54 | +在Shoplazza开发者中心配置以下URL: | |
| 55 | + | |
| 56 | +1. **应用URL**: `https://your-ngrok-url.ngrok.io/auth/install` | |
| 57 | +2. **回调URL**: `https://your-ngrok-url.ngrok.io/auth/shoplazza/callback` | |
| 58 | +3. **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook/shoplazza` | |
| 59 | + | |
| 60 | +## 第四步:启动应用 | |
| 61 | + | |
| 62 | +```bash | |
| 63 | +python run.py | |
| 64 | +``` | |
| 65 | + | |
| 66 | +## 第五步:测试OAuth流程 | |
| 67 | + | |
| 68 | +### 1. 开始认证 | |
| 69 | +访问以下URL(替换为实际的商店域名): | |
| 70 | +``` | |
| 71 | +https://your-ngrok-url.ngrok.io/auth/install?shop=your-shop.myshoplaza.com | |
| 72 | +``` | |
| 73 | + | |
| 74 | +### 2. 完成授权 | |
| 75 | +- 系统会重定向到Shoplazza授权页面 | |
| 76 | +- 商家确认授权 | |
| 77 | +- 系统自动处理回调并获取访问令牌 | |
| 78 | + | |
| 79 | +### 3. 测试API调用 | |
| 80 | +```bash | |
| 81 | +# 获取客户列表 | |
| 82 | +curl https://your-ngrok-url.ngrok.io/api/customers/your-shop.myshoplaza.com | |
| 83 | + | |
| 84 | +# 获取产品列表 | |
| 85 | +curl https://your-ngrok-url.ngrok.io/api/products/your-shop.myshoplaza.com | |
| 86 | +``` | |
| 87 | + | |
| 88 | +## 故障排除 | |
| 89 | + | |
| 90 | +### 常见问题 | |
| 91 | + | |
| 92 | +1. **HMAC验证失败** | |
| 93 | + - 检查CLIENT_SECRET是否正确 | |
| 94 | + - 确认请求来自Shoplazza | |
| 95 | + | |
| 96 | +2. **授权失败** | |
| 97 | + - 检查回调URL配置 | |
| 98 | + - 确认BASE_URL使用HTTPS | |
| 99 | + | |
| 100 | +3. **API调用失败** | |
| 101 | + - 检查访问令牌是否有效 | |
| 102 | + - 确认权限范围正确 | |
| 103 | + | |
| 104 | +### 调试技巧 | |
| 105 | + | |
| 106 | +1. **查看日志** | |
| 107 | + ```bash | |
| 108 | + # 应用会输出详细的日志信息 | |
| 109 | + python run.py | |
| 110 | + ``` | |
| 111 | + | |
| 112 | +2. **检查令牌状态** | |
| 113 | + ```bash | |
| 114 | + curl https://your-ngrok-url.ngrok.io/auth/tokens | |
| 115 | + ``` | |
| 116 | + | |
| 117 | +3. **健康检查** | |
| 118 | + ```bash | |
| 119 | + curl https://your-ngrok-url.ngrok.io/health | |
| 120 | + ``` | ... | ... |
No preview for this file type
No preview for this file type
No preview for this file type
| 1 | +++ a/old/routes/api.py | |
| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | +import requests | |
| 2 | +from flask import Blueprint, jsonify, request, current_app | |
| 3 | +from config import Config | |
| 4 | + | |
| 5 | +# 创建API蓝图 | |
| 6 | +api_bp = Blueprint('api', __name__, url_prefix='/api') | |
| 7 | + | |
| 8 | +@api_bp.route('/customers/<shop>') | |
| 9 | +def get_customers(shop): | |
| 10 | + """ | |
| 11 | + 获取客户列表 | |
| 12 | + """ | |
| 13 | + if shop not in Config.ACCESS_TOKENS: | |
| 14 | + return jsonify({'error': 'Shop not authorized'}), 401 | |
| 15 | + | |
| 16 | + token_data = Config.ACCESS_TOKENS[shop] | |
| 17 | + access_token = token_data.get('access_token') | |
| 18 | + | |
| 19 | + if not access_token: | |
| 20 | + return jsonify({'error': 'No access token available'}), 401 | |
| 21 | + | |
| 22 | + try: | |
| 23 | + # 调用Shoplazza API获取客户列表 | |
| 24 | + api_url = f"https://{shop}/openapi/2022-01/customers" | |
| 25 | + headers = { | |
| 26 | + 'Access-Token': access_token, | |
| 27 | + 'Content-Type': 'application/json' | |
| 28 | + } | |
| 29 | + | |
| 30 | + response = requests.get(api_url, headers=headers) | |
| 31 | + response.raise_for_status() | |
| 32 | + | |
| 33 | + return jsonify({ | |
| 34 | + 'shop': shop, | |
| 35 | + 'customers': response.json() | |
| 36 | + }) | |
| 37 | + | |
| 38 | + except Exception as e: | |
| 39 | + current_app.logger.error(f"获取客户列表失败: {str(e)}") | |
| 40 | + return jsonify({'error': 'Failed to fetch customers'}), 500 | |
| 41 | + | |
| 42 | +@api_bp.route('/products/<shop>') | |
| 43 | +def get_products(shop): | |
| 44 | + """ | |
| 45 | + 获取产品列表 | |
| 46 | + """ | |
| 47 | + if shop not in Config.ACCESS_TOKENS: | |
| 48 | + return jsonify({'error': 'Shop not authorized'}), 401 | |
| 49 | + | |
| 50 | + token_data = Config.ACCESS_TOKENS[shop] | |
| 51 | + access_token = token_data.get('access_token') | |
| 52 | + | |
| 53 | + if not access_token: | |
| 54 | + return jsonify({'error': 'No access token available'}), 401 | |
| 55 | + | |
| 56 | + try: | |
| 57 | + # 调用Shoplazza API获取产品列表 | |
| 58 | + api_url = f"https://{shop}/openapi/2020-01/products" | |
| 59 | + headers = { | |
| 60 | + 'Access-Token': access_token, | |
| 61 | + 'Content-Type': 'application/json' | |
| 62 | + } | |
| 63 | + | |
| 64 | + response = requests.get(api_url, headers=headers) | |
| 65 | + response.raise_for_status() | |
| 66 | + | |
| 67 | + return jsonify({ | |
| 68 | + 'shop': shop, | |
| 69 | + 'products': response.json() | |
| 70 | + }) | |
| 71 | + | |
| 72 | + except Exception as e: | |
| 73 | + current_app.logger.error(f"获取产品列表失败: {str(e)}") | |
| 74 | + return jsonify({'error': 'Failed to fetch products'}), 500 | |
| 75 | + | |
| 76 | +@api_bp.route('/orders/<shop>') | |
| 77 | +def get_orders(shop): | |
| 78 | + """ | |
| 79 | + 获取订单列表 | |
| 80 | + """ | |
| 81 | + if shop not in Config.ACCESS_TOKENS: | |
| 82 | + return jsonify({'error': 'Shop not authorized'}), 401 | |
| 83 | + | |
| 84 | + token_data = Config.ACCESS_TOKENS[shop] | |
| 85 | + access_token = token_data.get('access_token') | |
| 86 | + | |
| 87 | + if not access_token: | |
| 88 | + return jsonify({'error': 'No access token available'}), 401 | |
| 89 | + | |
| 90 | + try: | |
| 91 | + # 调用Shoplazza API获取订单列表 | |
| 92 | + api_url = f"https://{shop}/openapi/2020-01/orders" | |
| 93 | + headers = { | |
| 94 | + 'Access-Token': access_token, | |
| 95 | + 'Content-Type': 'application/json' | |
| 96 | + } | |
| 97 | + | |
| 98 | + response = requests.get(api_url, headers=headers) | |
| 99 | + response.raise_for_status() | |
| 100 | + | |
| 101 | + return jsonify({ | |
| 102 | + 'shop': shop, | |
| 103 | + 'orders': response.json() | |
| 104 | + }) | |
| 105 | + | |
| 106 | + except Exception as e: | |
| 107 | + current_app.logger.error(f"获取订单列表失败: {str(e)}") | |
| 108 | + return jsonify({'error': 'Failed to fetch orders'}), 500 | |
| 109 | + | |
| 110 | +@api_bp.route('/shop_info/<shop>') | |
| 111 | +def get_shop_info(shop): | |
| 112 | + """ | |
| 113 | + 获取商店信息 | |
| 114 | + """ | |
| 115 | + if shop not in Config.ACCESS_TOKENS: | |
| 116 | + return jsonify({'error': 'Shop not authorized'}), 401 | |
| 117 | + | |
| 118 | + token_data = Config.ACCESS_TOKENS[shop] | |
| 119 | + access_token = token_data.get('access_token') | |
| 120 | + | |
| 121 | + if not access_token: | |
| 122 | + return jsonify({'error': 'No access token available'}), 401 | |
| 123 | + | |
| 124 | + try: | |
| 125 | + # 调用Shoplazza API获取商店信息 | |
| 126 | + api_url = f"https://{shop}/openapi/2020-01/shop" | |
| 127 | + headers = { | |
| 128 | + 'Access-Token': access_token, | |
| 129 | + 'Content-Type': 'application/json' | |
| 130 | + } | |
| 131 | + | |
| 132 | + response = requests.get(api_url, headers=headers) | |
| 133 | + response.raise_for_status() | |
| 134 | + | |
| 135 | + return jsonify({ | |
| 136 | + 'shop': shop, | |
| 137 | + 'shop_info': response.json() | |
| 138 | + }) | |
| 139 | + | |
| 140 | + except Exception as e: | |
| 141 | + current_app.logger.error(f"获取商店信息失败: {str(e)}") | |
| 142 | + return jsonify({'error': 'Failed to fetch shop info'}), 500 | ... | ... |
| 1 | +++ a/old/routes/auth.py | |
| ... | ... | @@ -0,0 +1,143 @@ |
| 1 | +import secrets | |
| 2 | +import requests | |
| 3 | +from flask import Blueprint, request, redirect, jsonify, current_app | |
| 4 | +from config import Config | |
| 5 | +from middleware.hmac_validator import hmac_validator_required | |
| 6 | + | |
| 7 | +# 创建认证蓝图 | |
| 8 | +auth_bp = Blueprint('auth', __name__, url_prefix='/auth') | |
| 9 | + | |
| 10 | +@auth_bp.route('/install') | |
| 11 | +def install(): | |
| 12 | + """ | |
| 13 | + Step 1: 处理应用安装请求 | |
| 14 | + 当商家从应用商店安装应用时,Shoplazza会发送请求到这个端点 | |
| 15 | + """ | |
| 16 | + shop = request.args.get('shop') | |
| 17 | + if not shop: | |
| 18 | + return jsonify({'error': 'Missing shop parameter'}), 400 | |
| 19 | + | |
| 20 | + # 验证shop参数格式 | |
| 21 | + if not shop.endswith('.myshoplaza.com'): | |
| 22 | + return jsonify({'error': 'Invalid shop parameter'}), 400 | |
| 23 | + | |
| 24 | + # 定义需要的权限范围 | |
| 25 | + scopes = "read_customer,read_product,write_product,read_order,read_shop" | |
| 26 | + | |
| 27 | + # 生成随机state参数防止CSRF攻击 | |
| 28 | + state = secrets.token_hex(16) | |
| 29 | + | |
| 30 | + # 构建授权URL | |
| 31 | + auth_url = ( | |
| 32 | + f"https://{shop}/admin/oauth/authorize?" | |
| 33 | + f"client_id={Config.CLIENT_ID}&" | |
| 34 | + f"scope={scopes}&" | |
| 35 | + f"redirect_uri={Config.BASE_URL}{Config.REDIRECT_URI}&" | |
| 36 | + f"response_type=code&" | |
| 37 | + f"state={state}" | |
| 38 | + ) | |
| 39 | + | |
| 40 | + return redirect(auth_url) | |
| 41 | + | |
| 42 | +@auth_bp.route('/shoplazza/callback') | |
| 43 | +@hmac_validator_required | |
| 44 | +def callback(): | |
| 45 | + """ | |
| 46 | + Step 2: 处理OAuth回调 | |
| 47 | + 商家授权后,Shoplazza会重定向到这个端点 | |
| 48 | + """ | |
| 49 | + code = request.args.get('code') | |
| 50 | + shop = request.args.get('shop') | |
| 51 | + state = request.args.get('state') | |
| 52 | + | |
| 53 | + if not shop or not code: | |
| 54 | + return jsonify({'error': 'Missing required parameters'}), 400 | |
| 55 | + | |
| 56 | + try: | |
| 57 | + # 交换授权码获取访问令牌 | |
| 58 | + token_data = exchange_code_for_token(code, shop) | |
| 59 | + | |
| 60 | + # 存储访问令牌(生产环境应使用数据库) | |
| 61 | + Config.ACCESS_TOKENS[shop] = token_data | |
| 62 | + | |
| 63 | + current_app.logger.info(f"获取access_token成功 for shop: {shop}") | |
| 64 | + | |
| 65 | + return jsonify({ | |
| 66 | + 'message': 'Authorization successful', | |
| 67 | + 'shop': shop, | |
| 68 | + 'store_id': token_data.get('store_id'), | |
| 69 | + 'store_name': token_data.get('store_name') | |
| 70 | + }) | |
| 71 | + | |
| 72 | + except Exception as e: | |
| 73 | + current_app.logger.error(f"获取access_token失败: {str(e)}") | |
| 74 | + return jsonify({'error': 'Failed to obtain access token'}), 500 | |
| 75 | + | |
| 76 | +def exchange_code_for_token(code, shop): | |
| 77 | + """ | |
| 78 | + 使用授权码交换访问令牌 | |
| 79 | + """ | |
| 80 | + token_url = f"https://{shop}/admin/oauth/token" | |
| 81 | + | |
| 82 | + data = { | |
| 83 | + 'client_id': Config.CLIENT_ID, | |
| 84 | + 'client_secret': Config.CLIENT_SECRET, | |
| 85 | + 'code': code, | |
| 86 | + 'grant_type': 'authorization_code', | |
| 87 | + 'redirect_uri': f"{Config.BASE_URL}{Config.REDIRECT_URI}" | |
| 88 | + } | |
| 89 | + | |
| 90 | + response = requests.post(token_url, data=data) | |
| 91 | + response.raise_for_status() | |
| 92 | + | |
| 93 | + return response.json() | |
| 94 | + | |
| 95 | +@auth_bp.route('/refresh_token/<shop>') | |
| 96 | +def refresh_token(shop): | |
| 97 | + """ | |
| 98 | + 刷新访问令牌 | |
| 99 | + """ | |
| 100 | + if shop not in Config.ACCESS_TOKENS: | |
| 101 | + return jsonify({'error': 'Shop not found'}), 404 | |
| 102 | + | |
| 103 | + token_data = Config.ACCESS_TOKENS[shop] | |
| 104 | + refresh_token_value = token_data.get('refresh_token') | |
| 105 | + | |
| 106 | + if not refresh_token_value: | |
| 107 | + return jsonify({'error': 'No refresh token available'}), 400 | |
| 108 | + | |
| 109 | + try: | |
| 110 | + refresh_url = f"https://{shop}/admin/oauth/token" | |
| 111 | + | |
| 112 | + data = { | |
| 113 | + 'client_id': Config.CLIENT_ID, | |
| 114 | + 'client_secret': Config.CLIENT_SECRET, | |
| 115 | + 'refresh_token': refresh_token_value, | |
| 116 | + 'grant_type': 'refresh_token', | |
| 117 | + 'redirect_uri': f"{Config.BASE_URL}{Config.REDIRECT_URI}" | |
| 118 | + } | |
| 119 | + | |
| 120 | + response = requests.post(refresh_url, data=data) | |
| 121 | + response.raise_for_status() | |
| 122 | + | |
| 123 | + # 更新存储的令牌 | |
| 124 | + Config.ACCESS_TOKENS[shop] = response.json() | |
| 125 | + | |
| 126 | + return jsonify({ | |
| 127 | + 'message': 'Token refreshed successfully', | |
| 128 | + 'new_token': response.json() | |
| 129 | + }) | |
| 130 | + | |
| 131 | + except Exception as e: | |
| 132 | + current_app.logger.error(f"刷新token失败: {str(e)}") | |
| 133 | + return jsonify({'error': 'Failed to refresh token'}), 500 | |
| 134 | + | |
| 135 | +@auth_bp.route('/tokens') | |
| 136 | +def list_tokens(): | |
| 137 | + """ | |
| 138 | + 列出所有已授权的商店令牌(仅用于调试) | |
| 139 | + """ | |
| 140 | + return jsonify({ | |
| 141 | + 'authorized_shops': list(Config.ACCESS_TOKENS.keys()), | |
| 142 | + 'tokens': Config.ACCESS_TOKENS | |
| 143 | + }) | ... | ... |
| 1 | +++ a/old/routes/webhook.py | |
| ... | ... | @@ -0,0 +1,77 @@ |
| 1 | +import hmac | |
| 2 | +import hashlib | |
| 3 | +import base64 | |
| 4 | +from flask import Blueprint, request, jsonify, current_app | |
| 5 | +from config import Config | |
| 6 | +from middleware.hmac_validator import verify_webhook_hmac | |
| 7 | + | |
| 8 | +# 创建Webhook蓝图 | |
| 9 | +webhook_bp = Blueprint('webhook', __name__, url_prefix='/webhook') | |
| 10 | + | |
| 11 | +@webhook_bp.route('/shoplazza', methods=['POST']) | |
| 12 | +def shoplazza_webhook(): | |
| 13 | + """ | |
| 14 | + 处理Shoplazza Webhook | |
| 15 | + """ | |
| 16 | + # 获取HMAC签名头 | |
| 17 | + hmac_header = request.headers.get('X-Shoplazza-Hmac-Sha256') | |
| 18 | + if not hmac_header: | |
| 19 | + return jsonify({'error': 'Missing HMAC header'}), 400 | |
| 20 | + | |
| 21 | + # 获取原始数据 | |
| 22 | + data = request.get_data() | |
| 23 | + | |
| 24 | + # 验证Webhook签名 | |
| 25 | + if not verify_webhook_hmac(data, hmac_header): | |
| 26 | + current_app.logger.warning("Webhook HMAC verification failed") | |
| 27 | + return jsonify({'error': 'Invalid webhook signature'}), 403 | |
| 28 | + | |
| 29 | + try: | |
| 30 | + # 解析Webhook数据 | |
| 31 | + webhook_data = request.get_json() | |
| 32 | + current_app.logger.info(f"Received webhook: {webhook_data}") | |
| 33 | + | |
| 34 | + # 根据Webhook类型处理 | |
| 35 | + webhook_type = webhook_data.get('type') | |
| 36 | + | |
| 37 | + if webhook_type == 'app/uninstalled': | |
| 38 | + handle_app_uninstalled(webhook_data) | |
| 39 | + elif webhook_type == 'orders/create': | |
| 40 | + handle_order_created(webhook_data) | |
| 41 | + elif webhook_type == 'orders/updated': | |
| 42 | + handle_order_updated(webhook_data) | |
| 43 | + elif webhook_type == 'products/create': | |
| 44 | + handle_product_created(webhook_data) | |
| 45 | + else: | |
| 46 | + current_app.logger.info(f"Unhandled webhook type: {webhook_type}") | |
| 47 | + | |
| 48 | + return jsonify({'status': 'success'}) | |
| 49 | + | |
| 50 | + except Exception as e: | |
| 51 | + current_app.logger.error(f"Webhook处理失败: {str(e)}") | |
| 52 | + return jsonify({'error': 'Webhook processing failed'}), 500 | |
| 53 | + | |
| 54 | +def handle_app_uninstalled(webhook_data): | |
| 55 | + """处理应用卸载事件""" | |
| 56 | + shop = webhook_data.get('shop') | |
| 57 | + if shop and shop in Config.ACCESS_TOKENS: | |
| 58 | + del Config.ACCESS_TOKENS[shop] | |
| 59 | + current_app.logger.info(f"App uninstalled for shop: {shop}") | |
| 60 | + | |
| 61 | +def handle_order_created(webhook_data): | |
| 62 | + """处理订单创建事件""" | |
| 63 | + order = webhook_data.get('order') | |
| 64 | + if order: | |
| 65 | + current_app.logger.info(f"New order created: {order.get('id')}") | |
| 66 | + | |
| 67 | +def handle_order_updated(webhook_data): | |
| 68 | + """处理订单更新事件""" | |
| 69 | + order = webhook_data.get('order') | |
| 70 | + if order: | |
| 71 | + current_app.logger.info(f"Order updated: {order.get('id')}") | |
| 72 | + | |
| 73 | +def handle_product_created(webhook_data): | |
| 74 | + """处理产品创建事件""" | |
| 75 | + product = webhook_data.get('product') | |
| 76 | + if product: | |
| 77 | + current_app.logger.info(f"New product created: {product.get('id')}") | ... | ... |
| 1 | +++ a/old/run.py | |
| ... | ... | @@ -0,0 +1,40 @@ |
| 1 | +#!/usr/bin/env python3 | |
| 2 | +""" | |
| 3 | +Shoplazza OAuth2.0 后端应用启动脚本 | |
| 4 | +""" | |
| 5 | + | |
| 6 | +import os | |
| 7 | +import sys | |
| 8 | +from app import create_app | |
| 9 | + | |
| 10 | +def main(): | |
| 11 | + """主函数""" | |
| 12 | + # 检查环境变量 | |
| 13 | + required_vars = ['CLIENT_ID', 'CLIENT_SECRET', 'BASE_URL'] | |
| 14 | + missing_vars = [var for var in required_vars if not os.getenv(var)] | |
| 15 | + | |
| 16 | + if missing_vars: | |
| 17 | + print(f"错误: 缺少必需的环境变量: {', '.join(missing_vars)}") | |
| 18 | + print("请创建 .env 文件并配置以下变量:") | |
| 19 | + print("CLIENT_ID=your_client_id_here") | |
| 20 | + print("CLIENT_SECRET=your_client_secret_here") | |
| 21 | + print("BASE_URL=https://your-domain.com") | |
| 22 | + sys.exit(1) | |
| 23 | + | |
| 24 | + # 创建应用 | |
| 25 | + app = create_app() | |
| 26 | + | |
| 27 | + # 启动应用 | |
| 28 | + print("🚀 启动 Shoplazza OAuth2.0 后端服务...") | |
| 29 | + print(f"📡 服务地址: http://localhost:{app.config['PORT']}") | |
| 30 | + print(f"🔗 认证端点: {app.config['BASE_URL']}/auth/install?shop=your-shop.myshoplaza.com") | |
| 31 | + print(f"📋 API文档: {app.config['BASE_URL']}/") | |
| 32 | + | |
| 33 | + app.run( | |
| 34 | + host='0.0.0.0', | |
| 35 | + port=app.config['PORT'], | |
| 36 | + debug=app.config['DEBUG'] | |
| 37 | + ) | |
| 38 | + | |
| 39 | +if __name__ == '__main__': | |
| 40 | + main() | ... | ... |
| 1 | +++ a/old/step_by_step_oauth.md | |
| ... | ... | @@ -0,0 +1,184 @@ |
| 1 | +# OAuth2.0 认证步骤详解 | |
| 2 | + | |
| 3 | +## 🎯 完整认证流程 | |
| 4 | + | |
| 5 | +### 步骤1: 准备阶段 | |
| 6 | + | |
| 7 | +#### 1.1 获取Shoplazza应用凭证 | |
| 8 | +``` | |
| 9 | +1. 访问 https://partners.shoplazza.com/ | |
| 10 | +2. 登录并创建新应用 | |
| 11 | +3. 记录 CLIENT_ID 和 CLIENT_SECRET | |
| 12 | +``` | |
| 13 | + | |
| 14 | +#### 1.2 设置本地开发环境 | |
| 15 | +```bash | |
| 16 | +# 安装依赖 | |
| 17 | +pip install -r requirements.txt | |
| 18 | + | |
| 19 | +# 安装ngrok (用于本地开发) | |
| 20 | +npm install -g ngrok | |
| 21 | +``` | |
| 22 | + | |
| 23 | +#### 1.3 启动ngrok隧道 | |
| 24 | +```bash | |
| 25 | +# 在终端1中启动ngrok | |
| 26 | +ngrok http 3000 | |
| 27 | + | |
| 28 | +# 记录ngrok提供的HTTPS URL,例如: | |
| 29 | +# https://abc123.ngrok.io | |
| 30 | +``` | |
| 31 | + | |
| 32 | +### 步骤2: 配置应用 | |
| 33 | + | |
| 34 | +#### 2.1 创建环境配置文件 | |
| 35 | +创建 `.env` 文件: | |
| 36 | +```env | |
| 37 | +CLIENT_ID=your_actual_client_id_from_shoplazza | |
| 38 | +CLIENT_SECRET=your_actual_client_secret_from_shoplazza | |
| 39 | +BASE_URL=https://abc123.ngrok.io | |
| 40 | +REDIRECT_URI=/auth/shoplazza/callback | |
| 41 | +SECRET_KEY=your-random-secret-key | |
| 42 | +FLASK_ENV=development | |
| 43 | +FLASK_DEBUG=True | |
| 44 | +PORT=3000 | |
| 45 | +``` | |
| 46 | + | |
| 47 | +#### 2.2 在Shoplazza开发者中心配置URL | |
| 48 | +``` | |
| 49 | +应用URL: https://abc123.ngrok.io/auth/install | |
| 50 | +回调URL: https://abc123.ngrok.io/auth/shoplazza/callback | |
| 51 | +Webhook URL: https://abc123.ngrok.io/webhook/shoplazza | |
| 52 | +``` | |
| 53 | + | |
| 54 | +### 步骤3: 启动应用 | |
| 55 | + | |
| 56 | +```bash | |
| 57 | +# 在终端2中启动应用 | |
| 58 | +python run.py | |
| 59 | +``` | |
| 60 | + | |
| 61 | +您应该看到类似输出: | |
| 62 | +``` | |
| 63 | +🚀 启动 Shoplazza OAuth2.0 后端服务... | |
| 64 | +📡 服务地址: http://localhost:3000 | |
| 65 | +🔗 认证端点: https://abc123.ngrok.io/auth/install?shop=your-shop.myshoplaza.com | |
| 66 | +📋 API文档: https://abc123.ngrok.io/ | |
| 67 | +``` | |
| 68 | + | |
| 69 | +### 步骤4: 执行OAuth认证 | |
| 70 | + | |
| 71 | +#### 4.1 开始认证流程 | |
| 72 | +在浏览器中访问: | |
| 73 | +``` | |
| 74 | +https://abc123.ngrok.io/auth/install?shop=your-shop.myshoplaza.com | |
| 75 | +``` | |
| 76 | + | |
| 77 | +**注意**: 将 `your-shop.myshoplaza.com` 替换为实际的商店域名 | |
| 78 | + | |
| 79 | +#### 4.2 授权确认 | |
| 80 | +1. 系统会重定向到Shoplazza授权页面 | |
| 81 | +2. 商家点击"安装应用"或"授权" | |
| 82 | +3. 系统自动处理回调 | |
| 83 | + | |
| 84 | +#### 4.3 验证认证结果 | |
| 85 | +访问以下URL查看认证状态: | |
| 86 | +``` | |
| 87 | +https://abc123.ngrok.io/auth/tokens | |
| 88 | +``` | |
| 89 | + | |
| 90 | +### 步骤5: 测试API调用 | |
| 91 | + | |
| 92 | +#### 5.1 获取客户列表 | |
| 93 | +```bash | |
| 94 | +curl https://abc123.ngrok.io/api/customers/your-shop.myshoplaza.com | |
| 95 | +``` | |
| 96 | + | |
| 97 | +#### 5.2 获取产品列表 | |
| 98 | +```bash | |
| 99 | +curl https://abc123.ngrok.io/api/products/your-shop.myshoplaza.com | |
| 100 | +``` | |
| 101 | + | |
| 102 | +#### 5.3 获取订单列表 | |
| 103 | +```bash | |
| 104 | +curl https://abc123.ngrok.io/api/orders/your-shop.myshoplaza.com | |
| 105 | +``` | |
| 106 | + | |
| 107 | +#### 5.4 获取商店信息 | |
| 108 | +```bash | |
| 109 | +curl https://abc123.ngrok.io/api/shop_info/your-shop.myshoplaza.com | |
| 110 | +``` | |
| 111 | + | |
| 112 | +## 🔍 认证流程详解 | |
| 113 | + | |
| 114 | +### OAuth2.0 流程图 | |
| 115 | +``` | |
| 116 | +商家 → 应用安装 → 授权页面 → 确认授权 → 回调处理 → 获取令牌 → API调用 | |
| 117 | +``` | |
| 118 | + | |
| 119 | +### 详细步骤说明 | |
| 120 | + | |
| 121 | +1. **应用安装请求** | |
| 122 | + - 商家从应用商店安装应用 | |
| 123 | + - Shoplazza发送请求到 `/auth/install` | |
| 124 | + - 应用重定向到Shoplazza授权页面 | |
| 125 | + | |
| 126 | +2. **用户授权** | |
| 127 | + - 商家在Shoplazza页面确认授权 | |
| 128 | + - 选择需要的权限范围 | |
| 129 | + - 点击"安装应用" | |
| 130 | + | |
| 131 | +3. **授权回调** | |
| 132 | + - Shoplazza重定向到 `/auth/shoplazza/callback` | |
| 133 | + - 携带授权码和HMAC签名 | |
| 134 | + - 应用验证HMAC签名 | |
| 135 | + | |
| 136 | +4. **令牌交换** | |
| 137 | + - 应用使用授权码请求访问令牌 | |
| 138 | + - Shoplazza返回访问令牌和刷新令牌 | |
| 139 | + - 应用存储令牌用于后续API调用 | |
| 140 | + | |
| 141 | +5. **API调用** | |
| 142 | + - 使用访问令牌调用Shoplazza API | |
| 143 | + - 获取客户、产品、订单等数据 | |
| 144 | + | |
| 145 | +## 🛠️ 故障排除 | |
| 146 | + | |
| 147 | +### 常见错误及解决方案 | |
| 148 | + | |
| 149 | +1. **"Missing HMAC parameter"** | |
| 150 | + - 检查请求是否来自Shoplazza | |
| 151 | + - 确认URL参数完整 | |
| 152 | + | |
| 153 | +2. **"HMAC validation failed"** | |
| 154 | + - 检查CLIENT_SECRET配置 | |
| 155 | + - 确认请求参数顺序正确 | |
| 156 | + | |
| 157 | +3. **"Shop not authorized"** | |
| 158 | + - 确认已完成OAuth认证 | |
| 159 | + - 检查访问令牌是否有效 | |
| 160 | + | |
| 161 | +4. **"Failed to obtain access token"** | |
| 162 | + - 检查CLIENT_ID和CLIENT_SECRET | |
| 163 | + - 确认回调URL配置正确 | |
| 164 | + | |
| 165 | +### 调试命令 | |
| 166 | + | |
| 167 | +```bash | |
| 168 | +# 检查应用状态 | |
| 169 | +curl https://abc123.ngrok.io/health | |
| 170 | + | |
| 171 | +# 查看已授权的商店 | |
| 172 | +curl https://abc123.ngrok.io/auth/tokens | |
| 173 | + | |
| 174 | +# 刷新访问令牌 | |
| 175 | +curl https://abc123.ngrok.io/auth/refresh_token/your-shop.myshoplaza.com | |
| 176 | +``` | |
| 177 | + | |
| 178 | +## 📝 注意事项 | |
| 179 | + | |
| 180 | +1. **HTTPS要求**: 生产环境必须使用HTTPS | |
| 181 | +2. **令牌安全**: 生产环境应使用数据库存储令牌 | |
| 182 | +3. **错误处理**: 实现适当的错误处理和日志记录 | |
| 183 | +4. **权限范围**: 只请求应用实际需要的权限 | |
| 184 | +5. **令牌刷新**: 定期刷新访问令牌以保持有效性 | ... | ... |
| 1 | +++ a/old/test_oauth.py | |
| ... | ... | @@ -0,0 +1,137 @@ |
| 1 | +#!/usr/bin/env python3 | |
| 2 | +""" | |
| 3 | +OAuth2.0 认证测试脚本 | |
| 4 | +用于验证Shoplazza OAuth流程是否正常工作 | |
| 5 | +""" | |
| 6 | + | |
| 7 | +import requests | |
| 8 | +import json | |
| 9 | +import os | |
| 10 | +from dotenv import load_dotenv | |
| 11 | + | |
| 12 | +# 加载环境变量 | |
| 13 | +load_dotenv() | |
| 14 | + | |
| 15 | +def test_health_check(base_url): | |
| 16 | + """测试健康检查端点""" | |
| 17 | + try: | |
| 18 | + response = requests.get(f"{base_url}/health") | |
| 19 | + if response.status_code == 200: | |
| 20 | + print("✅ 健康检查通过") | |
| 21 | + print(f" 状态: {response.json()}") | |
| 22 | + return True | |
| 23 | + else: | |
| 24 | + print(f"❌ 健康检查失败: {response.status_code}") | |
| 25 | + return False | |
| 26 | + except Exception as e: | |
| 27 | + print(f"❌ 健康检查异常: {str(e)}") | |
| 28 | + return False | |
| 29 | + | |
| 30 | +def test_auth_endpoints(base_url): | |
| 31 | + """测试认证端点""" | |
| 32 | + print("\n🔐 测试认证端点...") | |
| 33 | + | |
| 34 | + # 测试根端点 | |
| 35 | + try: | |
| 36 | + response = requests.get(base_url) | |
| 37 | + if response.status_code == 200: | |
| 38 | + print("✅ 根端点正常") | |
| 39 | + endpoints = response.json().get('endpoints', {}) | |
| 40 | + print(f" 可用端点: {list(endpoints.keys())}") | |
| 41 | + else: | |
| 42 | + print(f"❌ 根端点异常: {response.status_code}") | |
| 43 | + except Exception as e: | |
| 44 | + print(f"❌ 根端点异常: {str(e)}") | |
| 45 | + | |
| 46 | +def test_oauth_flow(base_url, shop_domain): | |
| 47 | + """测试OAuth流程""" | |
| 48 | + print(f"\n🔄 测试OAuth流程 (商店: {shop_domain})...") | |
| 49 | + | |
| 50 | + # 构建认证URL | |
| 51 | + auth_url = f"{base_url}/auth/install?shop={shop_domain}" | |
| 52 | + print(f"认证URL: {auth_url}") | |
| 53 | + | |
| 54 | + # 测试认证端点(不跟随重定向) | |
| 55 | + try: | |
| 56 | + response = requests.get(auth_url, allow_redirects=False) | |
| 57 | + if response.status_code in [302, 301]: | |
| 58 | + print("✅ 认证重定向正常") | |
| 59 | + print(f" 重定向到: {response.headers.get('Location', 'N/A')}") | |
| 60 | + else: | |
| 61 | + print(f"❌ 认证重定向异常: {response.status_code}") | |
| 62 | + except Exception as e: | |
| 63 | + print(f"❌ 认证端点异常: {str(e)}") | |
| 64 | + | |
| 65 | +def test_api_endpoints(base_url, shop_domain): | |
| 66 | + """测试API端点""" | |
| 67 | + print(f"\n📡 测试API端点 (商店: {shop_domain})...") | |
| 68 | + | |
| 69 | + endpoints = [ | |
| 70 | + f"/api/customers/{shop_domain}", | |
| 71 | + f"/api/products/{shop_domain}", | |
| 72 | + f"/api/orders/{shop_domain}", | |
| 73 | + f"/api/shop_info/{shop_domain}" | |
| 74 | + ] | |
| 75 | + | |
| 76 | + for endpoint in endpoints: | |
| 77 | + try: | |
| 78 | + response = requests.get(f"{base_url}{endpoint}") | |
| 79 | + if response.status_code == 401: | |
| 80 | + print(f"⚠️ {endpoint} - 需要认证 (正常)") | |
| 81 | + elif response.status_code == 200: | |
| 82 | + print(f"✅ {endpoint} - 认证成功") | |
| 83 | + else: | |
| 84 | + print(f"❌ {endpoint} - 异常状态: {response.status_code}") | |
| 85 | + except Exception as e: | |
| 86 | + print(f"❌ {endpoint} - 异常: {str(e)}") | |
| 87 | + | |
| 88 | +def test_tokens_endpoint(base_url): | |
| 89 | + """测试令牌端点""" | |
| 90 | + print("\n🔑 测试令牌端点...") | |
| 91 | + | |
| 92 | + try: | |
| 93 | + response = requests.get(f"{base_url}/auth/tokens") | |
| 94 | + if response.status_code == 200: | |
| 95 | + tokens = response.json() | |
| 96 | + print("✅ 令牌端点正常") | |
| 97 | + print(f" 已授权商店: {tokens.get('authorized_shops', [])}") | |
| 98 | + if tokens.get('tokens'): | |
| 99 | + print(f" 令牌数量: {len(tokens.get('tokens', {}))}") | |
| 100 | + else: | |
| 101 | + print(f"❌ 令牌端点异常: {response.status_code}") | |
| 102 | + except Exception as e: | |
| 103 | + print(f"❌ 令牌端点异常: {str(e)}") | |
| 104 | + | |
| 105 | +def main(): | |
| 106 | + """主测试函数""" | |
| 107 | + print("🚀 Shoplazza OAuth2.0 认证测试") | |
| 108 | + print("=" * 50) | |
| 109 | + | |
| 110 | + # 获取配置 | |
| 111 | + base_url = os.getenv('BASE_URL', 'http://localhost:3000') | |
| 112 | + shop_domain = input("请输入商店域名 (例如: your-shop.myshoplaza.com): ").strip() | |
| 113 | + | |
| 114 | + if not shop_domain: | |
| 115 | + shop_domain = "your-shop.myshoplaza.com" | |
| 116 | + print(f"使用默认商店域名: {shop_domain}") | |
| 117 | + | |
| 118 | + print(f"\n测试配置:") | |
| 119 | + print(f" 基础URL: {base_url}") | |
| 120 | + print(f" 商店域名: {shop_domain}") | |
| 121 | + | |
| 122 | + # 执行测试 | |
| 123 | + if test_health_check(base_url): | |
| 124 | + test_auth_endpoints(base_url) | |
| 125 | + test_oauth_flow(base_url, shop_domain) | |
| 126 | + test_api_endpoints(base_url, shop_domain) | |
| 127 | + test_tokens_endpoint(base_url) | |
| 128 | + | |
| 129 | + print("\n" + "=" * 50) | |
| 130 | + print("🎯 测试完成!") | |
| 131 | + print("\n下一步操作:") | |
| 132 | + print(f"1. 在浏览器中访问: {base_url}/auth/install?shop={shop_domain}") | |
| 133 | + print("2. 完成OAuth认证流程") | |
| 134 | + print("3. 重新运行此测试脚本验证API调用") | |
| 135 | + | |
| 136 | +if __name__ == "__main__": | |
| 137 | + main() | ... | ... |
| 1 | +++ a/old/后端OAuth2.0认证流程.md | |
| ... | ... | @@ -0,0 +1,128 @@ |
| 1 | + | |
| 2 | +后端OAuth2.0认证流程 | |
| 3 | +1. 安装依赖 | |
| 4 | +Codeblock | |
| 5 | + 1 | |
| 6 | + npm install express crypto axios | |
| 7 | + 2. 配置⽂件/config/index.js | |
| 8 | +⽤于存放APP_NAME,CLIENT_ID,CLIENT_SECRET,REDIRECT_URI | |
| 9 | + Codeblock | |
| 10 | + | |
| 11 | + const CLIENT_ID = "<YOUR_CLIENT_ID>"; | |
| 12 | + const CLIENT_SECRET = "<YOUR_CLIENT_SECRET>"; | |
| 13 | + const BASE_URL = "<BASE_URL>"; | |
| 14 | + const REDIRECT_URI = `${BASE_URL}/auth/shoplazza/callback`; | |
| 15 | + let access_token = {}; | |
| 16 | + 3. HMAC校验中间件/middleware/hmacValidator.js | |
| 17 | + Codeblock | |
| 18 | + | |
| 19 | + 9 | |
| 20 | + import crypto from "crypto"; | |
| 21 | + export const hmacValidator = async (ctx, next) => { | |
| 22 | + const { hmac, ...queryParams } = ctx.query; | |
| 23 | + if (!hmac) { | |
| 24 | + ctx.status = 400; | |
| 25 | + ctx.body = { error: "Missing HMAC parameter" }; | |
| 26 | + return; | |
| 27 | + | |
| 28 | + } | |
| 29 | + // | |
| 30 | +计算 | |
| 31 | + HMAC | |
| 32 | +校验 | |
| 33 | + | |
| 34 | +const message = Object.keys(queryParams) | |
| 35 | + .sort() | |
| 36 | + .map((key) => `${key}=${queryParams[key]}`) | |
| 37 | + .join("&"); | |
| 38 | + const generatedHmac = crypto | |
| 39 | + .createHmac("sha256", process.env.CLIENT_SECRET) | |
| 40 | + .update(message) | |
| 41 | + .digest("hex"); | |
| 42 | + if (crypto.timingSafeEqual(Buffer.from(generatedHmac), Buffer.from(hmac))) { | |
| 43 | + await next(); | |
| 44 | + } else { | |
| 45 | + ctx.status = 403; | |
| 46 | + ctx.body = { error: "HMAC validation failed" }; | |
| 47 | + } | |
| 48 | + }; | |
| 49 | + 4. 认证路由/routes/auth.js | |
| 50 | + Codeblock | |
| 51 | + | |
| 52 | + import Router from "koa-router"; | |
| 53 | + import crypto from "crypto"; | |
| 54 | + import axios from "axios"; | |
| 55 | + import { APP_NAME, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI } from | |
| 56 | +"../config/index.js"; | |
| 57 | + import { hmacValidator } from "../middleware/hmacValidator.js"; | |
| 58 | + const router = new Router({ prefix: "/api" }); | |
| 59 | + router.get(`/auth/install`, async (ctx) => { | |
| 60 | + const shop = ctx.query.shop; | |
| 61 | + if (!shop) { | |
| 62 | + ctx.status = 400; | |
| 63 | + ctx.body = { error: "Missing shop parameter" }; | |
| 64 | + return; | |
| 65 | + } | |
| 66 | + const scopes = "read_customer,read_product,write_product"; | |
| 67 | + const state = crypto.randomBytes(16).toString("hex"); | |
| 68 | + const redirectUri = `https://${ctx.host}${REDIRECT_URI}`; | |
| 69 | + | |
| 70 | + const authUrl = `https://${shop}/admin/oauth/authorize? | |
| 71 | + client_id=${CLIENT_ID}&scope=${scopes}&redirect_uri=${redirectUri}&response_typ | |
| 72 | + e=code&state=${state}`; | |
| 73 | + ctx.redirect(authUrl); | |
| 74 | + }); | |
| 75 | + // ** Step 2: | |
| 76 | +处理 | |
| 77 | + OAuth | |
| 78 | +回调 | |
| 79 | +** | |
| 80 | +router.get(`/auth/callback`, hmacValidator, async (ctx) => { | |
| 81 | + const { code, shop } = ctx.query; | |
| 82 | + if (!shop || !code) { | |
| 83 | + ctx.status = 400; | |
| 84 | + ctx.body = { error: "Missing required parameters" }; | |
| 85 | + return; | |
| 86 | + } | |
| 87 | + const redirectUri = `https://${ctx.host}${REDIRECT_URI}`; | |
| 88 | + try { | |
| 89 | + const response = await axios.post(`https://${shop}/admin/oauth/token`, { | |
| 90 | + client_id: CLIENT_ID, | |
| 91 | + client_secret: CLIENT_SECRET, | |
| 92 | + code, | |
| 93 | + grant_type: "authorization_code", | |
| 94 | + redirect_uri: redirectUri, | |
| 95 | + }); | |
| 96 | + console.log(" | |
| 97 | +获取 | |
| 98 | + access_token | |
| 99 | +成功 | |
| 100 | +:", response.data); | |
| 101 | + ctx.body = response.data; // | |
| 102 | +返回 | |
| 103 | + token | |
| 104 | +及 | |
| 105 | + store | |
| 106 | +信息 | |
| 107 | + | |
| 108 | +} catch (error) { | |
| 109 | + console.error(" | |
| 110 | +获取 | |
| 111 | + access_token | |
| 112 | +失败 | |
| 113 | +:", error.message); | |
| 114 | + ctx.status = 500; | |
| 115 | + ctx.body = { error: "Failed to obtain access token" }; | |
| 116 | + } | |
| 117 | + }); | |
| 118 | + export default router; | |
| 119 | + 5. 启动服务器挂载oauth,认证路由 | |
| 120 | +Codeblock | |
| 121 | + 1 | |
| 122 | + 2 | |
| 123 | + const PORT = process.env.PORT || 3000; | |
| 124 | + app.listen(PORT, () => { | |
| 125 | +3 | |
| 126 | + 4 | |
| 127 | + console.log(`Server running on http://localhost:${PORT}`); | |
| 128 | + }); | |
| 0 | 129 | \ No newline at end of file | ... | ... |
| 1 | +++ a/python-app-demo/README.md | |
| ... | ... | @@ -0,0 +1,90 @@ |
| 1 | +# Python OAuth2 认证服务器 | |
| 2 | + | |
| 3 | +这是一个用 Flask 编写的 OAuth2 认证服务器,实现了与 Node.js 版本相同的功能。 | |
| 4 | + | |
| 5 | +## 功能特性 | |
| 6 | + | |
| 7 | +- ✅ OAuth2 认证流程 | |
| 8 | +- ✅ HMAC-SHA256 签名验证 | |
| 9 | +- ✅ Timing-safe 比较防止时序攻击 | |
| 10 | +- ✅ 支持 Shopify OAuth 集成 | |
| 11 | +- ✅ CORS 跨域支持 | |
| 12 | +- ✅ 环境变量配置 | |
| 13 | + | |
| 14 | +## 快速开始 | |
| 15 | + | |
| 16 | +### 1. 安装依赖 | |
| 17 | + | |
| 18 | +```bash | |
| 19 | +pip install -r requirements.txt | |
| 20 | +``` | |
| 21 | + | |
| 22 | +### 2. 运行服务器 | |
| 23 | + | |
| 24 | +```bash | |
| 25 | +python main.py | |
| 26 | +``` | |
| 27 | + | |
| 28 | +服务器将在 `http://localhost:4000` 运行 | |
| 29 | + | |
| 30 | +## API 端点 | |
| 31 | + | |
| 32 | +### 1. 启动 OAuth 认证 | |
| 33 | + | |
| 34 | +``` | |
| 35 | +GET /api/auth?shop=example.myshopify.com | |
| 36 | +``` | |
| 37 | + | |
| 38 | +重定向用户到 Shopify 授权页面。 | |
| 39 | + | |
| 40 | +### 2. OAuth 回调处理 | |
| 41 | + | |
| 42 | +``` | |
| 43 | +GET /api/auth/callback?code=xxx&hmac=xxx&state=xxx&shop=example.myshopify.com | |
| 44 | +``` | |
| 45 | + | |
| 46 | +- 验证 HMAC 签名 | |
| 47 | +- 交换授权码获取访问令牌 | |
| 48 | +- 返回令牌信息 | |
| 49 | + | |
| 50 | +## 项目结构 | |
| 51 | + | |
| 52 | +``` | |
| 53 | +python-app-demo/ | |
| 54 | +├── main.py # 主应用文件 | |
| 55 | +├── requirements.txt # Python 依赖 | |
| 56 | +├── .env # 环境变量(需要自己创建) | |
| 57 | +└── README.md # 说明文档 | |
| 58 | +``` | |
| 59 | + | |
| 60 | +## 与 Node.js 版本的对应关系 | |
| 61 | + | |
| 62 | +| Node.js (devServer) | Python (python-app-demo) | | |
| 63 | +|-------------------|----------------------| | |
| 64 | +| Express 服务器 | Flask 服务器 | | |
| 65 | +| hmacValidatorMiddleWare | @hmac_validator 装饰器 | | |
| 66 | +| secureCompare | secure_compare 函数 | | |
| 67 | +| crypto.timingSafeEqual | hmac.compare_digest | | |
| 68 | +| /api/auth | /api/auth 路由 | | |
| 69 | +| /api/auth/callback | /api/auth/callback 路由 | | |
| 70 | + | |
| 71 | +## 安全特性 | |
| 72 | + | |
| 73 | +1. **HMAC-SHA256 验证**:所有回调请求都通过 HMAC 签名验证 | |
| 74 | +2. **Timing-safe 比较**:使用 `hmac.compare_digest` 防止时序攻击 | |
| 75 | +3. **环境变量管理**:敏感配置存储在 `.env` 文件中 | |
| 76 | +4. **CORS 支持**:安全的跨域资源共享 | |
| 77 | + | |
| 78 | +## 使用 curl 测试 | |
| 79 | + | |
| 80 | +```bash | |
| 81 | +# 测试认证端点 | |
| 82 | +curl "http://localhost:4000/api/auth?shop=example.myshopify.com" | |
| 83 | + | |
| 84 | +# 测试回调端点(需要有效的HMAC) | |
| 85 | +curl "http://localhost:4000/api/auth/callback?code=test&hmac=xxx&state=xxx&shop=example.myshopify.com" | |
| 86 | +``` | |
| 87 | + | |
| 88 | +## 完成! | |
| 89 | + | |
| 90 | +现在您有一个完整的 Python OAuth2 认证服务器,功能与 Node.js 版本完全相同。🚀 | ... | ... |
| 1 | +++ a/python-app-demo/main.py | |
| ... | ... | @@ -0,0 +1,129 @@ |
| 1 | +from flask import Flask, request, redirect, jsonify | |
| 2 | +from flask_cors import CORS | |
| 3 | +import hmac | |
| 4 | +import hashlib | |
| 5 | +import secrets | |
| 6 | +import requests | |
| 7 | + | |
| 8 | +app = Flask(__name__) | |
| 9 | +CORS(app) | |
| 10 | + | |
| 11 | +# 配置 - 直接写死 | |
| 12 | +CLIENT_ID = "J2ntcUvAKRq_xVXcmysAc6iUJ-MYJF4JtoMKJGi_I9A" | |
| 13 | +CLIENT_SECRET = "WvmwpHVf1u1hfYEhsc7LPl0YxrbiFy3A2JVBhy8PWAU" | |
| 14 | +REDIRECT_URI = "https://rooster-wired-monster.ngrok-free.app/api/auth/callback" | |
| 15 | + | |
| 16 | +# 工具函数:安全比较HMAC | |
| 17 | +def secure_compare(a, b): | |
| 18 | + """ | |
| 19 | + 使用timing-safe比较来防止时序攻击 | |
| 20 | + """ | |
| 21 | + return hmac.compare_digest(a, b) | |
| 22 | + | |
| 23 | +# 中间件:HMAC验证 | |
| 24 | +def hmac_validator(func): | |
| 25 | + """ | |
| 26 | + HMAC验证装饰器 - 验证请求的HMAC签名 | |
| 27 | + """ | |
| 28 | + def wrapper(*args, **kwargs): | |
| 29 | + # 获取查询参数 | |
| 30 | + code = request.args.get('code') | |
| 31 | + hmac_value = request.args.get('hmac') | |
| 32 | + state = request.args.get('state') | |
| 33 | + shop = request.args.get('shop') | |
| 34 | + | |
| 35 | + if not hmac_value: | |
| 36 | + return jsonify({"error": "HMAC validation failed"}), 400 | |
| 37 | + | |
| 38 | + # 构建消息 | |
| 39 | + params = dict(request.args) | |
| 40 | + del params['hmac'] # 移除hmac参数 | |
| 41 | + | |
| 42 | + # 按字典序排序参数并构建消息字符串 | |
| 43 | + sorted_keys = sorted(params.keys()) | |
| 44 | + message = "&".join([f"{key}={params[key]}" for key in sorted_keys]) | |
| 45 | + | |
| 46 | + # 生成HMAC哈希 | |
| 47 | + generated_hash = hmac.new( | |
| 48 | + CLIENT_SECRET.encode(), | |
| 49 | + message.encode(), | |
| 50 | + hashlib.sha256 | |
| 51 | + ).hexdigest() | |
| 52 | + | |
| 53 | + # 比较HMAC | |
| 54 | + if not secure_compare(generated_hash, hmac_value): | |
| 55 | + return jsonify({"error": "HMAC validation failed"}), 400 | |
| 56 | + | |
| 57 | + return func(*args, **kwargs) | |
| 58 | + | |
| 59 | + wrapper.__name__ = func.__name__ | |
| 60 | + return wrapper | |
| 61 | + | |
| 62 | +# OAuth认证路由 | |
| 63 | +@app.route('/api/auth', methods=['GET']) | |
| 64 | +def auth(): | |
| 65 | + """ | |
| 66 | + OAuth认证端点 - 重定向到Shopify授权页面 | |
| 67 | + """ | |
| 68 | + shop = request.args.get('shop') | |
| 69 | + if not shop: | |
| 70 | + return jsonify({"error": "shop parameter is required"}), 400 | |
| 71 | + | |
| 72 | + scopes = "read_customer" | |
| 73 | + state = secrets.token_hex(16) # 生成随机state | |
| 74 | + | |
| 75 | + auth_url = ( | |
| 76 | + f"https://{shop}/admin/oauth/authorize?" | |
| 77 | + f"client_id={CLIENT_ID}" | |
| 78 | + f"&scope={scopes}" | |
| 79 | + f"&redirect_uri={REDIRECT_URI}" | |
| 80 | + f"&response_type=code" | |
| 81 | + f"&state={state}" | |
| 82 | + ) | |
| 83 | + | |
| 84 | + return redirect(auth_url) | |
| 85 | + | |
| 86 | +# OAuth回调路由 | |
| 87 | +@app.route('/api/auth/callback', methods=['GET']) | |
| 88 | +@hmac_validator | |
| 89 | +def auth_callback(): | |
| 90 | + """ | |
| 91 | + OAuth回调端点 - 交换授权码获取访问令牌 | |
| 92 | + """ | |
| 93 | + code = request.args.get('code') | |
| 94 | + shop = request.args.get('shop') | |
| 95 | + | |
| 96 | + if not shop or not code: | |
| 97 | + return jsonify({"error": "Required parameters missing"}), 400 | |
| 98 | + | |
| 99 | + try: | |
| 100 | + # 请求访问令牌 | |
| 101 | + token_url = f"https://{shop}/admin/oauth/token" | |
| 102 | + data = { | |
| 103 | + "client_id": CLIENT_ID, | |
| 104 | + "client_secret": CLIENT_SECRET, | |
| 105 | + "code": code, | |
| 106 | + "grant_type": "authorization_code", | |
| 107 | + "redirect_uri": REDIRECT_URI, | |
| 108 | + } | |
| 109 | + | |
| 110 | + response = requests.post(token_url, json=data) | |
| 111 | + response.raise_for_status() | |
| 112 | + | |
| 113 | + token_data = response.json() | |
| 114 | + print("token data:", token_data) | |
| 115 | + | |
| 116 | + return jsonify(token_data) | |
| 117 | + | |
| 118 | + # 这里可以调用Shoplazza OpenAPI获取更多数据 | |
| 119 | + # headers = {"Access-Token": token_data.get("access_token")} | |
| 120 | + # customers_url = f"https://{shop}/openapi/2022-01/customers" | |
| 121 | + # customers_response = requests.get(customers_url, headers=headers) | |
| 122 | + # return redirect(LAST_URL) | |
| 123 | + | |
| 124 | + except requests.exceptions.RequestException as e: | |
| 125 | + print(f"Error: {e}") | |
| 126 | + return jsonify({"error": "Failed to get access token"}), 500 | |
| 127 | + | |
| 128 | +if __name__ == '__main__': | |
| 129 | + app.run(host='0.0.0.0', port=4000, debug=True) | ... | ... |