110%通过Django&React 2020完成JWT身份验证

ReactJS是一个很棒的前端框架,而Django是一个很棒的后端框架。 但是,像往常一样,在处理琐碎复杂的事情时,很难将两者很好地放在一起。 这不像是在墙上贴香蕉。

这是一个中级教程,超出了大多数使Django和React协同工作的教程范围。 我不仅要在这里给您留下一张不完整的图片。 这就是整个shebang。 克服它并构建有用的东西。 当我们第一次为我的初创公司Lollipop.ai做这件事时,这是一个很大的痛苦,所以请明智地使用此知识并宽恕插件。

为什么还要使用JSON Web令牌? 由于应用程序通常不支持会话,因此我们希望能够构建一个同时支持网络和应用程序的后端API,因此我们将使用JSON Web令牌或JWT来处理会话之间的身份验证切换。前端和后端。 最重要的是,JWT非常紧凑且易于使用。 有关更多信息,请在此处检查Auth0的描述

仅需6个步骤即可完成所有操作!

第1部分-Django:
1. Django自定义用户
2. DRF序列化器和身份验证

第2部分-React:
3.作为独立应用程序将React安装在我们的Django项目中
4. 准备React for Authentication ,带有路由,以及注册和登录表单
5. Axios用于请求和令牌
6. 注销和列入黑名单令牌

完整的代码位于GitHub上 ,您可以在各个步骤中浏览分支以查看代码(1-1、1-2),等等。

第1部分:Django后端

1-1)在Django中设置自定义用户

首先,新建目录以容纳我们的整个项目。

$ mkdir django-jwt-react
$ cd django-jwt-react

然后创建我们的虚拟环境并使用pipenv安装。
PS:诗歌更好,但要使第一个虚拟动作开始运行就更加困难

$ pipenv --python 3.7
$ pipenv install django djangorestframework djangorestframework-simplejwt

注意:您可能会在网上看到对名为djangorestframework-jwt的软件包的引用,但不再对其进行维护。 请改用djangorestframework-simplejwt

激活虚拟环境并创建Django项目。

$ pipenv shell
$ django-admin startproject djsr

现在,您应该在django-jwt-react/目录中包含以下内容。

-django-jwt-react/
--djsr/
---djsr/ 
----__init__.py
----settings.py
----urls.py 
----wsgi.py
---manage.py
--Pipfile
--Pipfile.lock

在大多数情况下,您将不会在项目中使用Django的常规用户,而是会添加自定义属性,例如喜欢的颜色。 但是当我们在数据库中创建了User模型后,我们修改User模型时,Django并不非常喜欢它。 为了避免这些错误,我们首先创建我们的自定义用户,然后才进行并运行数据库迁移。 Django自己建议这样做。

这是在Django中创建我们的身份验证应用程序的好时机。

$ python djsr/manage.py startapp authentication

并将其添加到settings.pyINSTALLED_APPS中。

# djsr/djsr/settings.py
INSTALLED_APPS = [
    'django.contrib.admin' ,
    'django.contrib.auth' ,
    'django.contrib.contenttypes' ,
    'django.contrib.sessions' ,
    'django.contrib.messages' ,
    'django.contrib.staticfiles' ,
    'authentication'
]

让我们在authentication/models.py创建我们的自定义用户模型,并添加fav_color属性,因为我们确实关心多彩的用户。

# djsr/authentication/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
class CustomUser ( AbstractUser ):
    fav_color  = models.CharField(blank=True, max_length= 120 )

CustomUserAbstractUser扩展,它使我们可以访问标准Django User模型的属性和功能,例如用户名,密码等。 因此,我们不需要将这些添加到我们的。 真好

并使用最基本的ModelAdmin将其添加到authentication / ModelAdmin

# djsr/authentication/admin.py
from django.contrib import admin
from .models import CustomUser
class CustomUserAdmin ( admin . ModelAdmin ):
    model  = CustomUser

admin.site.register(CustomUser, CustomUserAdmin)

最后,在settings.py ,将CustomUser配置为AUTH_USER_MODEL

# djsr/djsr/settings.py
# ...
# Custom user model
AUTH_USER_MODEL = "authentication.CustomUser"

现在,通过我们的自定义用户设置,我们可以进行并运行迁移。 在执行此操作时,还要创建一个超级用户。

$ python djsr/manage.py makemigrations
$ python djsr/manage.py migrate
$ python djsr/manage.py createsuperuser

凉。 现在运行服务器。

$  python djsr/manage.py runserver

您应该看到默认的Django成功页面。

注意:如果在创建CustomUser之前进行了迁移,则可能必须删除并重新创建数据库。

第1–1节的GitHub代码位于此处

1–2)DRF串行器和身份验证

太好了,既然您的项目已经设置了“自定义用户”,我们就可以使用该自定义用户和Django Rest Framework + DRF Simple JWT创建基于Java Web令牌的身份验证。 我们已经安装了那些。

本节将涵盖:

一个。 配置DRF + DRF简单JWT
b。 验证并获取刷新和访问令牌
C。 刷新令牌
d。 自定义获取令牌序列化器和视图以添加额外的上下文
e。 注册新用户
F。 创建和测试受保护的视图

1–2a。 配置DRF + DRF简单JWT

为了使球滚动,请在settings.py配置DRF和Simple JWT。 将“rest_framework”添加到已安装的应用程序和REST_FRAMEWORK配置字典。 不需要将Django Rest Framework简单JWT包添加到INSTALLED_APPS

# djsr/djsr/settings.py
# Needed for SIMPLE_JWT
from datetime import timedelta
# ...
INSTALLED_APPS = [
    ... 
    'rest_framework' # add rest_framework
]
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES' : (
        'rest_framework.permissions.IsAuthenticated' ,
    ),
    'DEFAULT_AUTHENTICATION_CLASSES' : (
'rest_framework_simplejwt.authentication.JWTAuthentication' ,
    ),  # 
}
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME' : timedelta(minutes= 5 ),
    'REFRESH_TOKEN_LIFETIME' : timedelta(days= 14 ),
    'ROTATE_REFRESH_TOKENS' : True,
    'BLACKLIST_AFTER_ROTATION' : False,
    'ALGORITHM' : 'HS256' ,
    'SIGNING_KEY' : SECRET_KEY,
    'VERIFYING_KEY' : None,
    'AUTH_HEADER_TYPES' : ( 'JWT' ,),
    'USER_ID_FIELD' : 'id' ,
    'USER_ID_CLAIM' : 'user_id' ,
    'AUTH_TOKEN_CLASSES' : ( 'rest_framework_simplejwt.tokens.AccessToken' ,),
    'TOKEN_TYPE_CLAIM' : 'token_type' ,
}

默认情况下,我们只允许经过身份验证的查看者访问我们的视图,并且他们可以使用来自simplejwt包的JWTAuthentication进行身份验证。

配置简单的JWT可能会有些复杂。 这里要注意的关键是刷新令牌(持续14天)用于获取访问令牌(持续5分钟)。 用户只有使用有效的访问令牌才能访问视图,否则DRF将返回401未经授权的错误。 我们会轮流刷新令牌,以便用户在14天之内访问时无需再次登录,以便于使用。 您可以在旋转令牌后将其列入黑名单,但在此不做介绍。

如果您没有使用库存user_id ,而是使用诸如电子邮件地址之类的东西,那么您还需要更改USER_ID_FIELDUSER_ID_CLAIM以与新的用户ID字段相对应。

请特别注意“ AUTH_HEADER_TYPES ”,因为您稍后在此处放置的任何值都必须反映在React的标头中。 我们将其设置为"JWT" ,但我也看到了“Bearer”用法。

无需再次进行迁移。

1-2b。 验证并获取刷新和访问令牌

我们需要先将DRF简单JWT URL添加到我们的项目中,以便能够测试登录。

# djsr/djsr/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path( 'admin/' , admin.site.urls),
    path( 'api/' , include( 'authentication.urls' ))
]

并在身份验证目录中创建一个新的urls.py ,以便我们可以使用DRF Simple JWT提供的孪生视图来获取令牌对并刷新令牌。

# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views

urlpatterns = [
    path( 'token/obtain/' , jwt_views.TokenObtainPairView.as_view(), name= 'token_create' ),  # override sjwt stock token
    path( 'token/refresh/' , jwt_views.TokenRefreshView.as_view(), name= 'token_refresh' ),
]

现在,将CURL与您先前设置的超级用户凭据一起使用。

$ curl --header "Content-Type: application/json" -X POST http: //127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}'
{ "refresh" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI" , "access" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyNTQ5LCJqdGkiOiJiMmM0MjM4MzYyZjI0MTJhYTgyODJjMTMwNWU3ZTQwYiIsInVzZXJfaWQiOjF9.0ry66-v6SUxiewAPNmcpRt99D8B8bu-fgfqOCpVnN1k" }

