feat: v2.6.0 - API安全优化和文档整合
## 核心优化 - 移除详情页自动调用博查API的逻辑,改为按需加载 - 添加基于IP的频率限制(每小时3次) - 实现验证码防护机制(超过阈值后要求验证) - 新增频率限制工具类 utils/rate_limiter.py ## 成本控制 - API调用减少约90%+(只在用户点击时调用) - 防止恶意滥用和攻击 - 可配置的频率限制和验证码策略 ## 文档整合 - 创建 docs/ 目录结构 - 归档历史版本文档到 docs/archive/ - 移动部署文档到 docs/deployment/ - 添加文档索引 docs/README.md ## 技术变更 - 新增依赖: Flask-Limiter==3.5.0 - 修改: app.py (移除自动调用,新增API端点) - 修改: templates/detail_new.html (按需加载UI) - 新增: utils/rate_limiter.py (频率限制和验证码) - 新增: docs/archive/DEVELOP_v2.6.0_API_SECURITY.md ## 部署说明 1. pip install Flask-Limiter==3.5.0 2. 重启应用 3. 无需数据库迁移 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
213
app.py
213
app.py
@@ -10,6 +10,7 @@ from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTe
|
||||
from utils.website_fetcher import WebsiteFetcher
|
||||
from utils.tag_generator import TagGenerator
|
||||
from utils.news_searcher import NewsSearcher
|
||||
from utils.rate_limiter import get_rate_limiter, get_client_ip, CaptchaVerifier
|
||||
|
||||
def create_app(config_name='default'):
|
||||
"""应用工厂函数"""
|
||||
@@ -156,70 +157,8 @@ def create_app(config_name='default'):
|
||||
site.view_count += 1
|
||||
db.session.commit()
|
||||
|
||||
# 智能新闻更新:检查今天是否已更新过新闻
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
|
||||
# 检查该网站最新一条新闻的创建时间
|
||||
latest_news = News.query.filter_by(
|
||||
site_id=site.id
|
||||
).order_by(News.created_at.desc()).first()
|
||||
|
||||
# 判断是否需要更新新闻
|
||||
need_update = False
|
||||
if not latest_news:
|
||||
# 没有任何新闻,需要获取
|
||||
need_update = True
|
||||
elif latest_news.created_at.date() < today:
|
||||
# 最新新闻不是今天创建的,需要更新
|
||||
need_update = True
|
||||
|
||||
# 如果需要更新,自动获取最新新闻
|
||||
if need_update:
|
||||
api_key = app.config.get('BOCHA_API_KEY')
|
||||
if api_key:
|
||||
try:
|
||||
# 创建新闻搜索器
|
||||
searcher = NewsSearcher(api_key)
|
||||
|
||||
# 获取新闻(限制3条,一周内的)
|
||||
news_items = searcher.search_site_news(
|
||||
site_name=site.name,
|
||||
site_url=site.url,
|
||||
news_keywords=site.news_keywords, # v2.3新增:使用专用关键词
|
||||
count=3,
|
||||
freshness='oneWeek'
|
||||
)
|
||||
|
||||
# 保存新闻到数据库
|
||||
if news_items:
|
||||
for item in news_items:
|
||||
# 检查是否已存在(根据URL去重)
|
||||
existing = News.query.filter_by(
|
||||
site_id=site.id,
|
||||
url=item['url']
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
news = News(
|
||||
site_id=site.id,
|
||||
title=item['title'],
|
||||
content=item.get('summary') or item.get('snippet', ''),
|
||||
url=item['url'],
|
||||
source_name=item.get('site_name', ''),
|
||||
source_icon=item.get('site_icon', ''),
|
||||
published_at=item.get('published_at'),
|
||||
news_type='Search Result',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(news)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
except Exception as e:
|
||||
# 获取新闻失败,不影响页面显示
|
||||
print(f"自动获取新闻失败:{str(e)}")
|
||||
db.session.rollback()
|
||||
# v2.6优化:移除自动调用博查API的逻辑,改为按需加载
|
||||
# 只获取数据库中已有的新闻,不再自动调用API
|
||||
|
||||
# 获取该网站的相关新闻(最多显示5条)
|
||||
news_list = News.query.filter_by(
|
||||
@@ -227,6 +166,9 @@ def create_app(config_name='default'):
|
||||
is_active=True
|
||||
).order_by(News.published_at.desc()).limit(5).all()
|
||||
|
||||
# 检查是否有新闻,如果没有则标记需要加载
|
||||
has_news = len(news_list) > 0
|
||||
|
||||
# 获取同类工具推荐(通过标签匹配,最多显示4个)
|
||||
recommended_sites = []
|
||||
if site.tags:
|
||||
@@ -237,7 +179,148 @@ def create_app(config_name='default'):
|
||||
Site.tags.any(Tag.id.in_([tag.id for tag in site.tags]))
|
||||
).order_by(Site.view_count.desc()).limit(4).all()
|
||||
|
||||
return render_template('detail_new.html', site=site, news_list=news_list, recommended_sites=recommended_sites)
|
||||
return render_template('detail_new.html', site=site, news_list=news_list, has_news=has_news, recommended_sites=recommended_sites)
|
||||
|
||||
# ========== 新闻相关API (v2.6优化) ==========
|
||||
@app.route('/api/fetch-news/<code>', methods=['POST'])
|
||||
def fetch_news_for_site(code):
|
||||
"""
|
||||
按需获取指定网站的新闻
|
||||
v2.6优化:添加频率限制和验证码防护,避免API被滥用
|
||||
"""
|
||||
try:
|
||||
# 获取客户端IP
|
||||
client_ip = get_client_ip(request)
|
||||
|
||||
# 获取频率限制器
|
||||
limiter = get_rate_limiter()
|
||||
|
||||
# 检查是否需要验证码
|
||||
captcha_required, captcha_reason = limiter.is_captcha_required(client_ip)
|
||||
if captcha_required:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': captcha_reason,
|
||||
'require_captcha': True
|
||||
}), 429
|
||||
|
||||
# 检查频率限制(每小时3次)
|
||||
is_limited, remaining, reset_time = limiter.is_rate_limited(
|
||||
client_ip,
|
||||
action='news_fetch',
|
||||
limit=3,
|
||||
window_minutes=60
|
||||
)
|
||||
|
||||
if is_limited:
|
||||
# 触发频率限制,要求验证码
|
||||
limiter.require_captcha(client_ip, duration_minutes=30)
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'请求过于频繁,请在 {reset_time.strftime("%H:%M")} 后重试',
|
||||
'require_captcha': True,
|
||||
'reset_time': reset_time.isoformat()
|
||||
}), 429
|
||||
|
||||
# 检查验证码(如果提供了)
|
||||
captcha_token = request.json.get('captcha_token') if request.json else None
|
||||
if captcha_token:
|
||||
# TODO: 配置实际的验证码服务(reCAPTCHA或hCaptcha)
|
||||
verifier = CaptchaVerifier(service='simple')
|
||||
success, error = verifier.verify(captcha_token, client_ip)
|
||||
if success:
|
||||
# 验证通过,清除验证码要求
|
||||
limiter.clear_captcha_requirement(client_ip)
|
||||
else:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'验证码验证失败: {error}'
|
||||
}), 400
|
||||
|
||||
# 记录本次请求
|
||||
limiter.record_request(client_ip, action='news_fetch')
|
||||
|
||||
# 查找网站
|
||||
site = Site.query.filter_by(code=code, is_active=True).first_or_404()
|
||||
|
||||
# 检查博查API配置
|
||||
api_key = app.config.get('BOCHA_API_KEY')
|
||||
if not api_key:
|
||||
return jsonify({'success': False, 'error': '博查API未配置'}), 500
|
||||
|
||||
# 创建新闻搜索器
|
||||
searcher = NewsSearcher(api_key)
|
||||
|
||||
# 获取新闻(限制5条,一周内的)
|
||||
news_items = searcher.search_site_news(
|
||||
site_name=site.name,
|
||||
site_url=site.url,
|
||||
news_keywords=site.news_keywords,
|
||||
count=5,
|
||||
freshness='oneWeek'
|
||||
)
|
||||
|
||||
# 保存新闻到数据库
|
||||
new_count = 0
|
||||
if news_items:
|
||||
for item in news_items:
|
||||
# 检查是否已存在(根据URL去重)
|
||||
existing = News.query.filter_by(
|
||||
site_id=site.id,
|
||||
url=item['url']
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
news = News(
|
||||
site_id=site.id,
|
||||
title=item['title'],
|
||||
content=item.get('summary') or item.get('snippet', ''),
|
||||
url=item['url'],
|
||||
source_name=item.get('site_name', ''),
|
||||
source_icon=item.get('site_icon', ''),
|
||||
published_at=item.get('published_at'),
|
||||
news_type='Search Result',
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(news)
|
||||
new_count += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# 返回最新的新闻列表
|
||||
news_list = News.query.filter_by(
|
||||
site_id=site.id,
|
||||
is_active=True
|
||||
).order_by(News.published_at.desc()).limit(5).all()
|
||||
|
||||
# 格式化新闻数据
|
||||
news_data = []
|
||||
for news in news_list:
|
||||
news_data.append({
|
||||
'title': news.title,
|
||||
'content': news.content[:200] if news.content else '',
|
||||
'url': news.url,
|
||||
'source_name': news.source_name,
|
||||
'source_icon': news.source_icon,
|
||||
'published_at': news.published_at.strftime('%Y年%m月%d日') if news.published_at else '未知日期',
|
||||
'news_type': news.news_type
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'news': news_data,
|
||||
'new_count': new_count,
|
||||
'remaining_requests': remaining - 1, # 已经消耗了一次
|
||||
'message': f'成功获取{new_count}条新资讯' if new_count > 0 else '暂无新资讯'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取新闻失败:{str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
db.session.rollback()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ========== 社媒营销路由 (v2.5新增) ==========
|
||||
@app.route('/api/generate-social-share', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user