# +====================================================================== # | 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)}")