Авторизация в Django (DRF) и React по JWT-токену

8d8a9214c6d4f50575321a17025dc9bd

Когда-то давно я уже делал авторизацию в Django и думал, что знаю о ней всё, но то была ошибка и оказалось, что я вообще ничего не знал и пользовался готовыми инструментами Django из коробки.

Когда я начал писать авторизацию для своего сайта, я столкнулся с тем, что в интернете есть информация и по JWT токену и по самой реализации авторизации, однако все реализации, найденные мною были нагромождены ненужным кодом, который мало относится к основной идее, либо одна из частей реализации будь то BackEnd или FrontEnd были плохо раскрыты. Поэтому я решил написать эту статью

В двух словах о самом JWT

Полное устройство и принцип JWT токена, вы можете прочитать здесь. Если в двух словах, JWT токен состоит из трёх частей:

  • Header — здесь находятся метаданные о токене, по какому алгоритму он вычисляется и тип токена. Эта часть нас мало интересует

  • Payload — полезная информация, допустим id юзера, username или другие данные, которые могут вам понадобиться. Важно заметить, что в этой части токена не должны храниться чувствительные данные и не стоит перегружать её данными, от этого может упасть скорость расшифровки

  • Signature — эта часть помогает определить серверу был ли изменён токен. Здесь берутся две другие части токена, соединяются и шифруются с помощью секретного ключа, который хранится лишь на сервер. Если на сервер придёт изменённый токен, сервер это определит и забракует этот запрос

Принцип работы

Важно понять основной принцип работы, без лишней информации. А принцип работы состоит в том, что при регистрации или аутентификации пользователя на сервере формируется токен и отправляется на клиент. Клиент прячет его в localStorage/куки/ хранилище сессии — по этому вопросу ведутся споры, что выбрать — решайте сами. И в дальнейшем при запросе к ресурсам которые требует авторизации пользователя, клиент должен отправлять этот токен, хранящийся в localStorage, серверу, где сервер определит подлинность токена и решит давать положительный ответ или нет.

Аутентификация — проверка подлинность пользователя по введённым данным (логин, пароль и т.д)

Авторизация — проверка прав пользователя (обычный пользователь не может зайти в админ-панель)

Пара слов о Refresh токене

Вы наверное замечали как спустя некоторое время, сервис в котором вы были авторизованы снова запрашивает ваши логин и пароль. Дело в том, что у JWT-токена есть время жизни, по истечении которого, токен становится недействительным и сервер перестанет отвечать вам взаимностью. Это связано с тем, что ваш токен могут украсть и длительное время пользоваться им. Однако частые запросы ваших авторизационных данных может раздражать и ухудшать UX. Поэтому стали пользовать Refresh Token.

Теперь у вас есть два токена: Access Token и Refresh. Access используется для доступа, а Refresh восстанавливает Access, когда тот истекает. Срок годности Refresh заведомо больше чем у Access и вот, когда кончается срок годности Refresh в пору предложить вам заново ввести ваши данные. Также Refresh желательно прятать куда-нибудь поглубже, а не просто в localStorage

Реализация на стороне Django (DRF)

Установка

pip install djangorestframework-simplejwt

Файл settings.py

INSTALLED_APPS = [

...

'rest_framework_simplejwt',

'rest_framework_simplejwt.token_blacklist',

...

]

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': [

        'rest_framework_simplejwt.authentication.JWTAuthentication',

    ],

}

Здесь вы указываете время жизни и возможность обновления Refresh токенов и добавление их в чёрный список после этого

Когда Access токен истекает, Refresh-токен его реанимирует и сам умирает.

SIMPLE_JWT = {

    'ROTATE_REFRESH_TOKENS': True,

    'BLACKLIST_AFTER_ROTATION': True,

    'ACCESS_TOKEN_LIFETIME': timedelta(days=30),

    'REFRESH_TOKEN_LIFETIME': timedelta(days=60),

}

После этой настройки необходимо провести миграции, чтобы создалась модель для хранения Refresh токенов, находящихся в чёрном списке

Далее будет рассмотрен файл views.py

Регистрация. При регистрации пользователь проходит валидацию, создаётся и также создаются токены для него и и отправляются на клиент, где они добавляются в localstorage

from rest_framework_simplejwt.tokens import RefreshToken
class RegistrationAPIView(APIView):

    def post(self, request):

        serializer = CustomUserSerializer(data=request.data)

        if serializer.is_valid():

            user = serializer.save()

            refresh = RefreshToken.for_user(user) # Создание Refesh и Access

            refresh.payload.update({    # Полезная информация в самом токене

                'user_id': user.id,

                'username': user.username

            })

            return Response({

                'refresh': str(refresh),

                'access': str(refresh.access_token), # Отправка на клиент

            }, status=status.HTTP_201_CREATED)

