Flask API接口的安全风险
当Flask开发的API接口直接暴露给外部使用时,可能会遇到未授权调用、参数被中途修改、重复请求攻击等问题,这些问题会导致服务资源被滥用或者业务数据出现异常。通过API Key校验调用权限,结合签名验证确认参数未被篡改,是常用的轻量级防护方案。
API Key与签名验证的核心逻辑
API Key的作用
API Key是分配给合法调用方的身份标识,每个调用方持有唯一的Key,服务端通过校验请求中携带的Key判断调用方是否有权限访问接口。API Key通常不会直接用于防篡改,只做身份识别。
签名验证的作用
签名是对请求参数按照固定规则拼接、加密后生成的字符串,服务端用同样的规则对参数重新计算签名,与请求携带的签名对比,一致则说明参数未被修改。同时可以加入时间戳、随机字符串等参数,防止重放攻击。
完整实现步骤
1. 分配API Key与密钥
提前给每个调用方分配唯一的api_key和对应的secret_key,secret_key仅调用方和服务端知晓,用于生成签名,不随请求传输。
2. 定义签名生成规则
签名生成需要固定步骤,避免规则混乱导致校验失败:
- 收集所有请求参数,排除签名本身参数
- 按照参数名的字典序排序
- 拼接成
参数名=参数值的字符串,用&连接 - 在拼接字符串末尾追加
secret_key - 对拼接后的字符串做MD5加密,生成签名
3. 服务端校验逻辑实现
首先编写工具函数处理签名生成和校验:
import hashlib
import time
import json
# 模拟存储api_key和对应的secret_key,实际项目可存数据库或配置中心
API_KEY_MAP = {
"test_api_key_123": "test_secret_key_456"
}
def generate_sign(params, secret_key):
"""生成签名"""
# 排除sign参数本身
params_copy = {k: v for k, v in params.items() if k != "sign"}
# 按参数名字典序排序
sorted_params = sorted(params_copy.items(), key=lambda x: x[0])
# 拼接参数字符串
param_str = "&".join([f"{k}={v}" for k, v in sorted_params])
# 追加secret_key
sign_str = param_str + secret_key
# MD5加密,生成小写签名
sign = hashlib.md5(sign_str.encode("utf-8")).hexdigest()
return sign
def verify_request(params):
"""校验请求合法性,返回校验结果和错误信息"""
# 1. 校验api_key是否存在
api_key = params.get("api_key")
if not api_key or api_key not in API_KEY_MAP:
return False, "无效的api_key"
# 2. 校验签名是否存在
request_sign = params.get("sign")
if not request_sign:
return False, "缺少签名参数"
# 3. 校验时间戳,防止重放攻击,允许10秒误差
timestamp = params.get("timestamp")
if not timestamp:
return False, "缺少时间戳参数"
try:
timestamp = int(timestamp)
except ValueError:
return False, "时间戳格式错误"
if abs(int(time.time()) - timestamp) > 10:
return False, "请求已过期"
# 4. 重新计算签名并对比
secret_key = API_KEY_MAP.get(api_key)
server_sign = generate_sign(params, secret_key)
if server_sign != request_sign:
return False, "签名校验失败"
return True, "校验通过"
4. Flask接口编写
编写一个需要保护的示例接口,在接口中先执行校验逻辑,通过后再处理业务:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/get_user_info", methods=["GET"])
def get_user_info():
# 获取所有请求参数
params = request.args.to_dict()
# 校验请求合法性
is_valid, msg = verify_request(params)
if not is_valid:
return jsonify({"code": 401, "msg": msg}), 401
# 校验通过,处理业务逻辑,这里模拟返回用户信息
user_id = params.get("user_id")
return jsonify({
"code": 200,
"msg": "请求成功",
"data": {
"user_id": user_id,
"user_name": "测试用户",
"age": 25
}
})
if __name__ == "__main__":
app.run(debug=True, host="127.0.0.1", port=5000)
5. 调用方请求示例
调用方需要按照同样的规则生成签名,再发起请求,以下是调用方的示例代码:
import hashlib
import time
import requests
API_KEY = "test_api_key_123"
SECRET_KEY = "test_secret_key_456"
API_URL = "http://127.0.0.1:5000/api/get_user_info"
def call_api(user_id):
# 构造请求参数
params = {
"api_key": API_KEY,
"user_id": user_id,
"timestamp": str(int(time.time()))
}
# 生成签名
params["sign"] = generate_sign(params, SECRET_KEY)
# 发起请求
response = requests.get(API_URL, params=params)
print(response.json())
if __name__ == "__main__":
call_api("1001")
注意事项
- secret_key需要妥善保管,不能泄露给第三方,定期可以更换密钥提升安全性
- 时间戳的误差范围可以根据业务场景调整,对安全性要求高的场景可以缩短误差时间
- 如果接口支持POST请求,需要将请求体参数也纳入签名计算范围,规则与GET参数一致
- 实际生产环境中,API Key的分配、权限管理可以结合数据库或专业的API网关实现,更方便管理
签名验证可以有效防止参数篡改,但不能完全替代HTTPS,建议生产环境同时开启HTTPS,避免请求内容在传输过程中被明文窃取。