使用djangorestframework-simplejwt实现Django认证时,访问令牌过期后登录接口返回Invalid Credentials错误的问题
嘿,我仔细扒了你的代码和问题描述,终于找到你卡壳的根源了,咱们一步步来解决:
问题到底出在哪?
1. 登录视图画蛇添足加了JWT认证
你的login_view类设置了authentication_classes = [JWTAuthentication],这完全是多余的!登录接口本来就是用来获取JWT令牌的入口,根本不需要提前做JWT身份验证。当你之前的access_token过期后,如果请求登录时还带着这个过期令牌,JWTAuthentication会先跳出来验证,验证失败直接就触发401了,根本轮不到你后面的邮箱密码校验逻辑,最后就会返回"Invalid credentials"的错误。
2. 自定义用户模型的认证逻辑没配对
你的自定义用户模型把email设为了USERNAME_FIELD,但Django默认的认证后端只会认username字段,你直接调用authenticate(request, email=email, password=password),大概率找不到对应的用户,自然返回None,触发错误。
3. 多余的Django会话登录调用
JWT是无状态的认证机制,你在代码里调用login(request, user)完全没必要——这个是Django用来做会话登录的,和JWT的无状态设计冲突,反而可能引入额外的状态干扰。
具体修复步骤
第一步:删掉登录视图里的JWT认证配置
直接把这行代码从login_view里移除:
class login_view(APIView): """Logs in a user""" # 把这行删掉!!! # authentication_classes = [JWTAuthentication] def post(self, request): # 后面的代码保持不变,先做这一步 ...
第二步:配置支持邮箱登录的认证后端
在settings.py里添加认证后端配置,让Django能通过邮箱找到用户:
# settings.py AUTHENTICATION_BACKENDS = [ # 自定义的邮箱认证后端 '你的app名称.backends.EmailBackend', # 保留默认的ModelBackend以防万一 'django.contrib.auth.backends.ModelBackend', ]
然后在你的app目录下新建backends.py文件,写这个自定义认证后端:
from django.contrib.auth.backends import ModelBackend from django.contrib.auth import get_user_model UserModel = get_user_model() class EmailBackend(ModelBackend): def authenticate(self, request, email=None, password=None, **kwargs): # 用邮箱找用户 try: user = UserModel.objects.get(email=email) except UserModel.DoesNotExist: return None # 验证密码是否正确 if user.check_password(password) and self.user_can_authenticate(user): return user return None def get_user(self, user_id): # 根据用户ID获取用户 try: return UserModel.objects.get(pk=user_id) except UserModel.DoesNotExist: return None
第三步:去掉多余的会话登录代码
把login(request, user)这行代码删掉,JWT不需要这个:
if user is not None: # 删掉这行!!! # login(request, user) # 下面生成令牌的代码保持不变 refresh = RefreshToken.for_user(user) access_token = str(refresh.access_token) refresh_token = str(refresh) return Response({'access_token': access_token, 'refresh_token': refresh_token})
(可选)用simplejwt自带的登录视图更省心
其实simplejwt已经给你写好了登录视图,完全不用自己造轮子,你可以直接继承它来实现你的邮箱限制逻辑:
# 可以新建serializer文件或者直接在views里写 from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer import os from rest_framework import serializers class CustomLoginSerializer(TokenObtainPairSerializer): def validate(self, attrs): target_email = os.environ.get('TARGET_EMAIL') # 先获取用户 user = self.user if user.email != target_email: raise serializers.ValidationError("Invalid email") # 调用父类的逻辑生成令牌 data = super().validate(attrs) return data class CustomLoginView(TokenObtainPairView): serializer_class = CustomLoginSerializer
然后在urls.py里配置这个视图:
path('login/', CustomLoginView.as_view(), name='login'),
这样既符合simplejwt的最佳实践,又能实现你的邮箱限制需求,代码还更简洁。
验证修复效果
- 先清掉之前的过期令牌,用正确的邮箱密码请求登录接口
- 确认能正常拿到新的access_token和refresh_token
- 等access_token过期后,再用正确的邮箱密码请求登录,应该就能正常获取新令牌,不会再返回"Invalid credentials"了
备注:内容来源于stack exchange,提问作者nicholas odhiambo