繁荣。 代币。 我们已通过认证! 可是等等。 有令牌复数。 Refresh令牌持续14天(我们可以认为已登录),但是Access令牌仅持续5分钟。 这意味着,只要您的用户尝试在没有有效访问令牌的情况下访问某些内容,它将被拒绝,然后您需要从前端向后端发送刷新请求以获取新的请求。 让我们用CURL做到这一点。

1–2c。 刷新令牌

从上方获取刷新令牌,然后再次使用CURL:

$ curl --header "Content-Type: application/json" -X POST http: //127.0.0.1:8000/api/token/refresh/ --data '{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMTg0OSwianRpIjoiYmE3OWUxZTEwOWJkNGU3NmI1YWZhNWQ5OTg5MTE0NjgiLCJ1c2VyX2lkIjoxfQ.S7tDJaaymUUNs74Gnt6dX2prIU_E8uqCPzMtd8Le0VI"}'
{ "access" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDEyOTQ0LCJqdGkiOiI1N2ZiZmI3ZGFhN2Y0MzkwYTZkYTc5NDhhMjdhMzMwMyIsInVzZXJfaWQiOjF9.9p-cXSn2uwwW2E0fX1FcOuIkYPcM85rUJvKBhypy1_c" , "refresh" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyMjI0NCwianRpIjoiYWUyZTNiNmRiNTI0NGUyNDliZjAyZTBiMWI3NTFmZjMiLCJ1c2VyX2lkIjoxfQ.peB-nzZRjzgMjcNASp1TZZ510p3lJt7N9SeCWUt0ngI" }

看看那,新的代币! 如果您在settings.py没有ROTATE_REFRESH_TOKENS:True ,那么Refresh令牌将是相同的,但是由于我们正在轮换使用,因此它也是一个新的Refresh令牌。 只要用户在此过期之前继续访问,就不再需要再次登录。

到底什么构成了JWT? 转至jwt.io并插入您的令牌。对于上面的“刷新令牌”,解码后您将看到。

标头:

{
  "typ" : "JWT" ,
  "alg" : "HS256"
}

有效负载:

{
  "token_type" : "refresh" ,
  "exp" : 1561622244 ,
  "jti" : "ae2e3b6db5244e249bf02e0b1b751ff3" ,
  "user_id" : 1
}

请注意, 令牌!= jti 。 JTI包含在令牌中,以及类型,到期时间和您放入令牌中的任何其他信息。

和访问令牌保存类似的信息。

看看有效载荷如何包含user_id? 您可以使用令牌添加任何想要的信息,您只需要先稍微修改一下声明即可。

1–2d。 自定义获取令牌序列化器和视图

之前,我们在CustomUser模型上添加了fav_color属性。 首先,进入管理面板127.0.0.1:8000/admin/并选择一种颜色。

DRF Simple JWT包使开发自定义声明变得非常容易,因此我们可以通过导入和使用原始序列化器子类化,在每个令牌中发送用户喜欢的颜色。

# djsr/authentication/serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer


class MyTokenObtainPairSerializer ( TokenObtainPairSerializer ):

    @ classmethod
    def get_token ( cls , user ):
        token  = super (MyTokenObtainPairSerializer, cls).get_token(user)

        # Add custom claims
        token[ 'fav_color' ] = user.fav_color
        return token

它需要与之相伴的观点。

# djsr/authentication/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework import permissions
from .serializers import MyTokenObtainPairSerializer
class ObtainTokenPairWithColorView ( TokenObtainPairView ):
    permission_classes  = (permissions.AllowAny,)
    serializer_class = MyTokenObtainPairSerializer

urls.py需要一个新条目来替换打包的条目。

# djsr/authentication/urls.py
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView
urlpatterns = [
    path( 'token/obtain/' , ObtainTokenPairWithColorView.as_view(), name= 'token_create' ),  
    path( 'token/refresh/' , jwt_views.TokenRefreshView.as_view(), name= 'token_refresh' ),
]

旧的Refresh令牌仍然可以获取新的Access令牌,因此,此时您应该将所有未使用的令牌列入黑名单,以有效地注销所有人。

要查看运行中的新令牌,请再次使用CURL。

$ curl --header "Content-Type: application/json" -X POST http: //127.0.0.1:8000/api/token/obtain/ --data '{"username":"djsr","password":"djsr"}'
{ "refresh" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYyNjQ5NywianRpIjoiODVhMmRlNWUyNjQ0NGE1ZWFmOGQ1NDAzMmM1ODUxMzIiLCJ1c2VyX2lkIjoxLCJmYXZfY29sb3IiOiIifQ.1eJr6XVZXDm0nmm19tyu9WP9AfdY8Ny_D_tK4Qtvo9E" , "access" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDE3MTk3LCJqdGkiOiI5ZjE4NmM4OTQ0ZWI0NGYyYmNmYjZiMTQ5MzkyY2Y4YSIsInVzZXJfaWQiOjEsImZhdl9jb2xvciI6IiJ9.Ad2szXkTB4eOqnRk3GIcm1NDuNixZH3rNyf9RIePXCU" }

在那里,您可以在已解码令牌中看到自己的收藏夹颜色。

凉。 为什么这有用? 取决于您的实现,但是最好有一些额外的上下文以及令牌,或者使用自定义序列化器/视图执行自定义操作。 这并不意味着要充当任何类型的get_user_info()函数。 不要那样用。

现在,我们可以登录现有用户并给他们一个令牌。 我们还有两件事要做。 注册用户并创建受保护的视图。

1–2e。 注册用户

令人惊讶的(或没有)创建新用户与JWT毫无关系。 这只是原始的Django Rest Framework。

我们不需要对CustomUser模型做任何事情,但是我们需要为其创建一个序列化器,并将其放入带有URL的视图中。

首先, CustomUserSerializer模型序列化器。 如果您不熟悉Django Rest Framework,则序列化程序主要负责将JSON转换为可用的Python数据结构,然后采取相应措施。 有关串行器的更多信息,请参阅非常好的DRF文档 。 我们使用的这个序列化器是超级典型的。

# djsr/authentication/serializers.py

from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import serializers
from .models import CustomUser
# ...
class CustomUserSerializer(serializers.ModelSerializer):
    "" "
    Currently unused in preference of the below.
    " ""
    email = serializers.EmailField(
        required=True
    )
    username = serializers.CharField()
    password = serializers.CharField(min_length= 8 , write_only=True)

    class Meta :
        model  = CustomUser
        fields = ( 'email' , 'username' , 'password' )
        extra_kwargs = { 'password' : { 'write_only' : True}}

    def create(self, validated_data):
        password = validated_data.pop( 'password' , None)
        instance = self.Meta.model(**validated_data)  # as long as the fields are the same, we can just use this
        if password is not None:
            instance.set_password(password)
        instance.save()
        return instance

对于我们的视图集,而不是使用ModelViewSet ,我们创造我们自己只是一个POST端点观点。 对于CustomUser对象的任何GET请求,我们都有一个不同的终结CustomUser

settings.py由于REST_FRAMEWORK的权限默认值仅是经身份验证的用户只能访问的视图,因此我们必须将权限显式设置为AllowAny ,否则尝试注册并向您付款的新用户将获得未授权的错误。 坏枣

当像我们在此处将数据馈送到模型序列化程序时,只要序列化程序具有create()update()方法,就可以使用serializer.save()神奇地创建(或更新)相应的对象(在我们的案例CustomUser )并返回实例。 文件。

# djsr/authentication/views.py
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView

from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer

class ObtainTokenPairWithColorView ( TokenObtainPairView ):
    serializer_class  = MyTokenObtainPairSerializer