Аутентификация. При аутентификации, проходит валидация данных и создаются токены. Отличий от регистрации, в плане токенов — нет. Они также создаются.

class LoginAPIView(APIView):

    def post(self, request):

        data = request.data

        username = data.get('username', None)

        password = data.get('password', None)

        if username is None or password is None:

            return Response({'error': 'Нужен и логин, и пароль'},

                            status=status.HTTP_400_BAD_REQUEST)

        user = authenticate(username=username, password=password)

        if user is None:

            return Response({'error': 'Неверные данные'},

                            status=status.HTTP_401_UNAUTHORIZED)

        refresh = RefreshToken.for_user(user)

        refresh.payload.update({

            'user_id': user.id,

            'username': user.username

        })

        return Response({

            'refresh': str(refresh),

            'access': str(refresh.access_token),

        }, status=status.HTTP_200_OK)

Выход. На клиенте в это время вы удаляете всю информацию о токенах из localStorage или куда вы их добавляли.

class LogoutAPIView(APIView):

    def post(self, request):

        refresh_token = request.data.get('refresh_token') # С клиента нужно отправить refresh token

        if not refresh_token:

            return Response({'error': 'Необходим Refresh token'},

                            status=status.HTTP_400_BAD_REQUEST)

        try:

            token = RefreshToken(refresh_token)

            token.blacklist() # Добавить его в чёрный список

        except Exception as e:

            return Response({'error': 'Неверный Refresh token'},

                            status=status.HTTP_400_BAD_REQUEST)

        return Response({'success': 'Выход успешен'}, status=status.HTTP_200_OK)

Что делать, когда Access истёк?

В urls.py

from rest_framework_simplejwt.views import TokenRefreshView

 path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),

Т.е на клиенте вы делаете запрос к защищённому ресурсу и сервер отвечает вам, что ваш токен истёк или неверен, вы немедленно делаете запрос на этот url и восстанавливаете ваш Access.

Как Django понимает авторизован ли пользователь или нет?

Предположим есть страница пользователя, его личный кабинет. Туда может попасть только авторизованный пользователь. Тогда мы на клиенте формируем запрос в заголовок которого помещаем наш Access токен, а в Django делаем так:

from rest_framework.permissions import IsAuthenticated

class Profile(viewsets.ModelViewSet):

 permission_classes = [IsAuthenticated]

Теперь эта view будет из «коробки» с помощью [IsAuthenticated] будет проверять заголовок запроса и если там неверный или истёкший токен, она вернёт ошибку 401.

Для функциональных view, можно делать так с помощью декоратора:

@permission_classes([IsAuthenticated])

def profile(request, user_id):

     pass

Реализация на стороне клиента React

Установите axios

npm i axios

Вот как выглядит запрос для регистрации. Он схож с авторизацией. В случае успеха операции вы получаете токены и суёте их в localStorage:

const handleSubmitForm = (values) => {    

axios.post('ваш url указанный в urlpatterns', {

            username: values.login_reg,

            password: values.password_reg,

        })

        .then(response => {

            if (response.status != 201) return

            localStorage.setItem('accessToken', response.data.access);

            localStorage.setItem('refreshToken', response.data.refresh);

        })

        .catch(error => console.error(error))

    }

Для выхода:

const handleLogOut = () => {

        axios.post('ваш url как в urlpatterns', {

            refresh_token: localStorage.getItem('refreshToken'),

        })

        .then(response => {

            if (response.status != 200) return

            localStorage.removeItem('accessToken');

            localStorage.removeItem('refreshToken');

        })

        .catch(error => console.error(error))

    }

Истечение срока Access

В запросах, где вы используете Access токен вы можете добавить проверку на ошибку 401, если она возникает вы отправляете своей refresh, получаете пару новых токенов и обновляете свой localStorage:

export const updateTokens = () => {

  axios.post('Ваш url', { refresh: localStorage.getitem('refreshToken')})

  .then(response => {

      const newAccessToken = response.data.access;

      const newRefreshToken = response.data.refresh;

      localStorage.setItem('accessToken', newAccessToken)

      localStorage.setItem('refreshToken', newRefreshToken)

   

  })

  .catch(error => {

      console.error('Ошибка при обновлении токена:', error);

  })

} 

Как отправлять токен для доступа к страницам, требующих авторизованности?

Вот вы авторизовались, хотите попасть на страницу своего профиля, в view которого требуется авторизация:  permission_classes = [IsAuthenticated].

Для этого запрос нужно подкрепить заголовком с токеном:

axios.get('url', {

        headers: {

            'Authorization': `Bearer ${localStorage.getItem('accessToken')}`

        },

    })

© Habrahabr.ru