Compare commits
4 Commits
e2c778d1d6
...
0312afa7a5
| Author | SHA1 | Date | |
|---|---|---|---|
| 0312afa7a5 | |||
| bab0ecce51 | |||
| e2f4b29ac7 | |||
|
|
f853f6fce0 |
@ -1,3 +1,98 @@
|
|||||||
|
async def handle_login_failed(user_name):
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
# 1. 查询该用户当前的失败记录
|
||||||
|
record = await sor.sqlExe("SELECT first_failed_time, failed_count FROM login_fail_log WHERE user_name = '%s'" % user_name, {})
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
# === 第一次失败 ===
|
||||||
|
await sor.sqlExe("""
|
||||||
|
INSERT INTO login_fail_log (user_name, first_failed_time, failed_count)
|
||||||
|
VALUES ('%s', '%s', 1)
|
||||||
|
""" % (user_name, current_time), {})
|
||||||
|
return # 正常失败,不需要等待
|
||||||
|
|
||||||
|
|
||||||
|
first_failed_time = record[0]['first_failed_time']
|
||||||
|
failed_count = record[0]['failed_count']
|
||||||
|
|
||||||
|
first_failed_time = datetime.strptime(first_failed_time, '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# 2. 判断是否还在10分钟窗口内
|
||||||
|
time_diff = (current_time - first_failed_time).total_seconds() / 60 # 分钟
|
||||||
|
|
||||||
|
if time_diff <= 10:
|
||||||
|
# 还在10分钟窗口内,失败次数+1
|
||||||
|
new_count = failed_count + 1
|
||||||
|
|
||||||
|
if new_count >= 3:
|
||||||
|
# === 第三次及以上失败:触发锁定 ===
|
||||||
|
# 更新失败次数,并记录锁定开始时间
|
||||||
|
await sor.sqlExe("""
|
||||||
|
UPDATE login_fail_log
|
||||||
|
SET failed_count = 3, last_failed_time = '%s', lock_until = '%s'
|
||||||
|
WHERE user_name = '%s'
|
||||||
|
""" % (current_time, current_time + timedelta(minutes=20), user_name), {})
|
||||||
|
|
||||||
|
print("十分钟内登录失败三次,请等待20分钟后再试")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 第二次失败
|
||||||
|
await sor.sqlExe("""
|
||||||
|
UPDATE login_fail_log
|
||||||
|
SET failed_count = '%s', last_failed_time = '%s'
|
||||||
|
WHERE user_name = '%s'
|
||||||
|
""" % (new_count, current_time, user_name), {})
|
||||||
|
|
||||||
|
# 可选:返回还剩几次机会
|
||||||
|
if new_count == 2:
|
||||||
|
print("登录失败,还剩1次机会(10分钟内)")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# === 已经超过10分钟窗口,重置计数器 ===
|
||||||
|
await sor.sqlExe("""
|
||||||
|
UPDATE login_fail_log
|
||||||
|
SET first_failed_time = '%s', failed_count = 1, last_failed_time = '%s', lock_until = NULL
|
||||||
|
WHERE user_name = '%s'
|
||||||
|
""" % (current_time, current_time, user_name), {})
|
||||||
|
except Exception as e:
|
||||||
|
print("exception:", user_name, e)
|
||||||
|
return {'status': False, 'msg': '登录操作失败', 'error': str(e)}
|
||||||
|
|
||||||
|
async def check_login_allowed(user_name):
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext('kboss') as sor:
|
||||||
|
try:
|
||||||
|
record = await sor.sqlExe("SELECT lock_until, first_failed_time, failed_count FROM login_fail_log WHERE user_name = '%s'" % user_name, {})
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
if not record:
|
||||||
|
return {'status': True} # 允许登录
|
||||||
|
|
||||||
|
record = record[0]
|
||||||
|
if record.get('lock_until'):
|
||||||
|
record['lock_until'] = datetime.strptime(record['lock_until'], '%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# 检查是否处于锁定状态
|
||||||
|
if record.get('lock_until') and record['lock_until'] > current_time:
|
||||||
|
wait_minutes = (record['lock_until'] - current_time).total_seconds() // 60
|
||||||
|
msg = f"十分钟内登录失败三次,请等待{int(wait_minutes)}分钟后再试"
|
||||||
|
return {
|
||||||
|
'status': False,
|
||||||
|
'msg': msg
|
||||||
|
}
|
||||||
|
|
||||||
|
# 如果锁定时间已过,但还在检查窗口,也可以重置(可选)
|
||||||
|
return {'status': True} # 允许登录
|
||||||
|
except Exception as e:
|
||||||
|
print("exception:", user_name, e)
|
||||||
|
return {'status': False, 'msg': '登录操作失败, %s' % str(e)}
|
||||||
|
|
||||||
async def logintype(ns):
|
async def logintype(ns):
|
||||||
"""
|
"""
|
||||||
1、判断用户是否为主级(如果在reseller没要找到数据,证明就是主级)
|
1、判断用户是否为主级(如果在reseller没要找到数据,证明就是主级)
|
||||||
@ -46,6 +141,12 @@ async def logintype(ns):
|
|||||||
|
|
||||||
domain_name = ns.get('domain_name')
|
domain_name = ns.get('domain_name')
|
||||||
if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527'] and ns.get('username') not in ['开元云(北京)科技有限公司', 'admin', 'kyy_root']:
|
if domain_name in ['www.opencomputing.cn', 'dev.opencomputing.cn', 'localhost:9527'] and ns.get('username') not in ['开元云(北京)科技有限公司', 'admin', 'kyy_root']:
|
||||||
|
|
||||||
|
# 登录失败次数限制
|
||||||
|
login_allowed = await check_login_allowed(ns.get('username'))
|
||||||
|
if not login_allowed.get('status'):
|
||||||
|
return {'status': False, 'msg': login_allowed.get('msg')}
|
||||||
|
|
||||||
if not ns.get('mobile'):
|
if not ns.get('mobile'):
|
||||||
return {
|
return {
|
||||||
'status': False,
|
'status': False,
|
||||||
@ -77,11 +178,13 @@ async def logintype(ns):
|
|||||||
}
|
}
|
||||||
code = await sor.R('validatecode', {'id': ns.get('codeid'), 'vcode': ns.get('vcode')})
|
code = await sor.R('validatecode', {'id': ns.get('codeid'), 'vcode': ns.get('vcode')})
|
||||||
if len(code) < 1:
|
if len(code) < 1:
|
||||||
|
await handle_login_failed(ns.get('username'))
|
||||||
return {'status': False, 'msg': '验证码不正确'}
|
return {'status': False, 'msg': '验证码不正确'}
|
||||||
|
|
||||||
password = password_encode(ns['password'])
|
password = password_encode(ns['password'])
|
||||||
users = await sor.R('users', {'username': ns.get('username'), 'password': password})
|
users = await sor.R('users', {'username': ns.get('username'), 'password': password})
|
||||||
if len(users) < 1:
|
if len(users) < 1:
|
||||||
|
await handle_login_failed(ns.get('username'))
|
||||||
return {"status": False,'msg':'用户名或密码错误'}
|
return {"status": False,'msg':'用户名或密码错误'}
|
||||||
|
|
||||||
return {'status': True}
|
return {'status': True}
|
||||||
@ -100,6 +203,7 @@ async def logintype(ns):
|
|||||||
password = password_encode(ns['password'])
|
password = password_encode(ns['password'])
|
||||||
users = await sor.R('users', {'username': ns.get('username'), 'password': password})
|
users = await sor.R('users', {'username': ns.get('username'), 'password': password})
|
||||||
if len(users) < 1:
|
if len(users) < 1:
|
||||||
|
await handle_login_failed(ns.get('username'))
|
||||||
return {"status": False,'msg':'用户名或密码错误'}
|
return {"status": False,'msg':'用户名或密码错误'}
|
||||||
elif ns.get('username') == "admin":
|
elif ns.get('username') == "admin":
|
||||||
return {'status': True}
|
return {'status': True}
|
||||||
|
|||||||
@ -46,8 +46,7 @@
|
|||||||
<el-form ref="loginForm" :model="loginForm" :rules="rules" class="login-form-l" autocomplete="on"
|
<el-form ref="loginForm" :model="loginForm" :rules="rules" class="login-form-l" autocomplete="on"
|
||||||
label-position="left">
|
label-position="left">
|
||||||
|
|
||||||
<el-form-item prop="username"
|
<el-form-item prop="username" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<span class="svg-container icon-box">
|
<span class="svg-container icon-box">
|
||||||
<img src="../../assets/kyy/用户.svg" alt="">
|
<img src="../../assets/kyy/用户.svg" alt="">
|
||||||
@ -57,8 +56,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-tooltip v-model="capsTooltip" style="margin-bottom: 7px"
|
<el-tooltip v-model="capsTooltip" style="margin-bottom: 7px" content="Caps lock is On" placement="right"
|
||||||
content="Caps lock is On" placement="right" manual>
|
manual>
|
||||||
<el-form-item prop="password" class="password-mobile"
|
<el-form-item prop="password" class="password-mobile"
|
||||||
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
@ -76,8 +75,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
<el-form-item prop="mobile"
|
<el-form-item prop="mobile" style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
||||||
style="background-color: white; border: 1px solid #d9d9d9;width: 300px;">
|
|
||||||
<div class="user-input">
|
<div class="user-input">
|
||||||
<span class="svg-container icon-box">
|
<span class="svg-container icon-box">
|
||||||
<img src="../../assets/kyy/用户.svg" alt="">
|
<img src="../../assets/kyy/用户.svg" alt="">
|
||||||
@ -435,7 +433,8 @@ export default {
|
|||||||
// 轮询获取微信登录授权码
|
// 轮询获取微信登录授权码
|
||||||
pollWxCode() {
|
pollWxCode() {
|
||||||
this.getCodeTimer = setInterval(async () => {
|
this.getCodeTimer = setInterval(async () => {
|
||||||
try {x // 请求微信授权码x
|
try {
|
||||||
|
x // 请求微信授权码x
|
||||||
const res = await reqGetCodeAPI({ state: this.wxState });
|
const res = await reqGetCodeAPI({ state: this.wxState });
|
||||||
if (!res.status) return;
|
if (!res.status) return;
|
||||||
|
|
||||||
@ -879,7 +878,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$message({
|
this.$message({
|
||||||
message: "登录成功~",
|
message: res.msg,
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -899,14 +898,16 @@ export default {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: "登录失败~",
|
message: res.msg,
|
||||||
|
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$message({
|
this.$message({
|
||||||
message: "请查看登录信息格式",
|
message: res.msg,
|
||||||
|
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
this.$refs.loginForm.resetFields();
|
this.$refs.loginForm.resetFields();
|
||||||
@ -1065,7 +1066,8 @@ $dark_gray: #889aa4;
|
|||||||
|
|
||||||
/* 统一表单项目样式 + 边距 */
|
/* 统一表单项目样式 + 边距 */
|
||||||
::v-deep .el-form-item {
|
::v-deep .el-form-item {
|
||||||
margin-bottom: 24px !important; /* 给错误提示预留空间 */
|
margin-bottom: 24px !important;
|
||||||
|
/* 给错误提示预留空间 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 统一输入框外框 */
|
/* 统一输入框外框 */
|
||||||
@ -1188,6 +1190,7 @@ $dark_gray: #889aa4;
|
|||||||
top: 20px;
|
top: 20px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-top1 {
|
.logo-top1 {
|
||||||
width: 260px !important;
|
width: 260px !important;
|
||||||
height: 80px !important;
|
height: 80px !important;
|
||||||
@ -1215,6 +1218,7 @@ $dark_gray: #889aa4;
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
@ -1241,4 +1245,4 @@ $dark_gray: #889aa4;
|
|||||||
padding: 30px 20px;
|
padding: 30px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user