class CustomUserCreate ( APIView ):
    permission_classes  = (permissions.AllowAny,)

    def post(self, request, format= 'json' ):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            if user:
                json = serializer.data
                return Response(json, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

最后,在urls.py我们添加了新视图。

# djsr/authentication/urls.py

from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView, CustomUserCreate

urlpatterns = [
    path( 'user/create/' , CustomUserCreate.as_view(), name= "create_user" ),
    path( 'token/obtain/' , ObtainTokenPairWithColorView.as_view(), name= 'token_create' ),
    path( 'token/refresh/' , jwt_views.TokenRefreshView.as_view(), name= 'token_refresh' ),
]

查看+序列化器+ URL =很好。

此时,您将必须确保对其进行测试,因为如果此视图仅限于经过身份验证的用户,则将获得0个新用户。 他们会很不高兴,并且当您有用户时,吸引投资者的几率会上升。 这意味着更多的卷曲。

$ curl --header "Content-Type: application/json" -X POST http: //127.0.0.1:8000/api/user/create/ --data '{"email":"ichiro@mariners.com","username":"ichiro1","password":"konnichiwa"}'
{ "email" : "ichiro@mariners.com" , "username" : "ichiro1" }

答对了! 效果很好。

在Django Rest Framework方面,我们的最后一步是创建一个受保护的视图,供我们尝试访问。

1–2f。 创建和测试受保护的视图

我们判断所有这些操作是否奏效的唯一方法是创建一个虚拟视图……我们将其称为HelloWorld……并在尝试使用和不使用Javascript Web令牌身份验证令牌访问它时对其进行保护。

让我们做一个最简单的视图。

# djsr/authentication/views.py

...

class HelloWorldView(APIView):

    def get(self, request):
        return Response(data={ "hello" : "world" }, status=status.HTTP_200_OK)

并将其添加到urls.py

# djsr/authentication/urls.py

from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from .views import ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView

urlpatterns = [
    path( 'user/create/' , CustomUserCreate.as_view(), name= "create_user" ),
    path( 'token/obtain/' , ObtainTokenPairWithColorView.as_view(), name= 'token_create' ),
    path( 'token/refresh/' , jwt_views.TokenRefreshView.as_view(), name= 'token_refresh' ),
    path( 'hello/' , HelloWorldView.as_view(), name= 'hello_world' )
]

返回CURL。 如果我们正确执行了此操作,则没有令牌的API请求将失败。

$ curl --header "Content-Type: application/json" -X GET http: //127.0.0.1:8000/api/hello/
{ "detail" : "Authentication credentials were not provided." }

如预期的那样。 现在有了凭据。 确保先刷新它们,否则访问令牌已过期。 或尝试使用新用户。

$ curl --header "Content-Type: application/json" -X POST http: //127.0.0.1:8000/api/token/obtain/ --data '{"username":"ichiro1","password":"konnichiwa"}'
{ "refresh" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTU2MTYzODQxNiwianRpIjoiMGM5MjY5NWE0ZGQwNDUyNzk2YTM5NTY3ZDMyNTRkYzgiLCJ1c2VyX2lkIjoyLCJmYXZfY29sb3IiOiIifQ.sV6oNQjQkWw2F3NLMQh5VWWleIxB9OpmIFvI5TNsUjk" , "access" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k" }
$ curl --header "Content-Type: application/json" -X GET http: //127.0.0.1:8000/api/hello/
{ "detail" : "Authentication credentials were not provided." }
$ curl --header "Content-Type: application/json" --header "Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k"  -X GET http: //127.0.0.1:8000/api/hello/
{ "hello" : "world" }

首先,我们登录了新用户Ichiro,然后在受保护的端点上尝试了GET请求。 它仍然被拒绝,因为尽管我们可以用肉眼看到它,但实际上并未传递令牌。 它必须在标题中传递。 这就是为什么要记住在settings.py中我们在AUTH_HEADER_TYPES设置“JWT”AUTH_HEADER_TYPES

在标头中,令牌必须以“Authorization: JWT “ + access token 。 或将AUTH_HEADER_TYPES设置为的任何内容。 否则,没有骰子。 当我们连接前端时,这将变得很重要。

如您所见,当我们这样做时,我们能够获得视图的响应{"hello":"world"}

惊人。 现在,我们可以使用JWT Refresh和Access令牌对自定义用户进行身份验证,并且仅当在标头中传输Access令牌时才允许他们访问受保护的视图。

如果您等待了5分钟并尝试使用过期的访问令牌怎么办?

$ curl -Type: application/json " --header " Authorization: JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNTYwNDI5MTE2LCJqdGkiOiI1NWVlZDA4MGQ2YTg0MzI4YTZkZTE0Mjg4ZjE3OWE0YyIsInVzZXJfaWQiOjIsImZhdl9jb2xvciI6IiJ9.LXqfhFifGDA6Qg8s4Knl1grPusTLX1lh4YKWuQUuv-k "  -X GET http://127.0.0.1:8000/api/hello/
{" detail ":" Given token not valid for any token type "," code ":" token_not_valid "," messages ":[{" token_class ":" AccessToken "," token_type ":" access "," message ":" Token is invalid or expired "}]}

在Django服务器控制台中,您将能够看到魔术:

Unauthorized: /api/ hello/
[ 13 /Jun/ 2019 12 : 53 : 29 ] "GET /api/hello/ HTTP/1.1" 401 183

该视图将无法访问,您必须刷新。 手动使用CURL进行操作非常繁琐,但是在前端框架内,它很容易实现自动化。

接下来,我们将继续使用ReactJS创建前端以使用我们的API。

第1–2节的GitHub代码位于此处

第2部分:React Frontend

2–1)将React作为独立应用程序安装在Django项目中

设置前端框架以使用Django可以通过多种方法来完成。 它们可以完全分开,仅通过API进行联系。 在这种情况下,本地开发将运行Django开发服务器和单独的React开发服务器,而部署将涉及独立于后端部署的前端。 例如,当Django托管在EC2服务器上时,对AWS S3做出反应。 这将需要一些有趣的CORS配置。

另一方面,React可以在Django自己的模板系统中更深入地交织在一起,以便Django处理为模板提供服务,但您仍然可以使用React magic。 首先,它有点不利于拥有前端框架的目的。

本教程使用中间方法-将React安装在独立的Django应用程序中。 CORS头文件不需要太多工作,您将获得React框架的全部好处。 有一定的警告。 同一服务器将负责提供所有数据,这可能会减慢数据速度。 如果您想提供一些Django URL,例如运行状况检查或/ admin,URL路由也有些棘手。 初始设置也很麻烦。 我们将逐步解决。

首先要做的是制作一个新的Django应用来保存React。

$ cd djsr
$ python manage.py startapp frontend

并将其添加到settings.pyINSTALLED_APPS中。

在frontend中创建一个templates/frontend/index.html文件,它将作为React以及常规Django渲染索引视图的基础模板。 在此特定模板中,可以使用Django模板上下文处理器,如果要从settings.py控制React的行为,这将非常有用。 现在,让我们像标准Django基本模板一样准备它。

<!-- djsr/frontend/templates/frontend/index.html -->

<!DOCTYPE html>
 <html>
{% load static %}
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="{% static 'frontend/style.css' %}">
    <title>DRF + React = Winning the game</title>
</head>
<body>
    <div id="root" class="content">
        This will be the base template.
    </div>
</body>
</html>

并在使用时制作最小的style.css。

// djsr/fontend/static/frontend/style.css

#root{
    background-color:rebeccapurple;
}

我们以视图和更新的URL完成此操作。

# djsr/djsr/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path( 'admin/' , admin.site.urls),
    path( 'api/' , include( 'authentication.urls' )),
    path( '' , include( 'frontend.urls' ))
]

确保将这个include放在urlpatterns 。 这样,任何不匹配Django URL的内容都将由前端处理,这使我们可以使用React的路由器来管理前端视图,同时仍将其托管在同一服务器上。 因此很好地避免了CORS的疯狂。

# djsr/frontend/views.py

from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request, 'frontend/index.html' , context=None)

此视图呈现index.html ,它将作为所有React的基础模板。 它需要做的就是渲染模板。 如果需要,可以添加上下文。

最后,我们将此视图添加到前端URL。 我们必须添加两次,首先捕获空URL,例如https://lollipop.ai,然后捕获其他所有URL,例如https://lollipop.ai/lollisignup/

# djsr/frontend/urls.py

urlpatterns = [
    path( '' , index_view),  # for the empty url
    url(r '^.*/$' , index_view)  # for all other urls
]

最后,再次运行服务器,查看索引是否正确显示。

