Files
rag/api/auth_routes.py
lacerate551 3c262b8d35 fix(auth): 登录速率限制与用户会话隔离增强
- auth_routes: 新增内存级登录速率限制(5min/10次),防止暴力破解
- auth_routes: 统一 DEV_MODE 判断收归 config.py 集中管理
- auth_routes: update_user 增加用户存在性校验
- gateway: require_role 装饰器恢复实际角色校验(原为占位实现)
- session_routes: get_user_sessions 统一传入 limit=20 参数
- session: get_history SQL 增加 id DESC 排序,修复同秒消息顺序错乱

🤖 Generated with [Qoder][https://qoder.com]
2026-06-08 15:44:37 +08:00

261 lines
7.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
认证与系统状态 API
路由:
- POST /auth/login - 模拟登录(仅开发环境)
- GET /stats - 系统统计 (管理员)
- GET /health - 健康检查
- GET /auth/me - 当前用户信息
"""
from flask import Blueprint, request, jsonify
from auth.gateway import require_gateway_auth, require_role, get_user_permissions, MOCK_USERS
from core.status_codes import SUCCESS, BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, PERMISSION_DENIED, INTERNAL_ERROR
from api.response_utils import success_response, error_response
from config import DEV_MODE
import time
from pathlib import Path
from dotenv import load_dotenv
# 加载 .env 文件(从项目根目录)
env_path = Path(__file__).parent.parent / '.env'
load_dotenv(env_path)
auth_bp = Blueprint('auth', __name__)
# ==================== 登录速率限制 ====================
# 简单的内存级速率限制:每个 IP 在时间窗口内最多允许 N 次登录尝试
_login_attempts = {} # {ip: [(timestamp, ...), ...]}
_RATE_LIMIT_WINDOW = 300 # 5 分钟窗口
_RATE_LIMIT_MAX = 10 # 窗口内最多 10 次尝试
def _check_rate_limit(client_ip: str) -> bool:
"""检查是否超出登录速率限制,返回 True 表示允许"""
now = time.time()
if client_ip not in _login_attempts:
_login_attempts[client_ip] = []
# 清理过期记录
_login_attempts[client_ip] = [
t for t in _login_attempts[client_ip]
if now - t < _RATE_LIMIT_WINDOW
]
if len(_login_attempts[client_ip]) >= _RATE_LIMIT_MAX:
return False
_login_attempts[client_ip].append(now)
return True
def _is_dev_mode() -> bool:
"""统一的开发模式判断(由 config.py 集中管理)"""
return DEV_MODE
@auth_bp.route('/auth/login', methods=['POST'])
def mock_login():
"""
模拟登录(仅开发环境)
请求体:
{
"username": "admin",
"password": "admin123"
}
返回:
{
"token": "mock-token-admin",
"user": {
"user_id": "admin001",
"username": "admin",
"role": "admin",
"department": "管理部"
}
}
测试账号:
- admin / admin123 (管理员,管理部)
- admin2 / admin456 (管理员,技术部)
- admin3 / admin789 (管理员,运营部)
- manager / manager123 (经理,财务部)
- user / test123 (普通用户,技术部)
"""
if not _is_dev_mode():
return error_response("FORBIDDEN", FORBIDDEN, "仅开发环境可用,请在 .env 中设置 DEV_MODE=true", http_status=403)
# 速率限制检查
client_ip = request.remote_addr or 'unknown'
if not _check_rate_limit(client_ip):
return error_response(
"RATE_LIMITED", FORBIDDEN,
f"登录尝试过于频繁,请 {_RATE_LIMIT_WINDOW // 60} 分钟后再试",
http_status=429
)
data = request.json or {}
username = data.get('username')
password = data.get('password')
user = MOCK_USERS.get(username)
if not user or user['password'] != password:
return error_response("UNAUTHORIZED", UNAUTHORIZED, "用户名或密码错误", http_status=401)
return success_response(data={
"token": f"mock-token-{username}",
"user": {
"user_id": user['user_id'],
"username": username,
"role": user['role'],
"department": user['department']
}
})
@auth_bp.route('/stats', methods=['GET'])
@require_gateway_auth
@require_role('admin')
def get_stats():
"""获取系统统计信息(仅管理员)"""
from flask import current_app
session_manager = current_app.config.get('SESSION_MANAGER')
if not session_manager:
return error_response("UNAVAILABLE", INTERNAL_ERROR, "会话管理器未启用", http_status=503)
return success_response(data=session_manager.get_stats())
@auth_bp.route('/health', methods=['GET'])
def health():
"""健康检查"""
return jsonify({
"status": "ok",
"knowledge_base": "多向量库模式 (按集合提供服务)",
"bm25_index": "动态按需加载",
"mode": "Agentic RAG"
})
@auth_bp.route('/auth/me', methods=['GET'])
@require_gateway_auth
def get_current_user():
"""
获取当前用户信息
开发模式下支持模拟用户,生产模式下用户信息由后端控制。
"""
user = request.current_user
return success_response(data={
"user_id": user["user_id"],
"username": user["username"],
"role": user["role"],
"department": user["department"],
"permissions": get_user_permissions(user["role"])
})
@auth_bp.route('/auth/users', methods=['GET'])
@require_gateway_auth
def get_users():
"""
获取用户列表(仅开发环境)
返回:
{
"users": [
{
"user_id": "admin001",
"username": "admin",
"role": "admin",
"department": "管理部",
"is_active": true
}
]
}
"""
if not _is_dev_mode():
return error_response("FORBIDDEN", FORBIDDEN, "仅开发环境可用", http_status=403)
users = []
for username, info in MOCK_USERS.items():
users.append({
"user_id": info["user_id"],
"username": username,
"role": info["role"],
"department": info["department"],
"is_active": True # 模拟用户默认都是活跃状态
})
return success_response(data={"users": users})
@auth_bp.route('/auth/users/<user_id>', methods=['PUT'])
@require_gateway_auth
def update_user(user_id):
"""
更新用户信息(仅开发环境,模拟操作)
请求体:
{
"is_active": false
}
"""
if not _is_dev_mode():
return error_response("FORBIDDEN", FORBIDDEN, "仅开发环境可用", http_status=403)
# 验证目标用户是否存在
target_user = None
for username, info in MOCK_USERS.items():
if info['user_id'] == user_id:
target_user = info
break
if not target_user:
return error_response("NOT_FOUND", BAD_REQUEST, f"用户 {user_id} 不存在", http_status=404)
data = request.json or {}
# 模拟操作记录请求但不实际执行mock 用户数据是静态的)
return success_response(data={
"message": "操作成功(模拟)",
"user_id": user_id,
"applied_changes": data
})
@auth_bp.route('/auth/change-password', methods=['POST'])
@require_gateway_auth
def change_password():
"""
修改密码(仅开发环境,模拟操作)
请求体:
{
"old_password": "xxx",
"new_password": "xxx"
}
"""
if not _is_dev_mode():
return error_response("FORBIDDEN", FORBIDDEN, "仅开发环境可用", http_status=403)
data = request.json or {}
old_password = data.get('old_password')
new_password = data.get('new_password')
if not old_password or not new_password:
return error_response("MISSING_PARAMS", BAD_REQUEST, "请提供旧密码和新密码", http_status=400)
if len(new_password) < 6:
return error_response("INVALID_PARAMS", BAD_REQUEST, "新密码至少6位", http_status=400)
# 验证当前用户的旧密码
user = request.current_user
username = user.get('username', '')
mock_user = MOCK_USERS.get(username)
if mock_user and mock_user['password'] != old_password:
return error_response("UNAUTHORIZED", UNAUTHORIZED, "旧密码错误", http_status=401)
# 模拟环境返回成功不实际修改密码mock 数据是静态的)
return success_response(message="密码修改成功(模拟)")