chore: 基线提交(重构前代码快照)
- git 初始化 + .gitignore 补全(根 .env / logs / instance / __pycache__ 等) - 修复 utils/.gitignore 误将整个 utils/ 源码层忽略的问题,补入全部工具源码(含 ip2region.xdb) - 含后端全部代码:application / extensions / utils / config / migrations - 含前端 ui/(后续整体迁走) - 不含根 .env(含密钥,本地保留) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# +======================================================================
|
||||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||
# +----------------------------------------------------------------------
|
||||
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||
# +----------------------------------------------------------------------
|
||||
# | 开源协议: Apache-2.0
|
||||
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
||||
# +----------------------------------------------------------------------
|
||||
# | 官方网站: https://www.djangoadmin.cn
|
||||
# +----------------------------------------------------------------------
|
||||
# | 研发团队: 一米阳光团队 荣誉出品
|
||||
# +======================================================================
|
||||
# | 版权和免责声明:
|
||||
# | 一、知识产权声明
|
||||
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
||||
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
||||
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
||||
# | 二、使用限制与禁止条款
|
||||
# | 您理解并同意,禁止将本产品用于以下任何行为:
|
||||
# | 1. 任何违反中华人民共和国法律法规的用途;
|
||||
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
||||
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
||||
# | 三、责任豁免
|
||||
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
||||
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
||||
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
||||
# | 四、违约与法律责任
|
||||
# | 如发现任何违反本声明的行为,本团队有权:
|
||||
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
||||
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
||||
# | 3. 保留进一步采取刑事控告等法律措施的权利。
|
||||
# | 五、许可范围说明
|
||||
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
||||
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
||||
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
||||
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
||||
# | 成的一切经济损失及商誉损害。
|
||||
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
||||
# | 许可协议》附件为准。
|
||||
# +======================================================================
|
||||
|
||||
from flask import request, g
|
||||
from sqlalchemy import and_
|
||||
|
||||
from application.forms.login import LoginForm
|
||||
from application.models.login_log import LoginLog
|
||||
from application.models.user import User
|
||||
from application.services import login_log
|
||||
from config import CAPTCHA_BYPASS_CODE, FLASK_DEMO, LOGIN_FAIL_MAX, LOGIN_LOCK_SECONDS
|
||||
from utils import regular, R
|
||||
from utils.password import verify_password
|
||||
from utils.request import parse_request_body
|
||||
from utils.jwt import create_token
|
||||
from utils.redis_client import redis
|
||||
from utils.ip import get_client_ip
|
||||
from utils.ip2region import get_ip_location
|
||||
from utils.user_agent import parse_user_agent
|
||||
|
||||
|
||||
# 系统登录
|
||||
def login():
|
||||
"""
|
||||
用户登录接口
|
||||
|
||||
处理用户登录请求,验证用户名、密码和验证码,登录成功后返回访问令牌
|
||||
|
||||
Returns:
|
||||
dict: 包含状态码、消息和数据的响应字典
|
||||
- 成功: {"code": 200, "msg": "登录成功", "data": {"access_token": "token值"}}
|
||||
- 失败: {"code": 非200, "msg": "错误信息"}
|
||||
"""
|
||||
# 获取参数
|
||||
json_data, error = parse_request_body()
|
||||
if error:
|
||||
return R.failed(msg=error)
|
||||
# 表单验证
|
||||
form = LoginForm(data=json_data)
|
||||
if not form.validate():
|
||||
# 获取表单验证失败的错误信息
|
||||
err_msg = regular.get_err(form)
|
||||
return R.failed(msg=err_msg)
|
||||
|
||||
# 提取表单数据
|
||||
username = form.username.data # 登录账号
|
||||
password = form.password.data # 登录密码
|
||||
code = form.code.data # 验证码
|
||||
key = form.key.data # 验证码KEY
|
||||
|
||||
# 验证码校验
|
||||
image_code = redis.get(key)
|
||||
if FLASK_DEMO and CAPTCHA_BYPASS_CODE and code == CAPTCHA_BYPASS_CODE:
|
||||
pass # 演示模式万能验证码,仅FLASK_DEMO=True时生效
|
||||
else:
|
||||
# 验证码不区分大小写比对
|
||||
if not image_code or image_code.lower() != code.lower():
|
||||
return R.failed("验证码不正确")
|
||||
# 验证码使用后立即删除,防止重复使用
|
||||
redis.delete(key)
|
||||
|
||||
# 查询用户信息(只查询未删除的用户)
|
||||
user = User.query.filter(
|
||||
and_(
|
||||
User.username == username,
|
||||
User.is_delete == 0
|
||||
)
|
||||
).first()
|
||||
|
||||
# 校验用户是否存在
|
||||
if not user:
|
||||
_record_login(username, LoginLog.Status.ERROR, error="用户不存在")
|
||||
return R.failed("用户不存在")
|
||||
|
||||
# 校验用户状态(可选,根据业务需求)
|
||||
if hasattr(user, 'status') and user.status != 1:
|
||||
_record_login(username, LoginLog.Status.ERROR, error="账号已被禁用")
|
||||
return R.failed("账号已被禁用,请联系管理员")
|
||||
|
||||
# 检查登录失败次数(在密码验证之前,避免锁定后仍执行高成本的bcrypt计算)
|
||||
client_ip = request.remote_addr
|
||||
login_attempts_key = f"login_attempts:{client_ip}:{username}"
|
||||
attempts = redis.get(login_attempts_key) or 0
|
||||
if int(attempts) >= LOGIN_FAIL_MAX:
|
||||
_record_login(username, LoginLog.Status.ERROR, error="登录失败次数过多,账号被锁定")
|
||||
lock_minutes = LOGIN_LOCK_SECONDS // 60
|
||||
return R.failed(msg=f"登录失败次数过多,请{lock_minutes}分钟后再试")
|
||||
|
||||
# 密码验证
|
||||
if not verify_password(password, user.salt, user.password):
|
||||
# 递增失败计数,首次失败时原子设置初始值和过期时间
|
||||
if redis.ttl(login_attempts_key) == -2:
|
||||
redis.client.setex(login_attempts_key, LOGIN_LOCK_SECONDS, 1)
|
||||
else:
|
||||
redis.incr(login_attempts_key)
|
||||
_record_login(username, LoginLog.Status.ERROR, error="密码不正确")
|
||||
return R.failed(msg="密码不正确")
|
||||
|
||||
# 登录成功后清除失败计数
|
||||
redis.delete(login_attempts_key)
|
||||
|
||||
# 生成访问令牌
|
||||
access_token = create_token({
|
||||
"userId": user.id,
|
||||
"username": user.username,
|
||||
"realname": user.realname
|
||||
})
|
||||
|
||||
# 登录成功,返回令牌
|
||||
result = R.ok(
|
||||
msg="登录成功",
|
||||
data={
|
||||
"access_token": access_token
|
||||
}
|
||||
)
|
||||
|
||||
# 记录登录成功日志(传入result用于记录返回结果)
|
||||
_record_login(username, LoginLog.Status.NORMAL, result_data=result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _record_login(username, status, error="", result_data=None):
|
||||
"""
|
||||
记录登录日志(内部辅助函数)
|
||||
|
||||
自动收集请求信息(IP、UA、URL、参数等),调用 login_log.record_login_log。
|
||||
异常被吞掉,不影响登录主流程。
|
||||
|
||||
Args:
|
||||
username: 登录账号
|
||||
status: 操作状态(LoginLog.Status.NORMAL / ERROR)
|
||||
error: 错误信息(登录失败时)
|
||||
result_data: 返回结果字典(登录成功时传入 R.ok() 的返回值)
|
||||
"""
|
||||
try:
|
||||
import json
|
||||
import time
|
||||
|
||||
client_ip = get_client_ip()
|
||||
ua_info = parse_user_agent()
|
||||
|
||||
# 请求参数(密码脱敏)
|
||||
param = ''
|
||||
try:
|
||||
json_data = request.get_json(silent=True)
|
||||
if json_data:
|
||||
safe_data = dict(json_data)
|
||||
if 'password' in safe_data:
|
||||
safe_data['password'] = '******'
|
||||
param = json.dumps(safe_data, ensure_ascii=False)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 返回结果(只取响应JSON中的data字段)
|
||||
result = ''
|
||||
if result_data is not None:
|
||||
try:
|
||||
data = result_data.get_json(silent=True)
|
||||
if data and isinstance(data, dict):
|
||||
result_data_value = data.get('data')
|
||||
if result_data_value is not None:
|
||||
result = json.dumps(result_data_value, ensure_ascii=False)
|
||||
else:
|
||||
result = data.get('msg', '')
|
||||
except Exception:
|
||||
result = str(result_data)
|
||||
elif error:
|
||||
result = error
|
||||
|
||||
# 请求耗时
|
||||
start_time = getattr(g, 'request_start_time', None)
|
||||
consume_time = int((time.time() - start_time) * 1000) if start_time else 0
|
||||
|
||||
login_log.record_login_log(
|
||||
username=username,
|
||||
source=LoginLog.Source.ADMIN,
|
||||
status=status,
|
||||
log_type=LoginLog.LogType.LOGIN,
|
||||
method=request.method,
|
||||
request_method=request.endpoint or '',
|
||||
url=request.path,
|
||||
param=param,
|
||||
result=result,
|
||||
ip=client_ip,
|
||||
location=get_ip_location(client_ip),
|
||||
os=ua_info.get('os', '未知'),
|
||||
browser=ua_info.get('browser', '未知'),
|
||||
consume_time=consume_time,
|
||||
error=error,
|
||||
)
|
||||
except Exception as e:
|
||||
# 日志记录失败不影响主流程
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"记录登录日志失败: {str(e)}")
|
||||
Reference in New Issue
Block a user