$ cd ../.. (so you 're in project root: django-jwt-react)
$ python djsr/manage.py runserver

导航到http://127.0.0.1/并成功看到此漂亮的几乎为空的页面。 而且您会在http://127.0.0.1:8000/asdjfklasdjfklasdfjklasdf/上看到相同的内容,从而向我们展示了我们并没有弄乱。 至少还没有。 凉。 现在我们可以开始使用React了。

请注意,正则表达式以/ $结尾。 默认情况下,Django在每个URL的末尾添加一个/,因此在React中进行路由时,每个路径的末尾都需要一个/。 这有助于强制执行该纪律。

现在我们可以开始在前端应用程序设置React了。 这不是一项简单的任务,我们不能仅仅依靠Create React App。

相反,我们改编了Jedai Saboteur精彩教程 ,创建了自己的工具链。

确保您位于应用程序的根文件夹(带有Pipfile)。 首先创建package.json文件。 此处问题的答案无关紧要,因此只需将其保留为默认值即可。

$ npm init

现在,在frontend Django应用程序中,创建一个src目录。 这将保留我们的React组件。static/frontend目录中,创建另一个名为public的目录,以保存已编译的React文件。

现在,Django应用程序目录应如下所示:

djsr
+-- authentication/
+-- djsr/
+-- frontend/
| +-- migrations/
| +-- src/
| +-- static /
| | +-- frontend/
| | | +-- public/
| | | +-- style.css
| +-- templates/
| | +-- frontend/
| | | +-- index.html
+--db.sqlite3
+--manage.py

打开index.html并将这有点尴尬的行添加到正文的底部。 我们仍然可以在此html文件中使用Django的模板魔术。

<script type= "text/javascript" src= "{% static 'frontend/public/main.js' %}" > </ script >

编译后,我们制作的React组件将包含在main.js 。 您可以将此文件重命名为所需的名称。

为了使它在生产中更好地工作,如果main.js太胖,则应将其拆分成束。

这条线是Django和React对我们而言的关键交叉点。 通过Django服务的index.html加载main.js,我们可以提供我们想要的任何出色的React向导。 我们确实想做很多这样的巫术。

在我们的index.html准备就绪并等待时,我们必须弄清楚如何创建和编译某些内容以使其能够呈现和呈现。 这需要Babel和Webpack。

巴别塔

安装@babel/core@babel/preset-env@babel-preset-react

$ npm install --save-dev @babel/core@ 7.4 .5 @babel/preset-env@ 7.4 .5 @babel/preset-react@ 7.0 .0

Babel使用我们用任何方式编写的代码,并将其变成JavaScript浏览器所钟爱。 babel/preset-env用于现代Javascript ES6 +,而babel/preset-react用于JSX。 如果您来自Angular世界,并且想使用TypeScript(为什么不呢?这很棒),请安装babel/preset-typescript 。 在本教程中,我们将远离TypeScript。

要使用全新安装的Babel,我们必须在项目根目录中package.json旁边创建一个.babelrc文件。

{
  "presets" : [ "@babel/preset-env" , "@babel/preset-react" ]
}

目前还没有任何测试方法。

Webpack

Webpack是捆绑器。 它采用我们的模块(依赖项和自定义代码)并将其转换为静态资产。 他们网站https://webpack.js.org上的此信息图很好地说明了其工作原理。

这就是我们各种React组件的功能,并且(在Babel使用JS(X)之后)会将它们全部转换为漂亮的静态文件,以供浏览器使用。

让我们安装它以及更多使我们可以将其与Babel结合使用的软件包。

$ npm install --save-dev webpack webpack-cli babel-loader
+ babel-loader@ 8.0 .6
+ webpack-cli@ 3.3 .4
+ webpack@ 4.35 .0

我们将让Django在index.html进行CSS加载,但是如果您愿意的话,Webpack还可以接管该加载功能。 它也可以提供开发服务器,但是Django也可以处理。 它只是不会检测到src文件的更改(公平地说,这可能会变得很乏味)。

让我们在根目录下, packages.json旁边创建Webpack配置文件webpack.config.js

const path = require ( 'path' );

module .exports = {
    mode : "development" ,
    entry : path.resolve(__dirname, 'djsr/frontend/src/index.js' ),
    output : {
        // options related to how webpack emits results

        // where compiled files go
        path: path.resolve(__dirname, "djsr/frontend/static/frontend/public/" ),

        // 127.0.0.1/static/frontend/public/ where files are served from
        publicPath: "/static/frontend/public/" ,
        filename : 'main.js' ,  // the same one we import in index.html
    },
    module : {
        // configuration regarding modules
        rules: [
            {
                // regex test for js and jsx files
                test: /\.(js|jsx)?$/ ,
                // don't look in the node_modules/ folder
                exclude: /node_modules/ ,
                // for matching files, use the babel-loader
                use: {
                    loader : "babel-loader" ,
                    options : { presets : [ "@babel/env" ]}
                },
            }
        ],
    },
};

查看导出的配置对象,我们发现一些重要的注意事项。

入口点是Webpack可以在其中找到我们React应用程序的开始并从那里捆绑的地方。

对于Webpack,我们必须使用绝对路径,而当在输入和输出设置中使用绝对路径时,路径模块将使其变得非常容易。

将模式设置为“开发”与“生产”,并进行了一些优化,无论哪种方式都可以。 现在坚持发展。

在输出中,有3个相关设置。 path是编译后发出文件的位置。 publicPath本质上是设置应用程序将在其中找到React静态文件的位置。 在开发中,只需将其设置为发出它们的位置即可。 在使用CDN的生产中,此处的模式为STATIC_PATH/{{path after emitting}} ,其中STATIC_PATH是Django项目在运行collectstatic之后保存静态文件的位置。 filename当然是Webpack编译完成后发出的文件的名称。

由于我们要让Django处理很多加载任务,因此我们仅在模块设置中使用一条规则。 对于输入后找到的所有.js.jsx文件,这些文件将由babel-loader加载和转换。 我们明确排除了/node_modules/文件夹的测试范围。 比将它提交给Git还要糟糕!

如果您来自Django开发背景,那么在进行任何React编码之前,您可能会对所有这些配置感到震惊。 是的,很多。 值得!

我们终于到了可以安装React的地步了。

反应

$ npm install --save react react-dom
+ react@ 16.8 .6
+ react-dom@ 16.8 .6

安装React之后,我们终于可以制作在webpack.config.js引用的index.js了。 这是我们React应用程序的开始,现在只是import。

// djsr/frontend/src/index.js

import React from 'react'
import {render} from 'react-dom'
import App from './components/App' ;

render( < App  /> , document.getElementById('root'));

看到getElementByID('root')部分? 这将使该应用程序代替我们在index.html中创建的#root div。 它曾经说过“这将是基本模板”,但是在这里(很快)完成之后,我们还会看到其他内容。

我们App.js让人们思考,因此请在其中创建一个components/目录及其App.js

// djsr/frontend/src/components/App.js

import React, { Component} from "react" ;

class App extends Component {
  render(){
    return (
      < div className = "site" >
         <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1> 
      </ div >
    );
  }
}

export default App;

这是我们的第一个React组件! 太棒了 注意React使用的是className而不是class。

由于我们不使用Webpack开发服务器,因此必须以其他方式手动编译。 让我们对此懒些,并使其成为脚本。 打开package.json并添加”build”: “webpack — config webpack.config.js”

// package.json

{
  "name" : "django-jwt-react" ,
  "version" : "1.0.0" ,
  "description" : "To learn how to merge React and Django" ,
  "main" : "index.js" ,
  "scripts" : {
    "build" : "webpack --config webpack.config.js" ,
    "test" : "test"
  },
  "author" : "" ,
  "license" : "ISC" ,
  "devDependencies" : {
    "@babel/core" : "^7.4.5" ,
    "@babel/preset-env" : "^7.4.5" ,
    "@babel/preset-react" : "^7.0.0" ,
    "babel-loader" : "^8.0.6" ,
    "webpack" : "^4.35.0" ,
    "webpack-cli" : "^3.3.4"
  },
  "dependencies" : {
    "react" : "^16.8.6" ,
    "react-dom" : "^16.8.6"
  }
}

最后。

$ npm run build

如果djsr/frontend/public/ ,请查看djsr/frontend/public/ 。 那里有main.js文件吗? 应该有。

如果运行Django,则应该看到新行。 让我们测试一下。

$  python djsr/manage.py runserver

这既不简单也不容易,但是确实有效。 Django现在正在为React应用提供服务,该应用愉快地存在于我们的前端应用中。 保持这种状态的方法。

第2–1节的GitHub代码位于此处

2–2)准备React

我们的最终目标是通过React和Django进行身份验证。 我们已经在Django端完成了所需的所有端点。 在将身份验证位写在前端之前,我们必须做一些工作。 即:路由组件,设置Axios并制作表单视图。

现在,我们将跳过很多关于React中客户端路由的复杂性。 我可以,如果有需求后面的文章后,它扩大,但也有一些非常好的资源, 在那里已经,虽然这可能是第一个触及阵营路由器5中的一个。

路由

安装react-router-dom 。 我们使用-dom变体,因为我们是在构建网站而不是应用程序。 对于好奇地制作React Native应用程序,您可以使用react-router-native。

$ npm install --save react-router-dom
+ react-router-dom@ 5.0 .1

对于react-router-dom 4和5,对于将通过我们自己的浏览器使用的项目,我们提供了2种浏览器。 BrowserRouterHashRouter 。 基于历史API,当React应用程序是单页应用程序(每次由服务器提供index.html )或由动态服务器支持以处理所有请求时, BrowserRouter是首选。 网址的格式不带www.lollipop.ai/bananaphone/ www.lollipop.ai/bananaphone/ 。 HashRouter网址的格式为/#/www.lollipop.ai/#/bananaphone/ 。 当服务器仅提供静态页面和静态资产时, HashRouter更常用。 多亏了Django,我们有了一个动态服务器,因此我们可以使用BrowserRouter

我们还需要将其添加到index.js ,像这样包装我们的App,因为render只希望接收1个组件。

// djsr/frontend/src/index.js

import React from 'react'
import {render} from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
import App from './components/App' ;

render((
    < BrowserRouter >
         <App  /> 
    </ BrowserRouter >
), document .getElementById( 'root' ));

导入BrowserRouter并更新index.js以使用它之后,下一步是创建另一个可根据URL呈现的几个组件。

创建两个新文件: components/ login.jssignup.js

// djsr/frontend/components/login.js

import React, { Component } from "react" ;

class Login extends Component {
    constructor (props){
        super (props);
    }

    render() {
        return (
            < div >
                 <h2>Login page</h2> 
            </ div >
        )
    }
}
export default Login;

和:

// djsr/frontend/components/signup.js

import React, { Component } from "react" ;

class Signup extends Component {
    constructor (props){
        super (props);
    }

