Python分页安全案例:保障分页查询的完整方案
基础安全分页实现
from flask import Flask, request, jsonify
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from typing import Optional, Tuple
import re
import hashlib
import time
app = Flask(__name__)
# 数据库配置
DATABASE_URL = "mysql+pymysql://user:password@localhost/dbname"
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
class SecurePaginator:
"""安全分页器"""
def __init__(self, session):
self.session = session
def validate_page_params(self, page: int, per_page: int) -> Tuple[int, int]:
"""验证分页参数"""
# 设置安全范围
MAX_PER_PAGE = 100
MIN_PER_PAGE = 1
MAX_PAGE = 10000
# 验证并限制每页数量
try:
per_page = int(per_page)
except (ValueError, TypeError):
per_page = 20 # 默认值
per_page = max(MIN_PER_PAGE, min(per_page, MAX_PER_PAGE))
# 验证并限制页码
try:
page = int(page)
except (ValueError, TypeError):
page = 1
page = max(1, min(page, MAX_PAGE))
return page, per_page
def paginate_safe(self, query, page: int, per_page: int,
allowed_sort_fields: list = None,
sort_by: str = "",
sort_order: str = "asc"):
"""
安全分页查询
"""
# 1. 验证分页参数
page, per_page = self.validate_page_params(page, per_page)
# 2. 计算偏移量
offset = (page - 1) * per_page
# 3. 安全处理排序
if allowed_sort_fields and sort_by:
# 白名单验证
if sort_by not in allowed_sort_fields:
sort_by = allowed_sort_fields[0] # 默认排序字段
# 验证排序方向
if sort_order.lower() not in ['asc', 'desc']:
sort_order = 'asc'
# 安全拼接排序
sort_column = f"`{sort_by}`" # 使用反引号防止注入
order_clause = f"{sort_column} {sort_order}"
query = query.order_by(text(order_clause))
# 4. 执行分页查询
total = query.count()
items = query.limit(per_page).offset(offset).all()
# 5. 计算总页数
total_pages = (total + per_page - 1) // per_page
return {
'items': items,
'page': page,
'per_page': per_page,
'total': total,
'total_pages': total_pages,
'has_next': page < total_pages,
'has_previous': page > 1
}
# 路由示例
@app.route('/api/users', methods=['GET'])
def get_users():
"""安全的用户分页查询"""
session = Session()
try:
paginator = SecurePaginator(session)
# 获取并验证参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
sort_by = request.args.get('sort_by', 'id')
sort_order = request.args.get('sort_order', 'asc')
# 允许排序的字段白名单
allowed_sort_fields = ['id', 'username', 'email', 'created_at']
# 构建基础查询
from models import User
query = session.query(User)
# 执行安全分页
result = paginator.paginate_safe(
query,
page,
per_page,
allowed_sort_fields,
sort_by,
sort_order
)
# 序列化结果
serialized_items = []
for user in result['items']:
serialized_items.append({
'id': user.id,
'username': user.username,
'email': user.email,
'created_at': user.created_at.isoformat()
})
result['items'] = serialized_items
return jsonify(result)
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
session.close()
# 使用ORM的完整示例(SQLAlchemy)
@app.route('/api/orders', methods=['GET'])
def get_orders():
"""使用ORM的安全分页"""
session = Session()
try:
from models import Order
# 安全获取参数
page = max(1, int(request.args.get('page', 1)))
per_page = min(100, max(1, int(request.args.get('per_page', 20))))
# 使用filter进行安全过滤
status = request.args.get('status')
user_id = request.args.get('user_id')
query = session.query(Order)
# 安全添加过滤条件
if status and status in ['pending', 'completed', 'cancelled']:
query = query.filter(Order.status == status)
if user_id and user_id.isdigit():
query = query.filter(Order.user_id == int(user_id))
# 使用原生SQLAlchemy分页
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
return jsonify({
'items': [order.to_dict() for order in pagination.items],
'page': pagination.page,
'per_page': pagination.per_page,
'total': pagination.total,
'pages': pagination.pages,
'has_prev': pagination.has_prev,
'has_next': pagination.has_next
})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
session.close()
防SQL注入的游标分页
class CursorBasedPaginator:
"""基于游标的分页,防止偏移量攻击"""
def __init__(self, session):
self.session = session
def paginate_by_cursor(self, model, cursor: Optional[str] = None,
limit: int = 20, direction: str = 'next'):
"""使用游标进行安全分页"""
# 验证参数
limit = min(100, max(1, limit))
# 构建查询
query = self.session.query(model)
if direction == 'next' and cursor:
# 使用WHERE条件代替OFFSET
query = query.filter(model.id < int(cursor))
query = query.order_by(model.id.desc())
elif direction == 'prev' and cursor:
query = query.filter(model.id > int(cursor))
query = query.order_by(model.id.asc())
else:
query = query.order_by(model.id.desc())
items = query.limit(limit).all()
# 生成新的游标
next_cursor = str(items[-1].id) if items else None
prev_cursor = str(items[0].id) if items else None
return {
'items': [item.to_dict() for item in items],
'next_cursor': next_cursor,
'prev_cursor': prev_cursor,
'limit': limit,
'has_next': len(items) == limit
}
防止参数篡改的签名验证
import hmac
import base64
from urllib.parse import quote, unquote
class SignedPaginator:
"""带签名的分页器,防止参数篡改"""
def __init__(self, secret_key: str = "your-secret-key"):
self.secret_key = secret_key.encode()
def generate_page_signature(self, page: int, per_page: int,
timestamp: int = None) -> str:
"""生成分页参数的签名"""
if not timestamp:
timestamp = int(time.time())
# 创建签名字符串
message = f"{page}:{per_page}:{timestamp}"
signature = hmac.new(
self.secret_key,
message.encode(),
hashlib.sha256
).digest()
# Base64编码
return base64.urlsafe_b64encode(signature).decode()
def validate_page_request(self, page: int, per_page: int,
signature: str, timestamp: int) -> bool:
"""验证分页请求的签名"""
# 检查时间戳是否过期(5分钟内有效)
current_time = int(time.time())
if abs(current_time - timestamp) > 300: # 5分钟
return False
# 重新计算签名并比较
expected_signature = self.generate_page_signature(page, per_page, timestamp)
return hmac.compare_digest(expected_signature, signature)
# 使用示例
@app.route('/api/secure/users', methods=['GET'])
def get_secure_users():
"""使用签名的安全分页API"""
try:
# 获取签名参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
signature = request.args.get('sig', '')
timestamp = request.args.get('ts', 0, type=int)
# 验证签名
signed_paginator = SignedPaginator()
if not signed_paginator.validate_page_request(page, per_page, signature, timestamp):
return jsonify({'error': 'Invalid or expired request'}), 403
# 继续处理请求...
# 这里添加实际的业务逻辑
return jsonify({'message': 'Request validated'})
except Exception as e:
return jsonify({'error': str(e)}), 400
速率限制和防爬虫
from functools import wraps
from collections import defaultdict
import threading
class RateLimiter:
"""速率限制器"""
def __init__(self):
self.locks = defaultdict(threading.Lock)
self.counts = defaultdict(int)
self.reset_times = defaultdict(float)
def is_allowed(self, key: str, max_requests: int = 100,
window_seconds: int = 60) -> bool:
"""检查是否允许请求"""
current_time = time.time()
with self.locks[key]:
# 重置计数器(如果窗口已过)
if current_time - self.reset_times[key] > window_seconds:
self.counts[key] = 0
self.reset_times[key] = current_time
self.counts[key] += 1
return self.counts[key] <= max_requests
# 装饰器实现
def rate_limit(max_requests=100, window_seconds=60):
"""速率限制装饰器"""
limiter = RateLimiter()
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# 使用IP地址作为key
key = request.remote_addr
if not limiter.is_allowed(key, max_requests, window_seconds):
return jsonify({
'error': 'Too many requests',
'retry_after': window_seconds
}), 429
return f(*args, **kwargs)
return decorated_function
return decorator
@app.route('/api/rate-limited/users')
@rate_limit(max_requests=30, window_seconds=60)
def get_rate_limited_users():
"""带速率限制的分页API"""
# 实现逻辑
pass
综合安全分页示例
# models.py
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
username = Column(String(50), nullable=False)
email = Column(String(100), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
def to_dict(self):
return {
'id': self.id,
'username': self.username,
'email': self.email,
'created_at': self.created_at.isoformat()
}
# 完整的安全分页API
@app.route('/api/safe/users', methods=['GET'])
@rate_limit(max_requests=60, window_seconds=60)
def get_safe_users():
"""完整的安全分页API"""
session = Session()
try:
# 1. 参数验证
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
# 2. 参数范围限制
page = max(1, min(page, 10000))
per_page = max(1, min(per_page, 100))
# 3. 构建安全查询
query = session.query(User)
# 4. 添加安全过滤条件
search = request.args.get('search', '').strip()
if search and len(search) <= 50:
# 使用参数化查询防止SQL注入
query = query.filter(User.username.like(f'%{search}%'))
# 5. 安全排序
sort_by = request.args.get('sort_by', 'id')
sort_order = request.args.get('sort_order', 'asc')
allowed_sort = {
'id': User.id,
'username': User.username,
'created_at': User.created_at
}
if sort_by in allowed_sort:
column = allowed_sort[sort_by]
query = query.order_by(column.desc() if sort_order == 'desc' else column)
# 6. 执行分页
total = query.count()
offset = (page - 1) * per_page
users = query.limit(per_page).offset(offset).all()
# 7. 构建响应
return jsonify({
'data': [user.to_dict() for user in users],
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'total_pages': (total + per_page - 1) // per_page,
'has_next': page < (total + per_page - 1) // per_page,
'has_prev': page > 1
},
'meta': {
'timestamp': datetime.utcnow().isoformat(),
'request_id': str(uuid.uuid4())[:8]
}
})
except Exception as e:
return jsonify({'error': 'Internal server error'}), 500
finally:
session.close()
安全最佳实践总结
-
输入验证:

- 限制分页参数范围(如:per_page ≤ 100)
- 使用类型转换确保参数合法
- 对排序字段使用白名单
-
SQL注入防护:
- 使用参数化查询或ORM
- 避免字符串拼接SQL
- 对用户输入进行转义
-
性能防护:
- 限制最大偏移量
- 使用索引优化查询
- 考虑游标分页替代偏移量
-
访问控制:
- 实现速率限制
- 添加请求签名
- 验证用户权限
-
错误处理:
- 不暴露敏感信息
- 返回统一的错误格式
- 记录详细的错误日志
这些措施可以确保你的分页查询既安全又高效。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播