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