    render() {
        return (
            < div >
                 <h2>Signup page</h2> 
            </ div >
        )
    }
}
export default Signup;

几乎可以编写的最简单的React组件。 它具有其构造函数,呈现一些HTML,然后导出自身。

准备好组件之后,让我们导入所需的SwitchRouteLink

// djsr/frontend/src/components/App.js

import React, { Component} from "react" ;
import { Switch, Route, Link } from "react-router-dom" ;
import Login from "./login" ;
import Signup from "./signup" ;

class App extends Component  {
    render() {
        return (
            < div className = "site" >
                 <main>
                    <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>
                    <Switch>
                        <Route exact path={"/login/"} component={Login}/>
                        <Route exact path={"/signup/"} component={Signup}/>
                        <Route path={"/"} render={() => <div>Home again</div>}/>
                   </Switch>
               </main>
            </div>
        );
    }
}

export default App;

使用<main> HTML元素是一个好习惯,因此我们将其添加为主要内容。 在其中,我们添加了一个<Switch>块,该块使React知道在该空间中,我们将根据定义的路线切换渲染的组件。

React Switches中的URL路由与Django的路径语法非常相似。 通过使用exact属性,当URL路径完全匹配时,将呈现相关组件。 所有其他路径都可以到达首页(或者更好的是,将来的404页)。

这样,我们应该能够通过在浏览器的地址栏中键入URL来测试URL。

导航至http://127.0.0.1:8000/signup然后查看其中的内容。

看起来正在运作。 天哪,这很丑。 您可以建立外观更好的网站,对吗?

每次我们要测试URL时都要键入URL,这很繁琐,因此让我们添加一些App.js链接以节省一些击键。

// djsr/frontend/src/components/App.js

import React, { Component} from "react" ;
import { Switch, Route, Link } from "react-router-dom" ;
import Login from "./login" ;
import Signup from "./signup" ;

class App extends Component  {
    render() {
        return (
            < div className = "site" >
                 <nav>
                    <Link className={"nav-link"} to={"/"}>Home</Link>
                    <Link className={"nav-link"} to={"/login/"}>Login</Link>
                    <Link className={"nav-link"} to={"/signup/"}>Signup</Link>
                </nav>
                <main>
                    <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>

                    <Switch>
                        <Route exact path={"/login/"} component={Login}/>
                        <Route exact path={"/signup/"} component={Signup}/>
                        <Route path={"/"} render={() => <div>Home again</div>}/>
                    </Switch>
                </main>
            </div>
        );
    }
}

export default App;

仍然保持良好的习惯,我们当然将其全部放在<nav>元素中。 在React中,请不要使用 <a href=””> ,而应使用React路由器的 <Link> 组件,为了使React路由器与历史记录API正确配合,我们必须使用它。 现在,为最小的“样式”添加类“nav-link”

/* djsr/frontend/static/frontend/style.css */

#root{
    background-color:rebeccapurple;
    color:white;
}

.nav-link{
    color :white;
    border: 1 px solid white;
    padding: 1 em;
}

是的,一切仍然令人难以置信的丑陋。 也许以后我应该做点什么。

首先,确认<Link>的作用。

$ npm run build
$ python djsr/manage.py runserver

再次看起来不错。 路由完成! 让我们充实那些形式。

形式

登录和注册需要两种形式。 注册具有用户名,电子邮件和密码字段,而登录仅需要用户名和密码。 我真的很喜欢React如何使表单对开发人员和用户如此友好。

我通过从React 自己的docs中删除该表单开始了该表单。 这几乎是我们需要启动的所有内容。

// djsr/frontend/src/components/login.js

import React, { Component } from "react" ;

class Login extends Component  {
    constructor (props) {
        super (props);
        this .state = { username : "" , password : "" };

        this .handleChange = this .handleChange.bind( this );
        this .handleSubmit = this .handleSubmit.bind( this );
    }

    handleChange(event) {
        this .setState({[event.target.name]: event.target.value});
    }

    handleSubmit(event) {
        alert( 'A username and password was submitted: ' + this .state.username + " " + this .state.password);
        event.preventDefault();
    }

    render() {
        return (
            < div > Login
                 <form onSubmit={this.handleSubmit}>
                    <label>
                        Username:
                        <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
                    </label>
                    <label>
                        Password:
                        <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
                    </label>
                    <input type="submit" value="Submit"/>
                </form>
            </div>
        )
    }
}
export default Login;

首先,查看表单的HTML。 提交表单后,它将触发onSubmit方法,该方法已被编写为handleSubmit 。 用户提交表单后,此方法将处理我们希望执行的操作。 在这种情况下,我们要它做的是提醒我们输入的数据。 通常,提交会触发重新加载或重定向,因此通过添加preventDefault()可以停止不需要的行为。

对于其他两个输入字段(用户名,密码),我们将onChange属性设置为名为handleChange的方法。 只要该输入字段的内容发生变化,当发生击键时,它就会触发handleChange方法执行我们想要的操作,该方法就是使用setState更新本地组件状态以匹配每个输入字段的输入文本值。

如果不小心,可能会为每个输入字段使用handleSomething方法。 但是,由于我们要坚持DRY原则,因此我们可以巧妙地给每个输入一个名称,并确保本地组件状态与该名称匹配。 这样,我们仅需使用一行代码即可摆脱一个handleChange方法。 不完全是Python的一线工具,但是它会刮擦它的痕迹。

它结合是超级重要this在构造函数中的每个类的方法,否则这将在回调是不确定的。 很烦人忘了。

更新后的注册页面基本相同,但具有额外的电子邮件输入字段(和匹配的组件状态)。

// djsr/frontend/src/components/signup.js
import React, { Component } from "react" ;

class Signup extends Component {
    constructor (props){
        super (props);
        this .state = {
            username : "" ,
            password : "" ,
            email : ""
        };

        this .handleChange = this .handleChange.bind( this );
        this .handleSubmit = this .handleSubmit.bind( this );
    }

    handleChange(event) {
        this .setState({[event.target.name]: event.target.value});
    }

    handleSubmit(event) {
        alert( 'A username and password was submitted: ' + this .state.username + " " + this .state.password + " " + this .state.email);
        event.preventDefault();
    }

    render() {
        return (
            < div >
                Signup
                 <form onSubmit={this.handleSubmit}>
                    <label>
                        Username:
                        <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
                    </label>
                    <label>
                        Email:
                        <input name="email" type="email" value={this.state.email} onChange={this.handleChange}/>
                    </label>
                    <label>
                        Password:
                        <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
                    </label>
                    <input type="submit" value="Submit"/>
                </form>
            </div>
        )
    }
}
export default Signup;

通过构建和运行Django服务器并进行刷新来对其进行测试。

$ npm run build
$ pythong djsr/manage.py runserver

如果您一直在努力,应该会有一个超级吸引人的警报,甚至可能来自浏览器的警告,询问您是否要阻止该网站显示警报。

感谢Firefox保护我免受此烦人的注册页面的侵害。

好的,所以我们的React前端现在可以处理路由了,我们可以使用注册和登录表单-有点。

下一部分是我最不喜欢的部分。 Axios。

第2–2节的GitHub代码位于此处

2–3)Axios用于请求和令牌

在Javascript圈内,有两种从REST API进行GET / POST / UPDATE / DELETE /任何数据的主要方法: Axios和JavaScript附带的Fetch 。 就个人而言,我在Axios上获得了更多的成功。 两者都使用Promises,并且Internet Explorer不喜欢它们中的任何一个(但是似乎不那么喜欢Fetchy)。 我想稍后在后续文章中探索GraphQL。

我们希望将Axios用于:

POST到/api/user/create/创建/api/user/create/ POST到/api/token/obtain/登录用户并获取JWT令牌对POST到/api/token/refresh/刷新JWT令牌对从受保护的/api/hello/看看后端偷偷说了什么

向上滚动并查看我们的CURL命令。 要访问受保护的视图,必须在标头中发送JWT令牌。 除了发送POSTed数据外,Axios还需要注意这一点。 每当JWT访问令牌过期时,我们都不想让用户手动获得一个新令牌,因为它每5分钟过期一次。 该用户将逃跑。 Axios也将不得不自动获取新令牌。

这是一项艰巨的任务。

第一步-安装Axios:

$ npm install --save axios
+ axios@ 0.19 .0

Axios在这里有两个有用的超级大国。 首先,我们可以创建一个具有自定义配置的独立实例 ,该实例可以在整个网站中使用-在这里,我们将其设置为发送JWT标头。 其次,我们可以为新的Axios实例创建自定义拦截器 。 拦截器使我们可以在处理请求时“处理” —这是我们将处理刷新令牌的地方。

src/创建一个名为axiosApi.js的文件。

// djsr/frontend/src/axiosApi.js


import axios from 'axios'

const axiosInstance = axios.create({
    baseURL : 'http://127.0.0.1:8000/api/' ,
    timeout : 5000 ,
    headers : {
        'Authorization' : "JWT " + localStorage.getItem( 'access_token' ),
        'Content-Type' : 'application/json' ,
        'accept' : 'application/json'
    }
});

