如何在“django-rest-framework-simplejwt”中使用`email`而不是`username`来生成令牌?

Md *_*man 11 django django-rest-framework

在Django的休息框架-simplejwt插件usernamepassword默认情况下使用。但我想使用email而不是username. 所以,我确实喜欢以下内容:

在序列化器中:

class MyTokenObtainSerializer(Serializer):
    username_field = User.EMAIL_FIELD

    def __init__(self, *args, **kwargs):
        super(MyTokenObtainSerializer, self).__init__(*args, **kwargs)

        self.fields[self.username_field] = CharField()
        self.fields['password'] = PasswordField()

    def validate(self, attrs):
        # self.user = authenticate(**{
        #     self.username_field: attrs[self.username_field],
        #     'password': attrs['password'],
        # })
        self.user = User.objects.filter(email=attrs[self.username_field]).first()
        print(self.user)

        if not self.user:
            raise ValidationError('The user is not valid.')

        if self.user:
            if not self.user.check_password(attrs['password']):
                raise ValidationError('Incorrect credentials.')
        print(self.user)
        # Prior to Django 1.10, inactive users could be authenticated with the
        # default `ModelBackend`.  As of Django 1.10, the `ModelBackend`
        # prevents inactive users from authenticating.  App designers can still
        # allow inactive users to authenticate by opting for the new
        # `AllowAllUsersModelBackend`.  However, we explicitly prevent inactive
        # users from authenticating to enforce a reasonable policy and provide
        # sensible backwards compatibility with older Django versions.
        if self.user is None or not self.user.is_active:
            raise ValidationError('No active account found with the given credentials')

        return {}

    @classmethod
    def get_token(cls, user):
        raise NotImplemented(
            'Must implement `get_token` method for `MyTokenObtainSerializer` subclasses')


class MyTokenObtainPairSerializer(MyTokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

    def validate(self, attrs):
        data = super(MyTokenObtainPairSerializer, self).validate(attrs)

        refresh = self.get_token(self.user)

        data['refresh'] = text_type(refresh)
        data['access'] = text_type(refresh.access_token)

        return data
Run Code Online (Sandbox Code Playgroud)

鉴于:

class MyTokenObtainPairView(TokenObtainPairView):
   """
    Takes a set of user credentials and returns an access and refresh JSON web
    token pair to prove the authentication of those credentials.
   """
    serializer_class = MyTokenObtainPairSerializer
Run Code Online (Sandbox Code Playgroud)

它有效!!

现在我的问题是,我怎样才能更有效地做到这一点?任何人都可以对此提出建议吗?提前致谢。

Dav*_*art 12

这个答案是为了未来的读者,因此包含额外的信息。

为了简化身份验证后端,您需要挂钩多个类。我建议执行下面的选项 1(以及可选的选项 3,您的简化版本)。在继续阅读之前请注意以下几点:

  • 注意 1: django 不会强制要求电子邮件或在用户创建时保持唯一(您可以覆盖此设置,但它是偏离主题的)!因此,选项 3(您的实施)可能会给您带来重复电子邮件的问题。
  • 注 1b:用于User.objects.filter(email__iexact=...)以不区分大小写的方式匹配电子邮件。
  • Note 1c:当你将来更换默认的用户模型时使用get_user_model(),这对于初学者来说真的是一个救星!
  • 注意 2:避免将用户打印到控制台。您可能正在打印敏感数据。

至于3个选项:

  1. 使用 fe 调整 django 身份验证后端class EmailModelBackend(ModelBackend)并替换身份验证功能。
    • 不调整代币声明
    • 不依赖于 JWT 类/中间件(SimpleJWT、JWT 或其他)
    • 还调整其他身份验证类型(Session/Cookie/非 API 身份验证等)
    • 所需的输入参数仍然是username,示例如下。如果您不喜欢它,请进行调整,但要小心。(可能会破坏您的导入/插件,但不是必需的!)
  2. authenticate(username=, password=, **kwarg)django.contrib.auth替换 django
    • 不调整代币声明
    • 您还需要替换令牌后端,因为它应该使用不同的身份验证,就像上面所做的那样。
    • 不使用 调整其他应用程序authenticate(...),仅替换 JWT 身份验证(如果您这样设置)则不需要参数,因此不建议使用此选项)。
  3. MyTokenObtainPairSerializer使用电子邮件作为声明来 实施。
    • 现在电子邮件作为 JWT 数据(而不是 ID)发回。
    • 与选项 1 一起,您的应用程序身份验证已变得与用户名无关。

选项 1(请注意,这也允许用户名!!):

from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

class EmailorUsernameModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        UserModel = get_user_model()
        try:
            user = UserModel.objects.get(Q(username__iexact=username) | Q(email__iexact=username))
        except UserModel.DoesNotExist:
            return None
        else:
            if user.check_password(password):
                return user
        return None
Run Code Online (Sandbox Code Playgroud)

选项 2: 跳过,留给读者,不建议。

选项 3: 您似乎已经在上面介绍了这一点。

注意:您不必定义,您可以在 urls.py 中MyTokenObtainPairView使用。TokenObtainPairView(serializer_class=MyTokenObtainPairSerializer).as_view()小的简化覆盖了使用的令牌序列化器

注 2:您也可以在 settings.py(或设置文件)中指定识别声明和添加的数据,以使用电子邮件。这将使您的前端应用程序也使用电子邮件进行索赔(而不是默认用户.id)

SIMPLE_JWT = {
    'USER_ID_FIELD': 'id', # model property to attempt claims for
    'USER_ID_CLAIM': 'user_id', # actual keyword in token data
}
Run Code Online (Sandbox Code Playgroud)

但是,请注意创建者给出的独特性警告:

例如,指定“用户名”或“电子邮件”字段将是一个糟糕的选择,因为帐户的用户名或电子邮件可能会根据给定服务中帐户管理的设计方式而变化。

如果你能保证唯一性,那么一切就都准备好了。


小智 7

为什么要复制和粘贴这么多而不是子类化?我让它工作:

# serializers.py 
from rest_framework_simplejwt.serializers import TokenObtainSerializer

class EmailTokenObtainSerializer(TokenObtainSerializer):
    username_field = User.EMAIL_FIELD


class CustomTokenObtainPairSerializer(EmailTokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

    def validate(self, attrs):
        data = super().validate(attrs)

        refresh = self.get_token(self.user)

        data["refresh"] = str(refresh)
        data["access"] = str(refresh.access_token)

        return data

Run Code Online (Sandbox Code Playgroud)

# views.py
from rest_framework_simplejwt.views import TokenObtainPairView

class EmailTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer
Run Code Online (Sandbox Code Playgroud)

而且当然

#urls.py
from rest_framework_simplejwt.views import TokenRefreshView
from .views import EmailTokenObtainPairView

url("token/", EmailTokenObtainPairView.as_view(), name="token_obtain_pair"),
url("refresh/", TokenRefreshView.as_view(), name="token_refresh"),

Run Code Online (Sandbox Code Playgroud)


小智 5

对于那些使用自定义用户模型的人,您只需添加这些行:

class User(AbstractUser):
    ... 
    email = models.EmailField(verbose_name='email address', max_length=255, unique=True)
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

Run Code Online (Sandbox Code Playgroud)

然后,在 urls.py 中:

from rest_framework_simplejwt.views import TokenObtainPairView

urlpatterns = [
    ... 
    path('api/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
Run Code Online (Sandbox Code Playgroud)