英文:
Custom authentication backend works but doens't work with Django Rest's Token Authentication
问题
我的用户模型有一个电子邮件字段和一个用户名字段。USERNAME_FIELD被设置为电子邮件。但是我编写了一个身份验证后端,以支持使用用户名登录:
class UsernameAuthBackend(BaseBackend):
    def authenticate(self, request, email=None, password=None):
        try:
            user = User.objects.get(username=email)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
settings.py:
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.UsernameAuthBackend",
]
这个方法可以正常工作,但是我无法使用用户名来获取用户的令牌(也无法在Django的管理面板上工作)。
英文:
My User model has an email field and a username field. `USERNAME_FIELD' is set to email. But I wrote an authentication backend to also support logging in using username:
class UsernameAuthBackend(BaseBackend):
    def authenticate(self, request, email=None, password=None):
        try:
            user = User.objects.get(username=email)
            if user.check_password(password):
                return user
            return None
        except User.DoesNotExist:
            return None
    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
settings.py:
AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "accounts.authentication.UsernameAuthBackend",
]
This works fine but it I can't use usernames with obtain_auth_token to get a user's token. (It also doesn't work on Django's admin panel.)
答案1
得分: 1
DRF
覆盖序列化器可以提供一种更简洁的方式来调整obtain_auth_token的身份验证数据输入。这是因为实际的身份验证逻辑通常在序列化器内部处理。
以下是如何创建一个自定义序列化器来处理电子邮件和用户名的方法:
- 自定义身份验证序列化器:
 
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework import serializers
from django.contrib.auth import authenticate
class CustomAuthTokenSerializer(AuthTokenSerializer):
    username = serializers.CharField(label="Username or Email")
    def validate(self, attrs):
        username_or_email = attrs.get('username')
        password = attrs.get('password')
        # 首先尝试使用电子邮件进行身份验证
        try:
            user = User.objects.get(email=username_or_email)
        except User.DoesNotExist:
            # 如果电子邮件不存在,则尝试使用用户名进行身份验证
            try:
                user = User.objects.get(username=username_or_email)
            except User.DoesNotExist:
                msg = '无法使用提供的凭据登录。'
                raise serializers.ValidationError(msg, code='authorization')
        if not user.check_password(password):
            msg = '无法使用提供的凭据登录。'
            raise serializers.ValidationError(msg, code='authorization')
        attrs['user'] = user
        return attrs
- 更新
CustomObtainAuthToken视图以使用自定义序列化器: 
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from .serializers import CustomAuthTokenSerializer  # 调整导入路径
class CustomObtainAuthToken(ObtainAuthToken):
    serializer_class = CustomAuthTokenSerializer
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})
通过这种方式,在序列化器的validate方法中处理确定输入是用户名还是电子邮件的逻辑,使视图更简单,并与Django REST framework的模式更一致。
Django Admin
您可以使用django-username-email包或覆盖默认的身份验证表单以允许基于用户名的身份验证。以下是一种基本的方法:
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate
class CustomAdminAuthForm(AuthenticationForm):
    def clean(self):
        username_or_email = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        if username_or_email and password:
            self.user_cache = authenticate(self.request, email=username_or_email, password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
        return self.cleaned_data
# 更新您的admin.py
from django.contrib import admin
from .forms import CustomAdminAuthForm
admin.site.login_form = CustomAdminAuthForm
请注意,上述代码片段更多是概念性的,而不是确切的代码。
英文:
DRF
You can overriding the serializer can provide a cleaner way to adjust the authentication data input for obtain_auth_token. This is because the actual authentication logic is often handled within the serializer.
Here's how you can create a custom serializer to handle both email and username:
- Custom Authentication Serializer:
 
from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework import serializers
from django.contrib.auth import authenticate
class CustomAuthTokenSerializer(AuthTokenSerializer):
    username = serializers.CharField(label="Username or Email")
    def validate(self, attrs):
        username_or_email = attrs.get('username')
        password = attrs.get('password')
        # Try authentication with email first
        try:
            user = User.objects.get(email=username_or_email)
        except User.DoesNotExist:
            # If email doesn't exist, check with username
            try:
                user = User.objects.get(username=username_or_email)
            except User.DoesNotExist:
                msg = 'Unable to log in with provided credentials.'
                raise serializers.ValidationError(msg, code='authorization')
        if not user.check_password(password):
            msg = 'Unable to log in with provided credentials.'
            raise serializers.ValidationError(msg, code='authorization')
        attrs['user'] = user
        return attrs
- Update 
CustomObtainAuthTokenView to use the Custom Serializer: 
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from .serializers import CustomAuthTokenSerializer  # Adjust the import path
class CustomObtainAuthToken(ObtainAuthToken):
    serializer_class = CustomAuthTokenSerializer
    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})
This way, you're handling the logic of determining whether the input is a username or email in the serializer's validate method, making the view simpler and more consistent with Django REST framework's patterns.
Django Admin
You can use django-username-email package or override the default authentication form to allow for username-based authentication. Here's a basic approach:
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import authenticate
class CustomAdminAuthForm(AuthenticationForm):
    def clean(self):
        username_or_email = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')
        if username_or_email and password:
            self.user_cache = authenticate(self.request, email=username_or_email, password=password)
            if self.user_cache is None:
                raise forms.ValidationError(
                    self.error_messages['invalid_login'],
                    code='invalid_login',
                    params={'username': self.username_field.verbose_name},
                )
        return self.cleaned_data
# Update your admin.py
from django.contrib import admin
from .forms import CustomAdminAuthForm
admin.site.login_form = CustomAdminAuthForm
Please note that the above code snippets are more conceptual than exact code.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论