要创建Axios实例,请导入它,然后将.create()方法与自定义配置一起使用以设置默认值。 将baseURL设置为baseURL后端API的位置。 标头很重要。 在settings.pySIMPLE_JWT字典设置AUTH_HEADER_TYPES'JWT'这样的Authorization头在这里它必须是相同的。 不要忽略在axiosAPI.js中的JWT之后添加空格。 也不要在settings.py中留空格。

相同,但不同,但仍相同。

每次Axios获得令牌时,它将access_token存储在本地存储中。 我们通过获取令牌来启动Axios实例的创建。 如果本地存储中没有令牌,则甚至不必担心标题。 每次用户登录时都会设置。

要测试任何东西,我们仍然必须编写登录和注册方法。 然后我们仍然需要解决注销问题。 不能忘记那个!

在登录

在原始的login.js文件中,我们现在可以改进handleSubmit to POST到Django后端的令牌创建端点/api/token/obtain/并获得令牌对。

// djsr/frontend/src/components/login.js

import React, { Component } from "react" ;
import axiosInstance from "../axiosApi" ;

class Login extends Component  {
    constructor (props) {
        super (props);
        this .state = { username : "" , password : "" };

        this .handleChange = this .handleChange.bind( this );
        this .handleSubmit = this .handleSubmit.bind( this );
    }

    handleChange(event) {
        this .setState({[event.target.name]: event.target.value});
    }

    handleSubmit(event) {
        event.preventDefault();
        try {
            const response = axiosInstance.post( '/token/obtain/' , {
                username : this .state.username,
                password : this .state.password
            });
            axiosInstance.defaults.headers[ 'Authorization' ] = "JWT " + response.data.access;
            localStorage.setItem( 'access_token' , response.data.access);
            localStorage.setItem( 'refresh_token' , response.data.refresh);
            return data;
        } catch (error) {
            throw error;
        }
    }

    render() {
        return (
            < div >
                Login
                 <form onSubmit={this.handleSubmit}>
                    <label>
                        Username:
                        <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
                    </label>
                    <label>
                        Password:
                        <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
                    </label>
                    <input type="submit" value="Submit"/>
                </form>
            </div>
        )
    }
}
export default Login;

非常简单的升级。 我们先前的视图版本已经将用户名和密码设置为状态,因此现在剩下要做的就是导入自定义Axios实例,并发布用户名和密码。 如果可行,响应将包含一对全新的JWT令牌。 由于对视图的访问由访问令牌决定,因此每个标题都需要使用该令牌。 但是,我们要将它们都保存到本地存储中。 为什么? 我们将解决这个问题。

运行标准例程,然后导航到/login/端点并尝试登录:

$ npm run build
$ python djsr/manage.py runserver

Django表示一切正常。

Firefox也可以。

哦,嘿,看看令牌对!

测试受保护的视图

手中有令牌对。 我们可以看到受保护的视图了吗?

不。 实际上,React中还没有任何东西试图获取该信息。 是时候使用另一个React组件了。

在components文件夹中hello.js一个名为hello.js的文件。 在此组件中,我们只想做一件事:从后端受保护的API端点获取一条消息,然后显示它。 我们将再次使用自定义Axios实例。

有几个棘手的部分。

// djsr/frontend/src/components/hello.js

import React, { Component } from "react" ;
import axiosInstance from "../axiosApi" ;

class Hello extends Component  {
    constructor (props) {
        super (props);
        this .state = {
            message : "" ,
        };

        this .getMessage = this .getMessage.bind( this )
    }

    getMessage(){
        try {
            let response = axiosInstance.get( '/hello/' );
            const message = response.data.hello;
            this .setState({
                message : message,
            });
            return message;
        } catch (error){
            console .log( "Error: " , JSON .stringify(error, null , 4 ));
            throw error;
        }
    }

    componentDidMount(){
        // It's not the most straightforward thing to run an async method in componentDidMount

        // Version 1 - no async: Console.log will output something undefined.
        const messageData1 = this .getMessage();
        console .log( "messageData1: " , JSON .stringify(messageData1, null , 4 ));
    }

    render(){
        return (
            < div >
                 <p>{this.state.message}</p> 
            </ div >
        )
    }
}

export default Hello;

到目前为止,您应该已经熟悉了constructor()方法的内容。 我们将消息设置为state ,然后在render ,我们渲染任何消息。

无需过多介绍React生命周期,它很直观,仅表示在安装/加载componentDidMount ,将触发componentDidMount方法。 由于我们想马上获得此消息,因此这里是触发我们的GET请求的正确位置。

只是,首先,必须将此组件添加到Navbar并在App.js切换。 没什么大不了的。

// djsr/frontend/src/components/App.js

import React, { Component} from "react" ;
import { Switch, Route, Link } from "react-router-dom" ;
import Login from "./login" ;
import Signup from "./signup" ;
import Hello from "./hello" ;

class App extends Component  {
    render() {
        return (
            < div className = "site" >
                 <nav>
                    <Link className={"nav-link"} to={"/"}>Home</Link>
                    <Link className={"nav-link"} to={"/login/"}>Login</Link>
                    <Link className={"nav-link"} to={"/signup/"}>Signup</Link>
                    <Link className={"nav-link"} to={"/hello/"}>Hello</Link>
                </nav>
                <main>
                    <h1>Ahhh after 10,000 years I'm free. Time to conquer the Earth!</h1>

                    <Switch>
                        <Route exact path={"/login/"} component={Login}/>
                        <Route exact path={"/signup/"} component={Signup}/>
                        <Route exact path={"/hello/"} component={Hello}/>
                        <Route path={"/"} render={() => <div>Home again</div>}/>
                    </Switch>
                </main>
            </div>
        );
    }
}

export default App;

生成并重新编译服务器。 导航到/hello/然后查看会发生什么。

没用!

1号-访问令牌已过期,因此出现401错误。

2号-React尝试分配message = response.data.hello而不等待响应。

其中任何一个都会删除该站点。 您可以通过再次登录来确认#2将使用新的访问令牌使它失效。 或者,实际上不是。

这不是我们在等待响应之前第一次尝试分配值 。 当时我们无法看到它,因为它没有彻底破坏任何东西,但是在login.js早期,尽管我们进行了发布并获得了令牌并可以在控制台中看到它,但实际上并没有将其设置为localStorage或axiosInstance的标头。

Change getMessage() to see how the access_token in localStorage is logged as undefined.

// djsr/frontend/src/components/hello.js

...
getMessage(){
    try {
        const header = localStorage.getItem( "access_token" );
        console .log(header);
        // let response = axiosInstance.get('/hello/');
        // const message = response.data.hello;
        // this.setState({
        //     message: message,
        // });
        // return message;
    } catch (error){
        console .log( "Error: " , JSON .stringify(error, null , 4 ));
        throw error;
    }
}
...

With no token set in the headers, you can't test #1 either.

How do we fix that?

Async/Await & .then

We need to tell React to wait for a response before continuing along through a method, assigning values that haven't been returned in a response yet.

You can add a .then() callback function using promises, which is the other way to do it. 它看起来像这样:

// djsr/frontend/src/components/login.js

class Login extends Component  {
    constructor (props) {
...
        this.handleSubmitWThen = this .handleSubmitWThen.bind( this );
    }
...
handleSubmitWThen(event){
    event.preventDefault();
    axiosInstance.post( '/token/obtain/' , {
            username : this .state.username,
            password : this .state.password
        }).then(
            result => {
                axiosInstance.defaults.headers[ 'Authorization' ] = "JWT " + result.data.access;
                localStorage.setItem( 'access_token' , result.data.access);
                localStorage.setItem( 'refresh_token' , result.data.refresh);
            }
    ).catch ( error => {
        throw error;
    })
}
render() {
    return (
        < div >
            Login
             <form onSubmit={this.handleSubmitWThen}>
...
            </form> 
        </ div >
        )
    }
}

This code works, so you can totally ignore the following ES7 async/await version if you so choose. You could also use try to use React hooks, but they seem to still be not entirely ready as of June 2019.

Sidenote: .then() , .catch() , and .finally() do work together pretty well. I just prefer putting everything is standard try/catch/finally blocks.

Let's use the newer ES7 async/await with try/catch/finally blocks instead to stay on top of things. They work great, normally. However, as always, there's another caveat.

We have to configure webpack.

But first, the async handleSubmit .

// djsr/frontend/src/components/login.js

...
async handleSubmit(event) {
    event.preventDefault();
    try {
        const data = await axiosInstance.post( '/token/obtain/' , {
            username : this .state.username,
            password : this .state.password
        });
        axiosInstance.defaults.headers[ 'Authorization' ] = "JWT " + data.access;
        localStorage.setItem( 'access_token' , data.access);
        localStorage.setItem( 'refresh_token' , data.refresh);
        return data;
    } catch (error) {
        throw error;
    }
}
...
render() {
    return (
        < div >
            Login
             <form onSubmit={this.handleSubmit}>
...
            </form> 
        </ div >
        )
    }
}

