feat(auth): 实现账户登录失败次数限制和锁定机制(注意,需要迁移数据库)
- 数据库用户模型新增登录失败限制相关字段(login_failed_count, last_failed_login, login_locked_until) - 增加用户方法判断是否处于登录锁定状态及计算锁定持续时间 - 登录接口新增登录锁定检测,密码错误时增加失败计数及设置锁定时间 - 登录成功时重置失败计数和锁定状态 - 新增数据库迁移模块,自动检测并执行数据库结构更新,确保新增字段同步 - 前端登录页添加账户锁定状态显示及倒计时功能 - 前端请求中捕获423锁定状态,展示对应锁定提示,禁用登录按钮 - 优化本地状态管理,支持登录锁定错误抛出与异常处理机制
This commit is contained in:
parent
166990bc81
commit
747c5de88f
@ -7,6 +7,7 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from server.models import Base
|
||||
from server.models.user_model import User
|
||||
from server.utils.migrate import check_and_migrate
|
||||
from src import config
|
||||
from src.utils import logger
|
||||
|
||||
@ -24,8 +25,11 @@ class DBManager:
|
||||
# 创建会话工厂
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
# 确保表存在
|
||||
# 首先创建基本表结构
|
||||
self.create_tables()
|
||||
|
||||
# 然后检查并执行数据库迁移
|
||||
self.run_migrations()
|
||||
|
||||
def ensure_db_dir(self):
|
||||
"""确保数据库目录存在"""
|
||||
@ -37,6 +41,18 @@ class DBManager:
|
||||
# 确保所有表都会被创建
|
||||
Base.metadata.create_all(self.engine)
|
||||
logger.info("Database tables created/checked")
|
||||
|
||||
def run_migrations(self):
|
||||
"""运行数据库迁移"""
|
||||
try:
|
||||
success = check_and_migrate(self.db_path)
|
||||
if success:
|
||||
logger.info("数据库迁移检查完成")
|
||||
else:
|
||||
logger.warning("数据库迁移过程中出现问题")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库迁移失败: {e}")
|
||||
# 不抛出异常,以防止阻断应用启动
|
||||
|
||||
def get_session(self):
|
||||
"""获取数据库会话"""
|
||||
|
||||
@ -16,6 +16,11 @@ class User(Base):
|
||||
role = Column(String, nullable=False, default="user") # 角色: superadmin, admin, user
|
||||
created_at = Column(DateTime, default=func.now())
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# 登录失败限制相关字段
|
||||
login_failed_count = Column(Integer, nullable=False, default=0) # 登录失败次数
|
||||
last_failed_login = Column(DateTime, nullable=True) # 最后一次登录失败时间
|
||||
login_locked_until = Column(DateTime, nullable=True) # 锁定到什么时候
|
||||
|
||||
# 关联操作日志
|
||||
operation_logs = relationship("OperationLog", back_populates="user")
|
||||
@ -27,10 +32,56 @@ class User(Base):
|
||||
"role": self.role,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"last_login": self.last_login.isoformat() if self.last_login else None,
|
||||
"login_failed_count": self.login_failed_count,
|
||||
"last_failed_login": self.last_failed_login.isoformat() if self.last_failed_login else None,
|
||||
"login_locked_until": self.login_locked_until.isoformat() if self.login_locked_until else None,
|
||||
}
|
||||
if include_password:
|
||||
result["password_hash"] = self.password_hash
|
||||
return result
|
||||
|
||||
def is_login_locked(self):
|
||||
"""检查用户是否处于登录锁定状态"""
|
||||
if self.login_locked_until is None:
|
||||
return False
|
||||
from datetime import datetime
|
||||
return datetime.now() < self.login_locked_until
|
||||
|
||||
def get_remaining_lock_time(self):
|
||||
"""获取剩余锁定时间(秒)"""
|
||||
if not self.is_login_locked():
|
||||
return 0
|
||||
from datetime import datetime
|
||||
return int((self.login_locked_until - datetime.now()).total_seconds())
|
||||
|
||||
def calculate_lock_duration(self):
|
||||
"""根据失败次数计算锁定时长(秒)"""
|
||||
if self.login_failed_count < 10:
|
||||
return 0
|
||||
|
||||
# 从第10次失败开始,等待时间从1秒开始,每次翻倍
|
||||
wait_seconds = 2 ** (self.login_failed_count - 10)
|
||||
|
||||
# 最大锁定时间:365天
|
||||
max_seconds = 365 * 24 * 60 * 60
|
||||
return min(wait_seconds, max_seconds)
|
||||
|
||||
def increment_failed_login(self):
|
||||
"""增加登录失败次数并设置锁定时间"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
self.login_failed_count += 1
|
||||
self.last_failed_login = datetime.now()
|
||||
|
||||
lock_duration = self.calculate_lock_duration()
|
||||
if lock_duration > 0:
|
||||
self.login_locked_until = datetime.now() + timedelta(seconds=lock_duration)
|
||||
|
||||
def reset_failed_login(self):
|
||||
"""重置登录失败相关字段"""
|
||||
self.login_failed_count = 0
|
||||
self.last_failed_login = None
|
||||
self.login_locked_until = None
|
||||
|
||||
|
||||
class OperationLog(Base):
|
||||
|
||||
@ -65,15 +65,55 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
|
||||
# 查找用户
|
||||
user = db.query(User).filter(User.username == form_data.username).first()
|
||||
|
||||
# 验证用户存在且密码正确
|
||||
if not user or not AuthUtils.verify_password(user.password_hash, form_data.password):
|
||||
# 如果用户不存在,为防止用户名枚举攻击,返回通用错误信息
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 检查用户是否处于登录锁定状态
|
||||
if user.is_login_locked():
|
||||
remaining_time = user.get_remaining_lock_time()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_423_LOCKED,
|
||||
detail=f"登录被锁定,请等待 {remaining_time} 秒后再试",
|
||||
headers={
|
||||
"WWW-Authenticate": "Bearer",
|
||||
"X-Lock-Remaining": str(remaining_time)
|
||||
},
|
||||
)
|
||||
|
||||
# 更新最后登录时间
|
||||
# 验证密码
|
||||
if not AuthUtils.verify_password(user.password_hash, form_data.password):
|
||||
# 密码错误,增加失败次数
|
||||
user.increment_failed_login()
|
||||
db.commit()
|
||||
|
||||
# 记录失败操作
|
||||
log_operation(db, user.id if user else None, "登录失败", f"密码错误,失败次数: {user.login_failed_count}")
|
||||
|
||||
# 检查是否需要锁定
|
||||
if user.is_login_locked():
|
||||
remaining_time = user.get_remaining_lock_time()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_423_LOCKED,
|
||||
detail=f"由于多次登录失败,账户已被锁定 {remaining_time} 秒",
|
||||
headers={
|
||||
"WWW-Authenticate": "Bearer",
|
||||
"X-Lock-Remaining": str(remaining_time)
|
||||
},
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户名或密码错误",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 登录成功,重置失败计数器
|
||||
user.reset_failed_login()
|
||||
user.last_login = datetime.now()
|
||||
db.commit()
|
||||
|
||||
|
||||
268
server/utils/migrate.py
Normal file
268
server/utils/migrate.py
Normal file
@ -0,0 +1,268 @@
|
||||
"""
|
||||
数据库迁移系统
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from src.utils import logger
|
||||
|
||||
|
||||
class DatabaseMigrator:
|
||||
"""数据库迁移器"""
|
||||
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self.backup_dir = os.path.join(os.path.dirname(db_path), "backups")
|
||||
self.migration_version_key = "migration_version"
|
||||
|
||||
def ensure_backup_dir(self):
|
||||
"""确保备份目录存在"""
|
||||
Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def backup_database(self) -> str:
|
||||
"""备份数据库文件"""
|
||||
if not os.path.exists(self.db_path):
|
||||
logger.info("数据库文件不存在,无需备份")
|
||||
return ""
|
||||
|
||||
self.ensure_backup_dir()
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_filename = f"server_backup_{timestamp}.db"
|
||||
backup_path = os.path.join(self.backup_dir, backup_filename)
|
||||
|
||||
try:
|
||||
shutil.copy2(self.db_path, backup_path)
|
||||
logger.info(f"数据库已备份到: {backup_path}")
|
||||
return backup_path
|
||||
except Exception as e:
|
||||
logger.error(f"数据库备份失败: {e}")
|
||||
raise
|
||||
|
||||
def get_current_version(self) -> int:
|
||||
"""获取当前数据库版本"""
|
||||
if not os.path.exists(self.db_path):
|
||||
return 0
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查版本表是否存在
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='migration_versions'
|
||||
""")
|
||||
|
||||
if not cursor.fetchone():
|
||||
# 版本表不存在,检查是否为旧版本数据库
|
||||
cursor.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='users'
|
||||
""")
|
||||
if cursor.fetchone():
|
||||
# 用户表存在但版本表不存在,说明是旧版本
|
||||
return 0
|
||||
else:
|
||||
# 全新数据库
|
||||
return 0
|
||||
|
||||
# 获取当前版本
|
||||
cursor.execute("SELECT version FROM migration_versions ORDER BY version DESC LIMIT 1")
|
||||
result = cursor.fetchone()
|
||||
return result[0] if result else 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取数据库版本失败: {e}")
|
||||
return 0
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def set_version(self, version: int):
|
||||
"""设置数据库版本"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建版本表
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS migration_versions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
version INTEGER NOT NULL,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
# 插入版本记录
|
||||
cursor.execute("""
|
||||
INSERT INTO migration_versions (version, description)
|
||||
VALUES (?, ?)
|
||||
""", (version, f"Migration to version {version}"))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"数据库版本设置为: {version}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"设置数据库版本失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def execute_migration(self, version: int, description: str, sql_commands: List[str]):
|
||||
"""执行迁移"""
|
||||
logger.info(f"执行迁移 v{version}: {description}")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 执行迁移SQL命令
|
||||
for sql in sql_commands:
|
||||
if sql.strip(): # 跳过空命令
|
||||
logger.info(f"执行SQL: {sql}")
|
||||
cursor.execute(sql)
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"迁移 v{version} 执行成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"迁移 v{version} 执行失败: {e}")
|
||||
raise
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def check_column_exists(self, table_name: str, column_name: str) -> bool:
|
||||
"""检查列是否存在"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(f"PRAGMA table_info({table_name})")
|
||||
columns = [column[1] for column in cursor.fetchall()]
|
||||
return column_name in columns
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def run_migrations(self):
|
||||
"""运行所有待执行的迁移"""
|
||||
current_version = self.get_current_version()
|
||||
latest_version = self.get_latest_migration_version()
|
||||
|
||||
# 如果数据库已存在但没有版本表,创建版本表并设置为最新版本
|
||||
if current_version == 0 and latest_version > 0 and os.path.exists(self.db_path):
|
||||
# 检查users表是否已有新字段,如果有,说明是通过SQLAlchemy创建的
|
||||
if (self.check_column_exists('users', 'login_failed_count') and
|
||||
self.check_column_exists('users', 'last_failed_login') and
|
||||
self.check_column_exists('users', 'login_locked_until')):
|
||||
# 字段已存在,直接设置为最新版本
|
||||
logger.info(f"检测到现有数据库已包含最新字段,设置版本为 v{latest_version}")
|
||||
self.set_version(latest_version)
|
||||
return
|
||||
|
||||
if current_version >= latest_version:
|
||||
logger.info(f"数据库已是最新版本 v{current_version}")
|
||||
return
|
||||
|
||||
logger.info(f"开始数据库迁移: v{current_version} -> v{latest_version}")
|
||||
|
||||
# 备份数据库
|
||||
backup_path = self.backup_database()
|
||||
|
||||
try:
|
||||
# 执行迁移
|
||||
migrations = self.get_migrations()
|
||||
has_executed_migrations = False
|
||||
|
||||
for version, description, sql_commands in migrations:
|
||||
if version > current_version:
|
||||
if sql_commands: # 只有当有SQL命令时才执行迁移
|
||||
self.execute_migration(version, description, sql_commands)
|
||||
has_executed_migrations = True
|
||||
else:
|
||||
logger.info(f"迁移 v{version}: {description} - 无需执行,字段已存在")
|
||||
|
||||
# 无论是否有SQL命令,都设置版本
|
||||
self.set_version(version)
|
||||
|
||||
if has_executed_migrations:
|
||||
logger.info("数据库迁移完成")
|
||||
else:
|
||||
logger.info("数据库结构已是最新,仅更新版本记录")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"数据库迁移失败: {e}")
|
||||
if backup_path and os.path.exists(backup_path):
|
||||
logger.info(f"尝试从备份恢复: {backup_path}")
|
||||
try:
|
||||
shutil.copy2(backup_path, self.db_path)
|
||||
logger.info("数据库已从备份恢复")
|
||||
except Exception as restore_error:
|
||||
logger.error(f"数据库恢复失败: {restore_error}")
|
||||
raise
|
||||
|
||||
def get_latest_migration_version(self) -> int:
|
||||
"""获取最新迁移版本号"""
|
||||
# 这里返回硬编码的最新版本号,不依赖迁移定义
|
||||
# 因为迁移定义可能为空(字段已存在)
|
||||
return 1 # 当前最新版本是 v1
|
||||
|
||||
def get_migrations(self) -> List[Tuple[int, str, List[str]]]:
|
||||
"""获取所有迁移定义
|
||||
返回格式: [(version, description, [sql_commands])]
|
||||
"""
|
||||
migrations = []
|
||||
|
||||
# 迁移 v1: 为 users 表添加登录失败限制字段
|
||||
# 使用条件检查来避免重复添加字段
|
||||
v1_commands = []
|
||||
|
||||
# 检查并添加 login_failed_count 字段
|
||||
if not self.check_column_exists('users', 'login_failed_count'):
|
||||
v1_commands.append("ALTER TABLE users ADD COLUMN login_failed_count INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
# 检查并添加 last_failed_login 字段
|
||||
if not self.check_column_exists('users', 'last_failed_login'):
|
||||
v1_commands.append("ALTER TABLE users ADD COLUMN last_failed_login DATETIME")
|
||||
|
||||
# 检查并添加 login_locked_until 字段
|
||||
if not self.check_column_exists('users', 'login_locked_until'):
|
||||
v1_commands.append("ALTER TABLE users ADD COLUMN login_locked_until DATETIME")
|
||||
|
||||
# 如果有命令需要执行,才添加迁移
|
||||
if v1_commands:
|
||||
migrations.append((1, "为用户表添加登录失败限制字段", v1_commands))
|
||||
|
||||
# 未来的迁移可以在这里添加
|
||||
# migrations.append((
|
||||
# 2,
|
||||
# "添加新功能相关表",
|
||||
# [
|
||||
# "CREATE TABLE new_feature (...)",
|
||||
# "ALTER TABLE existing_table ADD COLUMN new_field ..."
|
||||
# ]
|
||||
# ))
|
||||
|
||||
return migrations
|
||||
|
||||
|
||||
def check_and_migrate(db_path: str):
|
||||
"""检查并执行数据库迁移"""
|
||||
migrator = DatabaseMigrator(db_path)
|
||||
|
||||
try:
|
||||
migrator.run_migrations()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"数据库迁移过程中发生错误: {e}")
|
||||
return False
|
||||
@ -206,7 +206,7 @@ const threadMessages = ref({});
|
||||
|
||||
const uiState = reactive({
|
||||
...props.state,
|
||||
isSidebarOpen: localStorage.getItem('chat_sidebar_open', 'true') === 'true',
|
||||
isSidebarOpen: localStorage.getItem('chat_sidebar_open') !== 'false',
|
||||
isInitialRender: true,
|
||||
showRenameButton: false,
|
||||
containerWidth: 0,
|
||||
|
||||
@ -27,6 +27,15 @@ export const useUserStore = defineStore('user', () => {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
|
||||
// 如果是423锁定状态码,抛出包含状态码的错误
|
||||
if (response.status === 423) {
|
||||
const lockError = new Error(error.detail || '账户被锁定')
|
||||
lockError.status = 423
|
||||
lockError.headers = response.headers
|
||||
throw lockError
|
||||
}
|
||||
|
||||
throw new Error(error.detail || '登录失败')
|
||||
}
|
||||
|
||||
|
||||
@ -119,7 +119,16 @@
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button type="primary" html-type="submit" :loading="loading" block>登录</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="loading"
|
||||
:disabled="isLocked"
|
||||
block
|
||||
>
|
||||
<span v-if="isLocked">账户已锁定 {{ formatTime(lockRemainingTime) }}</span>
|
||||
<span v-else>登录</span>
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 第三方登录选项 -->
|
||||
@ -166,7 +175,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/stores/user';
|
||||
import { useInfoStore } from '@/stores/info';
|
||||
@ -193,6 +202,11 @@ const serverStatus = ref('loading');
|
||||
const serverError = ref('');
|
||||
const healthChecking = ref(false);
|
||||
|
||||
// 登录锁定相关状态
|
||||
const isLocked = ref(false);
|
||||
const lockRemainingTime = ref(0);
|
||||
const lockCountdown = ref(null);
|
||||
|
||||
// 登录表单
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
@ -211,6 +225,49 @@ const showDevMessage = () => {
|
||||
message.info('该功能正在开发中,敬请期待!');
|
||||
};
|
||||
|
||||
// 清理倒计时器
|
||||
const clearLockCountdown = () => {
|
||||
if (lockCountdown.value) {
|
||||
clearInterval(lockCountdown.value);
|
||||
lockCountdown.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// 启动锁定倒计时
|
||||
const startLockCountdown = (remainingSeconds) => {
|
||||
clearLockCountdown();
|
||||
isLocked.value = true;
|
||||
lockRemainingTime.value = remainingSeconds;
|
||||
|
||||
lockCountdown.value = setInterval(() => {
|
||||
lockRemainingTime.value--;
|
||||
if (lockRemainingTime.value <= 0) {
|
||||
clearLockCountdown();
|
||||
isLocked.value = false;
|
||||
errorMessage.value = '';
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 格式化时间显示
|
||||
const formatTime = (seconds) => {
|
||||
if (seconds < 60) {
|
||||
return `${seconds}秒`;
|
||||
} else if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
return `${minutes}分${remainingSeconds}秒`;
|
||||
} else if (seconds < 86400) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}小时${minutes}分钟`;
|
||||
} else {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
return `${days}天${hours}小时`;
|
||||
}
|
||||
};
|
||||
|
||||
// 密码确认验证
|
||||
const validateConfirmPassword = (rule, value) => {
|
||||
if (value === '') {
|
||||
@ -224,9 +281,16 @@ const validateConfirmPassword = (rule, value) => {
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = async () => {
|
||||
// 如果当前被锁定,不允许登录
|
||||
if (isLocked.value) {
|
||||
message.warning(`账户被锁定,请等待 ${formatTime(lockRemainingTime.value)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
clearLockCountdown();
|
||||
|
||||
await userStore.login({
|
||||
username: loginForm.username,
|
||||
@ -278,7 +342,35 @@ const handleLogin = async () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
errorMessage.value = error.message || '登录失败,请检查用户名和密码';
|
||||
|
||||
// 检查是否是锁定错误(HTTP 423)
|
||||
if (error.status === 423) {
|
||||
// 尝试从响应头中获取剩余时间
|
||||
let remainingTime = 0;
|
||||
if (error.headers && error.headers.get) {
|
||||
const lockRemainingHeader = error.headers.get('X-Lock-Remaining');
|
||||
if (lockRemainingHeader) {
|
||||
remainingTime = parseInt(lockRemainingHeader);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有从头中获取到,尝试从错误消息中解析
|
||||
if (remainingTime === 0) {
|
||||
const lockTimeMatch = error.message.match(/(\d+)\s*秒/);
|
||||
if (lockTimeMatch) {
|
||||
remainingTime = parseInt(lockTimeMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (remainingTime > 0) {
|
||||
startLockCountdown(remainingTime);
|
||||
errorMessage.value = `由于多次登录失败,账户已被锁定 ${formatTime(remainingTime)}`;
|
||||
} else {
|
||||
errorMessage.value = error.message || '账户被锁定,请稍后再试';
|
||||
}
|
||||
} else {
|
||||
errorMessage.value = error.message || '登录失败,请检查用户名和密码';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -358,6 +450,11 @@ onMounted(async () => {
|
||||
// 检查是否是首次运行
|
||||
await checkFirstRunStatus();
|
||||
});
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
clearLockCountdown();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user