mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
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:
@@ -0,0 +1,9 @@
|
||||
from backend.common.enums import StrEnum
|
||||
|
||||
|
||||
class ConfigType(StrEnum):
|
||||
"""配置类型"""
|
||||
|
||||
email = 'EMAIL'
|
||||
user_security = 'USER_SECURITY'
|
||||
login = 'LOGIN'
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user