auth.py
4.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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/<shop>')
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
})