Django + simplejwt实现多次登录尝试失败后账户锁定功能的咨询
现成可用的Python包
目前有直接适配simplejwt的第三方包可以满足需求,不需要自己从零开发:
djangorestframework-simplejwt-ratelimit:专门为simplejwt登录场景开发的限流锁定插件,原生支持按用户名/IP维度计数错误登录次数,可自定义最大尝试次数、锁定时长、错误返回提示,仅需少量配置即可接入,不需要大幅改动现有simplejwt的业务逻辑。- 也可以选择通用的DRF限流扩展
django-rest-framework-throttling的定制版本,不过适配成本比上面的专用包略高。
自研实现方案(无额外依赖)
如果不想引入第三方包,仅基于simplejwt本身也可以快速实现该功能,步骤如下:
步骤1:扩展用户模型或新增失败登录记录表
两种方案二选一即可:
- 直接在现有用户模型新增字段,适合仅需要按账号锁定的场景:
from django.db import models from django.contrib.auth.models import AbstractUser class CustomUser(AbstractUser): failed_login_attempts = models.IntegerField(default=0, verbose_name="失败登录次数") lock_until = models.DateTimeField(null=True, blank=True, verbose_name="锁定截止时间")
- 单独新建失败登录日志表,适合需要按IP锁定、留存登录审计日志的场景:
from django.db import models from django.contrib.auth import get_user_model User = get_user_model() class FailedLoginLog(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, verbose_name="关联用户") ip_address = models.GenericIPAddressField(verbose_name="登录IP") attempt_time = models.DateTimeField(auto_now_add=True, verbose_name="尝试时间")
步骤2:自定义simplejwt登录视图
重写simplejwt自带的TokenObtainPairView,在密码校验前后加入锁定判断、计数逻辑:
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework.response import Response from rest_framework import status from datetime import datetime, timedelta from django.conf import settings from .models import CustomUser # 配置项建议统一放到settings.py中 MAX_FAILED_ATTEMPTS = 5 # 最大错误尝试次数 LOCK_DURATION = timedelta(minutes=30) # 单次锁定时长 class CustomTokenObtainPairView(TokenObtainPairView): def post(self, request, *args, **kwargs): username = request.data.get("username") client_ip = request.META.get("REMOTE_ADDR") # 校验用户是否存在 try: user = CustomUser.objects.get(username=username) except CustomUser.DoesNotExist: # 可按需添加对不存在用户的IP限流逻辑,防止账号枚举 return Response({"detail": "用户名或密码错误"}, status=status.HTTP_401_UNAUTHORIZED) # 判断用户是否处于锁定状态 if user.lock_until and user.lock_until > datetime.now(): remaining_minute = (user.lock_until - datetime.now()).seconds // 60 return Response( {"detail": f"账号已锁定,请{remaining_minute}分钟后再尝试"}, status=status.HTTP_403_FORBIDDEN ) # 执行原有登录校验逻辑 serializer = self.get_serializer(data=request.data) try: serializer.is_valid(raise_exception=True) except Exception: # 校验失败,计数+1,达到阈值则触发锁定 user.failed_login_attempts += 1 if user.failed_login_attempts >= MAX_FAILED_ATTEMPTS: user.lock_until = datetime.now() + LOCK_DURATION user.save() return Response({"detail": "用户名或密码错误"}, status=status.HTTP_401_UNAUTHORIZED) # 校验成功,重置失败计数和锁定状态 user.failed_login_attempts = 0 user.lock_until = None user.save() return Response(serializer.validated_data, status=status.HTTP_200_OK)
步骤3:替换原有登录路由
把路由配置中原来的simplejwt登录视图替换为自定义的视图即可:
from django.urls import path from .views import CustomTokenObtainPairView from rest_framework_simplejwt.views import TokenRefreshView urlpatterns = [ path("api/login/", CustomTokenObtainPairView.as_view(), name="token_obtain_pair"), path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), ]
可选优化点
- 增加IP维度的锁定逻辑,防止同一个IP批量爆破多个账号
- 实现阶梯锁定规则,比如第一次锁定30分钟,第二次1小时,第三次24小时
- 后台增加用户锁定管理入口,支持管理员手动解锁账号
- 对不存在的用户名请求也做IP限流,避免恶意用户枚举系统账号
内容的提问来源于stack exchange,提问作者Pythoner
相关产品推荐
相关产品推荐

