Update user and login security configs (#922)

* Update user and login security configs

* Optimize some code definitions

* Update config comments

* Update the captcha check

* Update the config plugin sql scripts

* Add user password history model to init

* Fix some logic errors

* Add last_password_changed_time to user sql

* Fix user update password

* Fix the dynamic config check

* Update the user sql style
This commit is contained in:
Wu Clan
2025-11-16 21:23:36 +08:00
committed by GitHub
parent 2c0acb1103
commit cf9e5dc4f4
33 changed files with 707 additions and 247 deletions
+9
View File
@@ -0,0 +1,9 @@
from backend.common.enums import StrEnum
class ConfigType(StrEnum):
"""配置类型"""
email = 'EMAIL'
user_security = 'USER_SECURITY'
login = 'LOGIN'
+12 -1
View File
@@ -5,4 +5,15 @@ values
(3, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(4, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
(5, '邮箱密码', 'EMAIL', 'EMAIL_PASSWORD', '', false, null, now(), null),
(6, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', '1', false, null, now(), null);
(6, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', 'true', false, null, now(), null),
(7, '状态', 'USER_SECURITY', 'USER_SECURITY_CONFIG_STATUS', '1', false, null, now(), null),
(8, '密码错误锁定阈值', 'USER_SECURITY', 'USER_LOCK_THRESHOLD', '5', false, '0 表示禁用锁定', now(), null),
(9, '密码错误锁定时长(秒)', 'USER_SECURITY', 'USER_LOCK_SECONDS', '300', false, null, now(), null),
(10, '密码有效期(天)', 'USER_SECURITY', 'USER_PASSWORD_EXPIRY_DAYS', '365', false, '0 表示永不过期', now(), null),
(11, '密码到期提醒(天)', 'USER_SECURITY', 'USER_PASSWORD_REMINDER_DAYS', '7', false, '0 表示不提醒', now(), null),
(12, '密码历史检查次数', 'USER_SECURITY', 'USER_PASSWORD_HISTORY_CHECK_COUNT', '3', false, null, now(), null),
(13, '密码最小长度', 'USER_SECURITY', 'USER_PASSWORD_MIN_LENGTH', '6', false, null, now(), null),
(14, '密码最大长度', 'USER_SECURITY', 'USER_PASSWORD_MAX_LENGTH', '32', false, null, now(), null),
(15, '密码必须包含特殊字符', 'USER_SECURITY', 'USER_PASSWORD_REQUIRE_SPECIAL_CHAR', 'false', false, null, now(), null),
(16, '状态', 'LOGIN', 'LOGIN_CONFIG_STATUS', '1', false, null, now(), null),
(17, '验证码开关', 'LOGIN', 'LOGIN_CAPTCHA_ENABLED', 'true', false, null, now(), null);
@@ -5,4 +5,15 @@ values
(2069061886627938306, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(2069061886627938307, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
(2069061886627938308, '邮箱密码', 'EMAIL', 'EMAIL_PASSWORD', '', false, null, now(), null),
(2069061886627938309, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', '1', false, null, now(), null);
(2069061886627938309, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', 'true', false, null, now(), null),
(2069061886627938310, '状态', 'USER_SECURITY', 'USER_SECURITY_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938311, '密码错误锁定阈值', 'USER_SECURITY', 'USER_LOCK_THRESHOLD', '5', false, '0 表示禁用锁定', now(), null),
(2069061886627938312, '密码错误锁定时长(秒)', 'USER_SECURITY', 'USER_LOCK_SECONDS', '300', false, null, now(), null),
(2069061886627938313, '密码有效期(天)', 'USER_SECURITY', 'USER_PASSWORD_EXPIRY_DAYS', '365', false, '0 表示永不过期', now(), null),
(2069061886627938314, '密码到期提醒(天)', 'USER_SECURITY', 'USER_PASSWORD_REMINDER_DAYS', '7', false, '0 表示不提醒', now(), null),
(2069061886627938315, '密码历史检查次数', 'USER_SECURITY', 'USER_PASSWORD_HISTORY_CHECK_COUNT', '3', false, null, now(), null),
(2069061886627938316, '密码最小长度', 'USER_SECURITY', 'USER_PASSWORD_MIN_LENGTH', '6', false, null, now(), null),
(2069061886627938317, '密码最大长度', 'USER_SECURITY', 'USER_PASSWORD_MAX_LENGTH', '32', false, null, now(), null),
(2069061886627938318, '密码必须包含特殊字符', 'USER_SECURITY', 'USER_PASSWORD_REQUIRE_SPECIAL_CHAR', 'false', false, null, now(), null),
(2069061886627938319, '状态', 'LOGIN', 'LOGIN_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938320, '验证码开关', 'LOGIN', 'LOGIN_CAPTCHA_ENABLED', 'true', false, null, now(), null);
+12 -1
View File
@@ -5,6 +5,17 @@ values
(3, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(4, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
(5, '邮箱密码', 'EMAIL', 'EMAIL_PASSWORD', '', false, null, now(), null),
(6, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', '1', false, null, now(), null);
(6, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', 'true', false, null, now(), null),
(7, '状态', 'USER_SECURITY', 'USER_SECURITY_CONFIG_STATUS', '1', false, null, now(), null),
(8, '密码错误锁定阈值', 'USER_SECURITY', 'USER_LOCK_THRESHOLD', '5', false, '0 表示禁用锁定', now(), null),
(9, '密码错误锁定时长(秒)', 'USER_SECURITY', 'USER_LOCK_SECONDS', '300', false, null, now(), null),
(10, '密码有效期(天)', 'USER_SECURITY', 'USER_PASSWORD_EXPIRY_DAYS', '365', false, '0 表示永不过期', now(), null),
(11, '密码到期提醒(天)', 'USER_SECURITY', 'USER_PASSWORD_REMINDER_DAYS', '7', false, '0 表示不提醒', now(), null),
(12, '密码历史检查次数', 'USER_SECURITY', 'USER_PASSWORD_HISTORY_CHECK_COUNT', '3', false, null, now(), null),
(13, '密码最小长度', 'USER_SECURITY', 'USER_PASSWORD_MIN_LENGTH', '6', false, null, now(), null),
(14, '密码最大长度', 'USER_SECURITY', 'USER_PASSWORD_MAX_LENGTH', '32', false, null, now(), null),
(15, '密码必须包含特殊字符', 'USER_SECURITY', 'USER_PASSWORD_REQUIRE_SPECIAL_CHAR', 'false', false, null, now(), null),
(16, '状态', 'LOGIN', 'LOGIN_CONFIG_STATUS', '1', false, null, now(), null),
(17, '验证码开关', 'LOGIN', 'LOGIN_CAPTCHA_ENABLED', 'true', false, null, now(), null);
select setval(pg_get_serial_sequence('sys_config', 'id'),coalesce(max(id), 0) + 1, true) from sys_config;
@@ -5,4 +5,15 @@ values
(2069061886627938306, '服务器端口', 'EMAIL', 'EMAIL_PORT', '465', false, null, now(), null),
(2069061886627938307, '邮箱账号', 'EMAIL', 'EMAIL_USERNAME', 'fba@qq.com', false, null, now(), null),
(2069061886627938308, '邮箱密码', 'EMAIL', 'EMAIL_PASSWORD', '', false, null, now(), null),
(2069061886627938309, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', '1', false, null, now(), null);
(2069061886627938309, 'SSL 加密', 'EMAIL', 'EMAIL_SSL', 'true', false, null, now(), null),
(2069061886627938310, '状态', 'USER_SECURITY', 'USER_SECURITY_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938311, '密码错误锁定阈值', 'USER_SECURITY', 'USER_LOCK_THRESHOLD', '5', false, '0 表示禁用锁定', now(), null),
(2069061886627938312, '密码错误锁定时长(秒)', 'USER_SECURITY', 'USER_LOCK_SECONDS', '300', false, null, now(), null),
(2069061886627938313, '密码有效期(天)', 'USER_SECURITY', 'USER_PASSWORD_EXPIRY_DAYS', '365', false, '0 表示永不过期', now(), null),
(2069061886627938314, '密码到期提醒(天)', 'USER_SECURITY', 'USER_PASSWORD_REMINDER_DAYS', '7', false, '0 表示不提醒', now(), null),
(2069061886627938315, '密码历史检查次数', 'USER_SECURITY', 'USER_PASSWORD_HISTORY_CHECK_COUNT', '3', false, null, now(), null),
(2069061886627938316, '密码最小长度', 'USER_SECURITY', 'USER_PASSWORD_MIN_LENGTH', '6', false, null, now(), null),
(2069061886627938317, '密码最大长度', 'USER_SECURITY', 'USER_PASSWORD_MAX_LENGTH', '32', false, null, now(), null),
(2069061886627938318, '密码必须包含特殊字符', 'USER_SECURITY', 'USER_PASSWORD_REQUIRE_SPECIAL_CHAR', 'false', false, null, now(), null),
(2069061886627938319, '状态', 'LOGIN', 'LOGIN_CONFIG_STATUS', '1', false, null, now(), null),
(2069061886627938320, '验证码开关', 'LOGIN', 'LOGIN_CAPTCHA_ENABLED', 'true', false, null, now(), null);
+9 -49
View File
@@ -4,17 +4,12 @@ from email.mime.text import MIMEText
from aiosmtplib import SMTP
from anyio import open_file
from jinja2 import Template
from sqlalchemy import inspect
from sqlalchemy.ext.asyncio import AsyncConnection, AsyncSession
from sqlalchemy.ext.asyncio import AsyncSession
from backend.common.enums import StatusType
from backend.common.exception import errors
from backend.common.log import log
from backend.core.conf import settings
from backend.core.path_conf import PLUGIN_DIR
from backend.database.db import async_engine
from backend.plugin.config.crud.crud_config import config_dao
from backend.utils.serializers import select_list_serialize
from backend.utils.dynamic_config import load_email_config
from backend.utils.timezone import timezone
@@ -62,52 +57,17 @@ async def send_email(
:param template: 邮件内容模板
:return:
"""
# 本地配置
email_host = settings.EMAIL_HOST
email_port = settings.EMAIL_PORT
email_ssl = settings.EMAIL_SSL
email_username = settings.EMAIL_USERNAME
email_password = settings.EMAIL_PASSWORD
# 动态配置
dynamic_config = None
def get_config_table(conn: AsyncConnection) -> bool:
inspector = inspect(conn)
return inspector.has_table('sys_config', schema=None)
async with async_engine.begin() as coon:
exists = await coon.run_sync(get_config_table)
if exists:
dynamic_config = await config_dao.get_all(db, 'EMAIL')
if dynamic_config:
status_key = 'EMAIL_STATUS'
host_key = 'EMAIL_HOST'
port_key = 'EMAIL_PORT'
ssl_key = 'EMAIL_SSL'
username_key = 'EMAIL_USERNAME'
password_key = 'EMAIL_PASSWORD'
configs = {d['key']: d['value'] for d in select_list_serialize(dynamic_config)}
if configs.get(status_key):
if len(dynamic_config) < 6:
raise errors.NotFoundError(msg='缺少邮件动态配置,请检查系统参数配置-邮件配置')
email_host = configs.get(host_key)
email_port = int(configs.get(port_key, 0))
email_ssl = configs.get(ssl_key, '') == str(StatusType.enable.value)
email_username = configs.get(username_key)
email_password = configs.get(password_key)
await load_email_config(db)
try:
message = await render_message(subject, email_username, content, template)
message = await render_message(subject, settings.EMAIL_USERNAME, content, template)
smtp_client = SMTP(
hostname=email_host,
port=email_port,
use_tls=email_ssl,
hostname=settings.EMAIL_HOST,
port=settings.EMAIL_PORT,
use_tls=settings.EMAIL_SSL,
)
async with smtp_client:
await smtp_client.login(email_username, email_password)
await smtp_client.sendmail(email_username, recipients, message)
await smtp_client.login(settings.EMAIL_USERNAME, settings.EMAIL_PASSWORD)
await smtp_client.sendmail(settings.EMAIL_USERNAME, recipients, message)
except Exception as e:
log.error(f'电子邮件发送失败:{e}')
+10 -10
View File
@@ -86,20 +86,20 @@ class OAuth2Service:
await user_social_dao.create(db, new_user_social)
# 创建 token
access_token = await jwt.create_access_token(
access_token_data = await jwt.create_access_token(
sys_user.id,
multi_login=sys_user.is_multi_login,
# extra info
username=sys_user.username,
nickname=sys_user.nickname or f'#{text_captcha(5)}',
nickname=sys_user.nickname,
last_login_time=timezone.to_str(timezone.now()),
ip=ctx.ip,
os=ctx.os,
browser=ctx.browser,
device=ctx.device,
)
refresh_token = await jwt.create_refresh_token(
access_token.session_uuid,
refresh_token_data = await jwt.create_refresh_token(
access_token_data.session_uuid,
sys_user.id,
multi_login=sys_user.is_multi_login,
)
@@ -114,18 +114,18 @@ class OAuth2Service:
status=LoginLogStatusType.success.value,
msg=t('success.login.oauth2_success'),
)
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ctx.ip}')
await redis_client.delete(f'{settings.LOGIN_CAPTCHA_REDIS_PREFIX}:{ctx.ip}')
response.set_cookie(
key=settings.COOKIE_REFRESH_TOKEN_KEY,
value=refresh_token.refresh_token,
value=refresh_token_data.refresh_token,
max_age=settings.COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS,
expires=timezone.to_utc(refresh_token.refresh_token_expire_time),
expires=timezone.to_utc(refresh_token_data.refresh_token_expire_time),
httponly=True,
)
data = GetLoginToken(
access_token=access_token.access_token,
access_token_expire_time=access_token.access_token_expire_time,
session_uuid=access_token.session_uuid,
access_token=access_token_data.access_token,
access_token_expire_time=access_token_data.access_token_expire_time,
session_uuid=access_token_data.session_uuid,
user=sys_user, # type: ignore
)
return data