Авторизация в 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')}`
},
})