452 lines
16 KiB
Python
452 lines
16 KiB
Python
from flask import request, jsonify, current_app
|
||
from datetime import datetime, timedelta
|
||
from app import db
|
||
from app.models import Product, License, Device, Version
|
||
from app.utils.crypto import generate_hash, verify_hash, generate_signature
|
||
from . import api_bp
|
||
|
||
@api_bp.route('/auth/verify', methods=['POST'])
|
||
def verify_license():
|
||
"""验证卡密接口"""
|
||
try:
|
||
# 检查Content-Type
|
||
content_type = request.content_type
|
||
if content_type and 'application/json' not in content_type:
|
||
current_app.logger.warning(f"验证请求:Content-Type不正确 - {content_type}")
|
||
|
||
data = request.get_json(force=True) # 强制解析JSON,即使Content-Type不正确
|
||
if not data:
|
||
# 尝试获取原始数据用于调试
|
||
raw_data = request.get_data(as_text=True)
|
||
current_app.logger.warning(f"验证请求:请求数据为空或无法解析 - Content-Type: {content_type}, 原始数据: {raw_data[:200] if raw_data else 'None'}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空或格式错误'
|
||
}), 400
|
||
|
||
# 获取请求参数
|
||
software_id = data.get('software_id')
|
||
license_key = data.get('license_key')
|
||
machine_code = data.get('machine_code')
|
||
timestamp = data.get('timestamp')
|
||
signature = data.get('signature')
|
||
|
||
# 验证必填参数,并记录缺失的参数
|
||
missing_params = []
|
||
if not software_id:
|
||
missing_params.append('software_id')
|
||
if not license_key:
|
||
missing_params.append('license_key')
|
||
if not machine_code:
|
||
missing_params.append('machine_code')
|
||
if timestamp is None:
|
||
missing_params.append('timestamp')
|
||
if not signature:
|
||
missing_params.append('signature')
|
||
|
||
if missing_params:
|
||
current_app.logger.warning(f"验证请求:缺少必要参数 - {', '.join(missing_params)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'缺少必要参数: {", ".join(missing_params)}'
|
||
}), 400
|
||
|
||
# 验证时间戳(防止重放攻击)
|
||
try:
|
||
# 确保timestamp是数字类型
|
||
if isinstance(timestamp, str):
|
||
timestamp = int(timestamp)
|
||
# 使用utcfromtimestamp确保时间戳被解析为UTC时间(与time.time()生成的UTC时间戳一致)
|
||
request_time = datetime.utcfromtimestamp(timestamp)
|
||
# 使用UTC时间进行比较
|
||
current_time = datetime.utcnow()
|
||
time_diff = abs((current_time - request_time).total_seconds())
|
||
if time_diff > 300: # 5分钟有效期
|
||
current_app.logger.warning(f"验证请求:请求已过期 - 时间差: {time_diff}秒, 当前时间: {current_time}, 请求时间: {request_time}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求已过期'
|
||
}), 400
|
||
except (ValueError, TypeError) as e:
|
||
current_app.logger.warning(f"验证请求:时间戳格式错误 - timestamp: {timestamp}, 错误: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'时间戳格式错误: {str(e)}'
|
||
}), 400
|
||
|
||
# 验证签名
|
||
secret_key = current_app.config.get('AUTH_SECRET_KEY', 'default-secret-key')
|
||
signature_data = f"{software_id}{license_key}{machine_code}{timestamp}"
|
||
expected_signature = generate_signature(signature_data, secret_key)
|
||
|
||
# 记录请求信息(不记录敏感信息)
|
||
current_app.logger.info(f"验证请求 - software_id: {software_id}, machine_code: {machine_code[:8]}..., timestamp: {timestamp}")
|
||
|
||
# 调试信息(仅在调试模式记录,且不输出密钥)
|
||
if current_app.debug:
|
||
current_app.logger.debug(f"签名数据: {signature_data}")
|
||
current_app.logger.debug(f"客户端签名: {signature}")
|
||
current_app.logger.debug(f"服务端签名匹配: {signature == expected_signature}")
|
||
|
||
if signature != expected_signature:
|
||
current_app.logger.warning(f"验证请求:签名验证失败 - software_id: {software_id}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '签名验证失败'
|
||
}), 401
|
||
|
||
# 查找产品
|
||
product = Product.query.filter_by(product_id=software_id).first()
|
||
if not product:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品不存在'
|
||
}), 404
|
||
|
||
if not product.is_enabled():
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品已禁用'
|
||
}), 403
|
||
|
||
# 查找卡密
|
||
license_obj = License.query.filter_by(
|
||
license_key=license_key,
|
||
product_id=software_id
|
||
).first()
|
||
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
# 验证卡密
|
||
software_version = request.json.get('software_version', '1.0.0') if request.json else '1.0.0'
|
||
success, message = license_obj.verify(machine_code, software_version)
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 403
|
||
|
||
# 处理首次激活
|
||
if license_obj.status == 0: # 未激活
|
||
software_version = request.json.get('software_version', '1.0.0') if request.json else '1.0.0'
|
||
success, message = license_obj.activate(
|
||
machine_code,
|
||
software_version
|
||
)
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 403
|
||
|
||
# 更新设备验证时间
|
||
device = Device.query.filter_by(machine_code=machine_code).first()
|
||
if device:
|
||
device.update_verify_time()
|
||
|
||
# 获取最新版本信息
|
||
latest_version = Version.query.filter_by(
|
||
product_id=software_id,
|
||
publish_status=1
|
||
).order_by(Version.update_time.desc(), Version.create_time.desc()).first()
|
||
|
||
response_data = {
|
||
'license_key': license_obj.license_key,
|
||
'type': license_obj.type,
|
||
'type_name': '试用' if license_obj.is_trial() else '正式',
|
||
'expire_time': license_obj.expire_time.isoformat() if license_obj.expire_time else None,
|
||
'remaining_days': license_obj.get_remaining_days(),
|
||
'product_name': product.product_name,
|
||
'activate_time': license_obj.activate_time.isoformat() if license_obj.activate_time else None
|
||
}
|
||
|
||
# 添加版本信息
|
||
if latest_version:
|
||
current_version = request.json.get('software_version', '1.0.0') if request.json else '1.0.0'
|
||
response_data.update({
|
||
'new_version': latest_version.version_num,
|
||
'download_url': latest_version.download_url,
|
||
'force_update': latest_version.is_force_update(),
|
||
'update_log': latest_version.update_log,
|
||
'need_update': latest_version.version_num != current_version
|
||
})
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '验证成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
# 记录详细的错误信息,包括堆栈跟踪
|
||
import traceback
|
||
error_trace = traceback.format_exc()
|
||
current_app.logger.error(f"卡密验证失败: {str(e)}\n{error_trace}")
|
||
|
||
# 尝试回滚数据库事务
|
||
try:
|
||
db.session.rollback()
|
||
except:
|
||
pass
|
||
|
||
# 检查是否是数据库连接错误
|
||
error_str = str(e).lower()
|
||
error_type = type(e).__name__
|
||
|
||
if 'operationalerror' in error_str or 'connection' in error_str or 'database' in error_str or 'OperationalError' in error_type:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '数据库连接失败,请稍后重试'
|
||
}), 503
|
||
elif 'timeout' in error_str or 'Timeout' in error_type:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求处理超时,请稍后重试'
|
||
}), 503
|
||
else:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/auth/activate', methods=['POST'])
|
||
def activate_license():
|
||
"""激活卡密接口"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空'
|
||
}), 400
|
||
|
||
software_id = data.get('software_id')
|
||
license_key = data.get('license_key')
|
||
machine_code = data.get('machine_code')
|
||
software_version = data.get('software_version', '1.0.0')
|
||
|
||
# 验证必填参数
|
||
if not all([software_id, license_key, machine_code]):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '缺少必要参数'
|
||
}), 400
|
||
|
||
# 查找产品
|
||
product = Product.query.filter_by(product_id=software_id).first()
|
||
if not product:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品不存在'
|
||
}), 404
|
||
|
||
# 查找卡密
|
||
license_obj = License.query.filter_by(
|
||
license_key=license_key,
|
||
product_id=software_id
|
||
).first()
|
||
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
# 激活卡密
|
||
success, message = license_obj.activate(machine_code, software_version)
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 400
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '激活成功',
|
||
'data': license_obj.to_dict()
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"卡密激活失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/auth/info', methods=['GET'])
|
||
def get_auth_info():
|
||
"""获取授权信息接口"""
|
||
try:
|
||
software_id = request.args.get('software_id')
|
||
machine_code = request.args.get('machine_code')
|
||
|
||
if not software_id or not machine_code:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '缺少必要参数'
|
||
}), 400
|
||
|
||
# 查找设备
|
||
device = Device.query.filter_by(
|
||
machine_code=machine_code,
|
||
status=1
|
||
).first()
|
||
|
||
if not device:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '设备未激活'
|
||
}), 404
|
||
|
||
# 查找关联的卡密
|
||
license_obj = device.license
|
||
if not license_obj or license_obj.product_id != software_id:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '授权信息不匹配'
|
||
}), 403
|
||
|
||
# 检查授权状态
|
||
if license_obj.is_expired():
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '授权已过期'
|
||
}), 403
|
||
|
||
# 返回授权信息
|
||
return jsonify({
|
||
'success': True,
|
||
'data': {
|
||
'license_key': license_obj.license_key,
|
||
'type': license_obj.type,
|
||
'type_name': '试用' if license_obj.is_trial() else '正式',
|
||
'expire_time': license_obj.expire_time.isoformat() if license_obj.expire_time else None,
|
||
'remaining_days': license_obj.get_remaining_days(),
|
||
'activate_time': license_obj.activate_time.isoformat() if license_obj.activate_time else None,
|
||
'last_verify_time': license_obj.last_verify_time.isoformat() if license_obj.last_verify_time else None
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"获取授权信息失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|
||
|
||
@api_bp.route('/auth/heartbeat', methods=['POST'])
|
||
def heartbeat():
|
||
"""心跳接口(用于在线验证)"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({'success': False}), 400
|
||
|
||
software_id = data.get('software_id')
|
||
machine_code = data.get('machine_code')
|
||
|
||
if not software_id or not machine_code:
|
||
return jsonify({'success': False}), 400
|
||
|
||
# 更新设备最后验证时间
|
||
device = Device.query.filter_by(
|
||
machine_code=machine_code,
|
||
status=1
|
||
).first()
|
||
|
||
if device:
|
||
device.update_verify_time()
|
||
|
||
return jsonify({'success': True})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"心跳处理失败: {str(e)}")
|
||
return jsonify({'success': False}), 500
|
||
|
||
|
||
@api_bp.route('/auth/unbind', methods=['POST'])
|
||
def unbind_license():
|
||
"""用户端解绑卡密接口"""
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请求数据为空'
|
||
}), 400
|
||
|
||
software_id = data.get('software_id')
|
||
license_key = data.get('license_key')
|
||
machine_code = data.get('machine_code')
|
||
timestamp = data.get('timestamp')
|
||
signature = data.get('signature')
|
||
|
||
# 验证必填参数
|
||
if not all([software_id, license_key, machine_code, timestamp, signature]):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '缺少必要参数'
|
||
}), 400
|
||
|
||
# 验证签名
|
||
secret_key = current_app.config.get('AUTH_SECRET_KEY', 'default-secret-key')
|
||
signature_data = f"{software_id}{license_key}{machine_code}{timestamp}"
|
||
expected_signature = generate_signature(signature_data, secret_key)
|
||
|
||
if signature != expected_signature:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '签名验证失败'
|
||
}), 401
|
||
|
||
# 查找产品
|
||
product = Product.query.filter_by(product_id=software_id).first()
|
||
if not product:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '产品不存在'
|
||
}), 404
|
||
|
||
# 查找卡密
|
||
license_obj = License.query.filter_by(
|
||
license_key=license_key,
|
||
product_id=software_id
|
||
).first()
|
||
|
||
if not license_obj:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密不存在'
|
||
}), 404
|
||
|
||
# 检查卡密是否绑定到当前机器码
|
||
if license_obj.bind_machine_code != machine_code:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '卡密未绑定到当前设备'
|
||
}), 400
|
||
|
||
# 执行解绑操作
|
||
success, message = license_obj.unbind()
|
||
if not success:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': message
|
||
}), 400
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '解绑成功'
|
||
})
|
||
|
||
except Exception as e:
|
||
current_app.logger.error(f"卡密解绑失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '服务器内部错误'
|
||
}), 500
|