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

от автора

Когда-то давно я уже делал авторизацию в 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')}`          },      })  


ссылка на оригинал статьи https://habr.com/ru/articles/793058/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *