带有 JWT 身份验证实现的 Django GraphQL API 仍然允许来自 Postman 的未经身份验证的请求获取数据。我该如何解决?

Posted

技术标签:

【中文标题】带有 JWT 身份验证实现的 Django GraphQL API 仍然允许来自 Postman 的未经身份验证的请求获取数据。我该如何解决?【英文标题】:Django GraphQL API with JWT authentication implementation still allows for unauthenticated requests from Postman get data. How do I fix this? 【发布时间】:2021-08-16 18:00:42 【问题描述】:

我已经构建了一个 Django API,它使用 django-graphql-auth 和 django-graphql-jwt 包来实现身份验证。我按照包的文档进行操作,一切正常,一切都在我的 Angular UI 中运行。唯一的问题是,即使是从 Postman 发出的没有 Authorization 标头的请求,也能够从 graphql API 获取数据。

这是我的 Django 项目的 settings.py

"""
Django settings for myproject project.

Generated by 'django-admin startproject' using Django 3.2.3.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""
from pathlib import Path
import os
import sys

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-)3@2sm6lgn_p83_t(l-44hd16ou5-qbk=rso!$b1#$fu*n2^rq'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ["*"]

CORS_ORIGIN_ALLOW_ALL = True

# Application definition

INSTALLED_APPS = [
    'corsheaders',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp',
    'graphene_django',
    'graphql_jwt.refresh_token.apps.RefreshTokenConfig',
    'graphql_auth',
    'rest_framework',
    'django_filters'
]

GRAPHENE = 
    'SCHEMA': 'myproject.schema.schema',
    'MIDDLEWARE': [
        'graphql_jwt.middleware.JSONWebTokenMiddleware',
    ],


GRAPHENE_DJANGO_EXTRAS = 
    'DEFAULT_PAGINATION_CLASS': 'graphene_django_extras.paginations.LimitOffsetGraphqlPagination',
    'DEFAULT_PAGE_SIZE': 20,
    'MAX_PAGE_SIZE': 50,
    'CACHE_ACTIVE': True,
    'CACHE_TIMEOUT': 300    # seconds



MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'common.utils.UpdateLastActivityMiddleware'
]

AUTHENTICATION_BACKENDS = [
    'graphql_auth.backends.GraphQLAuthBackend',
    'django.contrib.auth.backends.ModelBackend',
]

GRAPHQL_JWT = 
    "JWT_ALLOW_ANY_CLASSES": [
        "graphql_auth.mutations.Register",
        "graphql_auth.mutations.VerifyAccount",
        "graphql_auth.mutations.ResendActivationEmail",
        "graphql_auth.mutations.SendPasswordResetEmail",
        "graphql_auth.mutations.PasswordReset",
        "graphql_auth.mutations.ObtainJSONWebToken",
        "graphql_auth.mutations.VerifyToken",
        "graphql_auth.mutations.RefreshToken",
        "graphql_auth.mutations.RevokeToken",
    ],
    'JWT_PAYLOAD_HANDLER': 'common.utils.jwt_payload',
    "JWT_VERIFY_EXPIRATION": True,
    "JWT_LONG_RUNNING_REFRESH_TOKEN": True


EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

ROOT_URLCONF = 'myproject.urls'

TEMPLATES = [
    
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates'), ],
        'APP_DIRS': True,
        'OPTIONS': 
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        ,
    ,
]

WSGI_APPLICATION = 'myproject.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

# DATABASES = 
#     'default': 
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     
# 

DATABASES = 
    'default': 
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myprojectdb',
        'USER': 'myprojectadmin',
        'PASSWORD': 'password',
        'HOST': 'db',
        'PORT': '5432',
    


# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    ,
]


# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, javascript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

STATIC_URL = '/static/'

STATICFILES_DIRS = (
    BASE_DIR / "static",
    '/var/www/static/',
)

# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'

# This is here because we are using a custom User model
# https://docs.djangoproject.com/en/2.2/topics/auth/customizing/#substituting-a-custom-user-model
AUTH_USER_MODEL = "myapp.User"

urls.py

from django.contrib import admin
from django.urls import include, path
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('', include('myapp.urls')),
    path('admin/', admin.site.urls),
    path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True))),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

示例查询:-

query users 
  users 
    id
    nickName
    lastLogin
  

如您所见,我已按照所有必要的步骤来适应两个包中的说明。如何防止未经授权的请求访问我的数据?

更新:-

我正在使用Django-graphql-extras 进行分页,在我的 Graphql api 上进行过滤。所以查询都使用该包中的内置方法。

然而,突变是手动的。

查询文件:-

from graphene_django.types import ObjectType
from .gqTypes import InstitutionType, UserType, GroupType
from graphene_django_extras import DjangoObjectField, DjangoFilterPaginateListField, LimitOffsetGraphqlPagination


class Query(ObjectType):
    institution = DjangoObjectField(
        InstitutionType, description='Single User query')
    user = DjangoObjectField(UserType, description='Single User query')
    group = DjangoObjectField(GroupType, description='Single User query')
    institutions = DjangoFilterPaginateListField(
        InstitutionType, pagination=LimitOffsetGraphqlPagination())
    users = DjangoFilterPaginateListField(
        UserType, pagination=LimitOffsetGraphqlPagination())
    groups = DjangoFilterPaginateListField(
        GroupType, pagination=LimitOffsetGraphqlPagination())

变异代码示例:-

class CreateUser(graphene.Mutation):
    class Meta:
        description = "Mutation to create a new User"

    class Arguments:
        input = UserInput(required=True)

    ok = graphene.Boolean()
    user = graphene.Field(UserType)

    @staticmethod
    def mutate(root, info, input=None):
        ok = True
        error = ""
        if input.name is None:
            error += "Name is a required field<br />"
        if len(error) > 0:
            raise GraphQLError(error)
        searchField = input.name
        searchField += input.title if input.title is not None else ""
        searchField += input.bio if input.bio is not None else ""
        searchField = searchField.lower()

        user_instance = User(user_id=input.user_id, title=input.title, bio=input.bio,
                             institution_id=input.institution_id, searchField=searchField)
        user_instance.save()
        return CreateUser(ok=ok, user=user_instance)

【问题讨论】:

你能添加任何查询或突变吗? 不确定这有什么帮助,但我添加了一个示例查询,我可以通过邮递员发送并获得结果。无需在标题中添加任何内容。我还添加了我的urls.py 的样子。 对不起,不是您为获取数据所做的查询,我需要您声明的查询的代码,例如用户,您在其中进行过滤并返回。 对不起,这是我的错。我应该猜到了。我已经添加了它。我正在使用 django-graphql-extras 包进行分页,所以我使用它们的内置方法进行查询定义。我认为他们还没有提供一种方法来为这些分页/过滤查询编写自定义解析器。我正在使用自定义突变。我添加了一个示例供您查看。 【参考方案1】:

您应该将 login_required 装饰器添加到您的查询和突变解析器中。像这样:

from graphql_jwt.decorators import login_required


class Query(graphene.ObjectType):
    viewer = graphene.Field(UserType)

    @login_required
    def resolve_viewer(self, info, **kwargs):
        return info.context.user

在你的情况下,把它放在 staticmethod 装饰器之后,像这样:

@staticmethod
@login_required
def mutate():
    pass

【讨论】:

我让这个为突变工作。谢谢。但我不知道如何处理 graphene-django-extras 包附带的内置查询定义。你有什么想法吗?我进行了初步搜索,但无法获得太多信息。 我不使用那个库,所以我不知道这是否可行,但我发现了这个:github.com/eamigo86/graphene-django-extras/issues/… 非常感谢,我在这里发表评论后发现了同样的事情。但我试过了,它没有用。我刚刚使用自己的分页和过滤器解决方案编写了自己的自定义查询方法。非常感谢您的帮助!

以上是关于带有 JWT 身份验证实现的 Django GraphQL API 仍然允许来自 Postman 的未经身份验证的请求获取数据。我该如何解决?的主要内容,如果未能解决你的问题,请参考以下文章

带有 jwt 身份验证的 django rest api 要求 csrf 令牌

Django Rest Framework JWT 未提供身份验证凭据

带有 okta OAUTH 令牌身份验证的 Django Rest API

JSON:带有 django-rest-framework-json-api 和 JWT 的 API

Grafana:如何使用 JWT 身份验证?

如何自定义 django jwt graphql 身份验证