Just need to add async at the beginning when declaring the method, and at the part you want the code to wait, add an await . 就这么简单。 The rest of the code can be written like it's synchronous, and that includes errors. 很干净。 That cleanliness makes it my personal preference. Don't neglect to change the onSubmit back to handleSubmit .

Babel again

Since we're using a custom Webpack configuration, using async/await won't work without some additional elbow grease. Right now, the console will show us a big red ReferenceError: regeneratorRuntime is not defined error and React will break. Googling around will lead you to various StackOverflow Q&As leading to the answer. Just add babel-polyfill to the entry line of webpack.config.js or import it. We had better install that package.

$ npm install --save-dev babel-polyfill
+ babel-polyfill@ 6.26 .0

The cleanest way is to just add that as an entry point. 因此,让我们这样做。

// webpack.config.js

const path = require ( 'path' );

module .exports = {
    mode : "development" ,
    entry : [ 'babel-polyfill' , path.resolve(__dirname, 'djsr/frontend/src/index.js' )],
...
}

Yes, you can have multiple entries for entry. Just make sure the final entry in the list is for the actual index.js file.

好。 So now we should be able to use Async/await in our components.

Build it and run the server.

$ npm run build
$ python djsr/manage.py runserver

Login again and navigate to /hello/ and check the console — we should see that it now runs succesfully with the async/await method working on login. 凉。

GETting from a Protected API endpoint

With async working with Login.js , storing the token properly and including it in the headers, it can now be used as authentication for accessing protected API endpoints in Django.

最后。 All this work to get right here.

hello.js is nearly ready. Set free the commented-out lines and add async + await.

// djsr/frontend/src/components/hello.js

...

async getMessage(){
    try {
        let response = await axiosInstance.get( '/hello/' );
        const message = response.data.hello;
        this .setState({
            message : message,
        });
        return message;
    } catch (error){
        console .log( "Error: " , JSON .stringify(error, null , 4 ));
        throw error;
    }
}

...

Test it out.

Unless you test /hello/ within 5 minutes of logging in, you'll get a 401 Unauthorized rejection. You can verify it in the Django console or the browser console.

Unauthorized: /api/ hello/
"GET /api/hello/ HTTP/1.1" 401 183

It's a bit vague, but this 401 is due to the access token we're sending along with the request exceeding its short 5 minute lifespan. DRF rejects the request with a 401 error.

It's kind of like forgetting the water is boiling for spaghetti, and you forget, and then the water has completely evaporated and you have to start all over again.

Login one more time, get a fresh token, and you'll see the message “world” load.

Should a user have to login every 5 minutes? 没门。 让我们修复它。

Axios interceptor

With instance and refresh token in hand, let's add the interceptor. It isn't as robust as I would like, so if anybody can suggest improvements, please drop them in the comments.

// djsr/frontend/src/axiosApi.js

import axios from 'axios'
const axiosInstance = axios.create({
    baseURL : 'http://127.0.0.1:8000/api/' ,
    timeout : 5000 ,
    headers : {
        'Authorization' : "JWT " + localStorage.getItem( 'access_token' ),
        'Content-Type' : 'application/json' ,
        'accept' : 'application/json'
    }
});
axiosInstance.interceptors.response.use(
    response => response,
    error => {
      const originalRequest = error.config;
      
      if (error.response.status === 401 && error.response.statusText === "Unauthorized" ) {
          const refresh_token = localStorage.getItem( 'refresh_token' );

          return axiosInstance
              .post( '/token/refresh/' , { refresh : refresh_token})
              .then( ( response ) => {

                  localStorage.setItem( 'access_token' , response.data.access);
                  localStorage.setItem( 'refresh_token' , response.data.refresh);

                  axiosInstance.defaults.headers[ 'Authorization' ] = "JWT " + response.data.access;
                  originalRequest.headers[ 'Authorization' ] = "JWT " + response.data.access;

                  return axiosInstance(originalRequest);
              })
              .catch( err => {
                  console .log(err)
              });
      }
      return Promise .reject(error);
  }
);
export default axiosInstance

Every single time a request is made with the Axios instance we created earlier, if there are no errors, it just works as expected.

When there are errors, first we get the configuration from the errant request to use later. Then, we must see what kind of error code is being sent along with it. Django Rest Framework sends a 401 Unauthorized error status when a user isn't authorized to view a protected view.

If the error is a 401, we need to refresh it, which means we need to have the refresh token handy to get a new token pair. POST the token with the custom AxiosInstance and barring any surprises, we'll have a pair of refreshed tokens to save to localStorage.

New tokens in hand, send on the original request, and it should work. 理论上。

Build it and refresh /hello/ to see. Take a look at what happens in the Django dev server console:

Unauthorized: /api/ hello/
"GET /api/hello/ HTTP/1.1" 401 183
"POST /api/token/refresh/ HTTP/1.1" 200 491
"GET /api/hello/ HTTP/1.1" 200 17

Note that sometimes I have had problems using the interceptor method when the request itself has errors, and it results in a bit of a loop.

你有它。 Users can login and access protected data with their access token getting refreshed each time it expires. 不容易。

Signup

Let's flesh out Signup with a functional handleSubmit() method to actually talk to the backend. It's very similar to the Login component.

// djsr/frontend/src/components/signup.js

...

async handleSubmit(event) {
    event.preventDefault();
    try {
        const response = await axiosInstance.post( '/user/create/' , {
            username : this .state.username,
            email : this .state.email,
            password : this .state.password
        });
        return response;
    } catch (error) {
         console .log(error.stack);
    }
}

...

This seems to work. Now, try to sign up again in a fresh browser or clear your cookies. Private window may even work. Pay attention to the dev server console.

Unauthorized: /api/u ser/create/
"POST /api/user/create/ HTTP/1.1" 401 183
Bad Request: /api/ token/refresh/
"POST /api/token/refresh/ HTTP/1.1" 400 43

But wait, we set permission_classes to AllowAny for the CustomUserCreate view. Shouldn't it work? It worked fine for CURL.

# djsr/authentication/views.py

class CustomUserCreate ( APIView ):
    permission_classes  = (permissions.AllowAny,)

    def post(self, request, format= 'json' ):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            if user:
                json = serializer.data
                return Response(json, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Turns out, this CustomUserCreate view was missing something.

# djsr/authentication/views.py

class CustomUserCreate ( APIView ):
    permission_classes  = (permissions.AllowAny,)
    authentication_classes = ()

    def post(self, request, format= 'json' ):
        serializer = CustomUserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            if user:
                json = serializer.data
                return Response(json, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

We need to specify an empty list or tuple for authentication_classes in addition to setting permission_classes to convince DRF to open up a view to the public. 再试一次。 No more 401 Unauthorized error .

Errors

Speaking of errors, how can we handle errors from DRF?

At a minimum we should present the field errors for the Signup component. The quickest way to do that is to combine setState with some ternary operators within the rendered HTML.

// djsr/frontend/src/components/signup.js

...

async handleSubmit(event) {
    event.preventDefault();
    try {
        const response = await axiosInstance.post( '/user/create/' , {
            username : this .state.username,
            email : this .state.email,
            password : this .state.password
        });
        return response;
    } catch (error) {
        console .log(error.stack);
        this .setState({
            errors :error.response.data
        });
    }
}

render() {
    return (
        < div >
            Signup
             <form onSubmit={this.handleSubmit}>
                <label>
                    Username:
                    <input name="username" type="text" value={this.state.username} onChange={this.handleChange}/>
                    { this.state.errors.username ? this.state.errors.username : null}
                </label>
                <label>
                    Email:
                    <input name="email" type="email" value={this.state.email} onChange={this.handleChange}/>
                    { this.state.errors.email ? this.state.errors.email : null}
                </label>
                <label>
                    Password:
                    <input name="password" type="password" value={this.state.password} onChange={this.handleChange}/>
                    { this.state.errors.password ? this.state.errors.password : null}
                </label>
                <input type="submit" value="Submit"/>
            </form>
        </div>
    )
}

...

For a properly made DRF API View, when encountering errors, it will return those errors in JSON form in the response. We log the error and set the state directly to the JSON object containing the error messages.

Every time the state is set, it triggers a re-render of the component — in this case that would render the error messages.

{ this .state.errors.password ? this .state.errors.password : null }

Ternary operators like this are done in this format:

{ Boolean ? (content to show if True) : (content to show if False)

So if a password doesn't pass the password validator on the backed Serializer, it would send an error which we store in the state at state.errors.password and if that is present, it shows the error text. Otherwise it shows nothing.

Question for the readers: Is there a different way to build forms with error handling based on some kind of formset for Django and React? Right now it's quite a manual approach, isn't it?

Now that we can login, signup, get and refresh tokens, handle basic errors and view content with proper authorization. It's time for the final step: Logging Out.

GitHub code for section 2–3 lives here .

2–4) Logging out and blacklisting tokens

According to the JWT documentation, there isn't really a way to log out in the conventional sense. The tokens cannot be forced to expire before their time, and if you delete the token in a users's localStorage , but they somehow come up with a token that hasn't yet expired(even that same one), they are able once again to access the website as a logged-in user.

There are a few ways to tackle logging out.

Just delete the tokens in localStorage1 + blacklist that token1 + blacklist all tokens for the user

Deleting the tokens in localStorage alone means every token is still valid to use on the site.

The way we have set the project up for this tutorial, each time a user needs to use their refresh token to get a fresh access token, they are issued a fresh PAIR of tokens. A new refresh token with a new validity included, and the old pair is . The number of tokens per user will grow very quickly, so blacklisting whatever token is currently in use doesn't do much more than merely deleting the token... Unless of course you blacklist each old token after rotation, which you should .

Finally, blacklisting every single token for a user would force them to login again on every single device, not just the device they're currently logged into.

该怎么办?

Let's tackle option 2 as an exercise. First, take a look at settings.py. Ah, it looks like we are NOT blacklisting after rotation. Better change that by adding the blacklist app from django-rest-framework-simplejwt and setting BLACKLIST_AFTER_ROTATION to True.

# djsr/djsr/settings.py

INSTALLED_APPS = (
    ...
    'rest_framework_simplejwt.token_blacklist' ,
    ...
}
...
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME' : timedelta(minutes= 5 ),
    'REFRESH_TOKEN_LIFETIME' : timedelta(days= 14 ),
    'ROTATE_REFRESH_TOKENS' : True,
    'BLACKLIST_AFTER_ROTATION' : True,
    'ALGORITHM' : 'HS256' ,
    'SIGNING_KEY' : SECRET_KEY,
    'VERIFYING_KEY' : None,
    'AUTH_HEADER_TYPES' : ( 'JWT' ,),
    'USER_ID_FIELD' : 'id' ,
    'USER_ID_CLAIM' : 'user_id' ,
    'AUTH_TOKEN_CLASSES' : ( 'rest_framework_simplejwt.tokens.AccessToken' ,),
    'TOKEN_TYPE_CLAIM' : 'token_type' ,
}

The blacklist relies on saving blacklisted tokens in the database, so you know you have a migration to run.

$  python djsr/manage.py migrate

Now each time a token pair gets refreshed, the old ones get blacklisted.

First, an API view for blacking out tokens:

# djsr/authentication/views.py

...

from rest_framework_simplejwt.tokens import RefreshToken

...

class LogoutAndBlacklistRefreshTokenForUserView(APIView):
    permission_classes = (permissions.AllowAny,)
    authentication_classes = ()

    def post(self, request):
        try :
            refresh_token = request.data[ "refresh_token" ]
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response(status=status.HTTP_205_RESET_CONTENT)
        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST)

The view accepts a POSTed refresh_token , uses that to create a RefreshToken object for access to the blacklist class method, and blacklists it.

Add it to urls.py.

# djsr/authentication/urls.py

from .views import ObtainTokenPairWithColorView, CustomUserCreate, HelloWorldView, LogoutAndBlacklistRefreshTokenForUserView
urlpatterns = [
    ...
    path( 'blacklist/' , LogoutAndBlacklistRefreshTokenForUserView.as_view(), name= 'blacklist' )
]

Next we need to add a button to our navbar to delete the localStorage tokens and to post the token to a blackout API view, which we will make shortly. This will go in App.js for now, but making a dedicated Nav component at this point also makes sense.

// djsr/frontend/src/components/App.js

...

import axiosInstance from "../axiosApi" ;


class App extends Component  {

    constructor () {
        super ();
        this .handleLogout = this .handleLogout.bind( this );
    }

    async handleLogout() {
    try {
        const response = await axiosInstance.post( '/blacklist/' , {
            "refresh_token" : localStorage.getItem( "refresh_token" )
        });
        localStorage.removeItem( 'access_token' );
        localStorage.removeItem( 'refresh_token' );
        axiosInstance.defaults.headers[ 'Authorization' ] = null ;
        return response;
    }
    catch (e) {
        console .log(e);
    }
};

    render() {
        return (
            < div className = "site" >
                 <nav>
                    <Link className={"nav-link"} to={"/"}>Home</Link>
                    <Link className={"nav-link"} to={"/login/"}>Login</Link>
                    <Link className={"nav-link"} to={"/signup/"}>Signup</Link>
                    <Link className={"nav-link"} to={"/hello/"}>Hello</Link>
                    <button onClick={this.handleLogout}>Logout</button>
                </nav> 
         
       
...

The button triggers handleLogout() when clicked. Handle logout posts the refresh token to the blackout API View to black it out, and then deletes access and refresh tokens from localStorage , while resetting the Authorization header for the axios instance. Need to do both, otherwise Axios will still be able to get authorized access to protected view.

Build it, run it, and test it by viewing /hello/ before logging in, while logged in, and after logging out. Clicking the logout button doesn't trigger any kind of global refresh for the site, and clicking the link to the /hello/ page also doesn't refresh the component if you're already there, so you'll may have to manually refresh to see the message disappear.

Code for this section is on GitHub here .

3) Bugfixes

If you've been testing what we've written so far, you'll have noticed that certain conditions, such as using the wrong password, can cause throw the Axios interceptor into an infinite loop of trying to get a new refresh token but never getting it. Bad juju .

Mosta in the comments down below generously provided a fix that solves this problem and adds a check the token's expiration time before making a request to begin with. 谢谢!

To improve a liiiitle bit further upon that, we can also reject any 401 errors thrown by the token refresh endpoint itself while redirecting the user to the login page.

Incorporating both changes into the interceptor, here is the final djsr/frontend/frontend/src/axiosApi.js : (note the change to the baseURL in the axiosInstance)

// djsr/frontend/src/axiosApi.js

import axios from 'axios'

const baseURL = 'http://127.0.0.1:8000/api/'

const axiosInstance = axios.create({
    baseURL : baseURL,
    timeout : 5000 ,
    headers : {
        'Authorization' : localStorage.getItem( 'access_token' ) ? "JWT " + localStorage.getItem( 'access_token' ) : null ,
        'Content-Type' : 'application/json' ,
        'accept' : 'application/json'
    }
});


axiosInstance.interceptors.response.use(
    response => response,
    error => {
        const originalRequest = error.config;

        // Prevent infinite loops
        if (error.response.status === 401 && originalRequest.url === baseURL+ 'token/refresh/' ) {
            window .location.href = '/login/' ;
            return Promise .reject(error);
        }

        if (error.response.data.code === "token_not_valid" &&
            error.response.status === 401 && 
            error.response.statusText === "Unauthorized" ) 
            {
                const refreshToken = localStorage.getItem( 'refresh_token' );

                if (refreshToken){
                    const tokenParts = JSON .parse(atob(refreshToken.split( '.' )[ 1 ]));

                    // exp date in token is expressed in seconds, while now() returns milliseconds:
                    const now = Math .ceil( Date .now() / 1000 );
                    console .log(tokenParts.exp);

                    if (tokenParts.exp > now) {
                        return axiosInstance
                        .post( '/token/refresh/' , { refresh : refreshToken})
                        .then( ( response ) => {
            
                            localStorage.setItem( 'access_token' , response.data.access);
                            localStorage.setItem( 'refresh_token' , response.data.refresh);
            
                            axiosInstance.defaults.headers[ 'Authorization' ] = "JWT " + response.data.access;
                            originalRequest.headers[ 'Authorization' ] = "JWT " + response.data.access;
            
                            return axiosInstance(originalRequest);
                        })
                        .catch( err => {
                            console .log(err)
                        });
                    } else {
                        console .log( "Refresh token is expired" , tokenParts.exp, now);
                        window .location.href = '/login/' ;
                    }
                } else {
                    console .log( "Refresh token not available." )
                    window .location.href = '/login/' ;
                }
        }
      
     
      // specific error handling done elsewhere
      return Promise .reject(error);
  }
);

export default axiosInstance

而已!

Final code on GitHub here

结论

Thanks for coding along with me! Follow me on Hackernoon @Toruitas or on Twitter: @Stuart_Leitch as I work on stuff like this, machine learning, philosophy in the digital age, and Creative Coding at my startup Lollipop.ai and at University of the Art London's Creative Computing Institute .

If you've made it this far, you've got a React frontend hosted by and interacting with a nice Django Rest Framework based API backend using JWT for authorization. It's got essentials like Client-side routing, automatic token refreshing, and field errors. Plus nice-to-haves like token blacklisting and ES7 async/await syntax.

Well done.

If you want to use this as a starting point, do it! The GitHub repo is here

For potential followup articles, we could expand this by refactoring and including Redux for state management — it's what I actually use regularly but would be too much to fit into a single article. I also want to explore how to adapt Django and React to using GraphQL.

From: https://hackernoon.com/110percent-complete-jwt-authentication-with-django-and-react-2020-iejq34ta

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值