diff --git a/docs/ru/docs/tutorial/security/simple-oauth2.md b/docs/ru/docs/tutorial/security/simple-oauth2.md new file mode 100644 index 000000000..7c77e9076 --- /dev/null +++ b/docs/ru/docs/tutorial/security/simple-oauth2.md @@ -0,0 +1,433 @@ +# Простой OAuth2 с паролем и Bearer + +Теперь давайте, отталкиваясь от предыдущей главы, добавим недостающие части, чтобы получить полную безопасности. + +## Получение `имени пользователя` и `пароля` + +Для получения `имени пользователя` и `пароля` мы будем использовать утилиты безопасности **FastAPI**. + +OAuth2 определяет, что при использовании "аутентификации по паролю" (которую мы и используем) клиент/пользователь должен передавать поля `username` и `password` в качестве данных формы. + +В спецификации сказано, что поля должны быть названы именно так. Поэтому `user-name` или `email` работать не будут. + +Но не волнуйтесь, вы можете показать его конечным пользователям во фронтенде в том виде, в котором хотите. + +А ваши модели баз данных могут использовать любые другие имена. + +Но в *операции пути* нам необходимо использовать именно эти имена, чтобы соответствовать спецификации (и иметь возможность, например, использовать это во встроенной системе документации API). + +В спецификации также указано, что `username` и `password` должны передаваться в виде данных формы (так что никакого JSON здесь нет). + +### `Scope` + +В спецификации также говорится, что клиент может передать еще одно поле формы "`scope`". + +Имя поля формы - `scope` (в единственном числе), но на самом деле это длинная строка "scopes", разделенными пробелами. + +Каждая "область видимости" - это просто строка (без пробелов). + +Обычно они используются, например, для объявления определенных разрешений безопасности: + +* `users:read` или `users:write` являются распространенными примерами. +* `instagram_basic` используется Facebook / Instagram. +* `https://www.googleapis.com/auth/drive` используется компанией Google. + +!!! info "Дополнительнаяя информация" + В OAuth2 "scope" - это просто строка, которая объявляет конкретное требуемое разрешение. + + Не имеет значения, содержит ли он другие символы, например `:`, или является ли он URL. + + Эти детали зависят от конкретной реализации. + + Для OAuth2 это просто строки. + +## Код получения `имени пользователя` и `пароля` + +Теперь воспользуемся для этого утилитами, предоставляемыми **FastAPI**. + +### `OAuth2PasswordRequestForm` + +Во-первых, импортируйте `OAuth2PasswordRequestForm` и используйте ее как зависимость с `Depends` в *операции пути* для пути `/token`: + +=== "Python 3.10+" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="4 78" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="4 79" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="2 74" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="4 76" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +`OAuth2PasswordRequestForm` - это зависимость от класса, которая объявляет тело формы со следующими полями: + +* `username`. +* `password`. +* Необязательное поле `scope` в виде большой строки, состоящей из строк, разделенных пробелами. +* Необязательное поле `grant_type`. + +!!! tip "Подсказка" + В спецификации OAuth2 действительно *требуется* поле `grant_type` с фиксированным значением `password`, но `OAuth2PasswordRequestForm` не обеспечивает этого. + + Если вам необходимо обеспечить это, используйте `OAuth2PasswordRequestFormStrict` вместо `OAuth2PasswordRequestForm`. + +* Необязательное поле `client_id` (в нашем примере он не нужен). +* Необязательное поле `client_secret` (в нашем примере он не нужен). + +!!! info "Дополнительная информация" + Форма `OAuth2PasswordRequestForm` не является специальным классом для **FastAPI**, как и `OAuth2PasswordBearer`. + + `OAuth2PasswordBearer` дает понять **FastAPI**, что это схема безопасности. Поэтому она добавляется в OpenAPI именно таким образом. + + Но `OAuth2PasswordRequestForm` - это всего лишь зависимость от класса, которую вы могли бы написать сами, или объявить параметры `Form` напрямую. + + Но поскольку это распространенный вариант использования, он предоставляется **FastAPI** напрямую, просто чтобы облегчить задачу. + +### Использование данных формы + +!!! tip "Подсказка" + Экземпляр зависимого класса `OAuth2PasswordRequestForm` не будет иметь атрибута `scope` с длинной строкой, разделенной пробелами, вместо этого он будет иметь атрибут `scopes` с фактическим списком строк для каждого отправляемого диапазона. + + В данном примере мы не используем `scopes`, но если вам это необходимо, то такая функциональность имеется. + +Теперь получим данные о пользователе из (ненастоящей) базы данных, используя `имя пользователя` из поля формы. + +Если такого пользователя нет, то мы возвращаем ошибку "неверное имя пользователя или пароль". + +Для ошибки мы используем исключение `HTTPException`: + +=== "Python 3.10+" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="3 79-81" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="3 80-82" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="1 75-77" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="3 77-79" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +### Проверка пароля + +На данный момент у нас есть данные о пользователе из нашей базы данных, но мы еще не проверили пароль. + +Давайте сначала поместим эти данные в модель Pydantic `UserInDB`. + +Ни в коем случае нельзя сохранять пароли в открытом виде, поэтому мы будем использовать систему хеширования паролей. + +Если пароли не совпадают, мы возвращаем ту же ошибку. + +#### Хеширование паролей + +"Хеширование" означает: преобразование некоторого содержимого (в данном случае пароля) в последовательность байтов (просто строку), которая выглядит как тарабарщина. + +Каждый раз, когда вы передаете точно такое же содержимое (точно такой же пароль), вы получаете точно такую же тарабарщину. + +Но преобразовать тарабарщину обратно в пароль невозможно. + +##### Зачем использовать хеширование паролей + +Если ваша база данных будет украдена, то у вора не будет паролей пользователей в открытом виде, только хэши. + +Таким образом, вор не сможет попытаться использовать эти же пароли в другой системе (поскольку многие пользователи используют одни и те же пароли повсеместно, это было бы опасно). + +=== "Python 3.10+" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="82-85" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="83-86" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="78-81" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="80-83" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +#### Про `**user_dict` + +`UserInDB(**user_dict)` означает: + +*Передавать ключи и значения `user_dict` непосредственно в качестве аргументов ключ-значение, что эквивалентно:* + +```Python +UserInDB( + username = user_dict["username"], + email = user_dict["email"], + full_name = user_dict["full_name"], + disabled = user_dict["disabled"], + hashed_password = user_dict["hashed_password"], +) +``` + +!!! info "Дополнительная информация" + Более полное объяснение `**user_dict` можно найти в [документации к **Дополнительным моделям**](../extra-models.md#about-user_indict){.internal-link target=_blank}. + +## Возврат токена + +Ответ конечной точки `token` должен представлять собой объект в формате JSON. + +Он должен иметь `token_type`. В нашем случае, поскольку мы используем токены типа "Bearer", тип токена должен быть "`bearer`". + +И в нем должна быть строка `access_token`, содержащая наш токен доступа. + +Для этого простого примера мы будем совершенно небезопасны и вернем то же самое `имя пользователя`, что и токен. + +!!! tip "Подсказка" + В следующей главе мы рассмотрим реальную защищенную реализацию с хешированием паролей и токенами JWT. + + Но пока давайте остановимся на необходимых нам деталях. + +=== "Python 3.10+" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="87" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="88" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="83" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="85" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +!!! tip "Подсказка" + Согласно спецификации, вы должны возвращать JSON с `access_token` и `token_type`, как в данном примере. + + Это то, что вы должны сделать сами в своем коде, и убедиться, что вы используете эти JSON-ключи. + + Это практически единственное, что нужно не забывать делать самостоятельно, чтобы соответствовать спецификации. + + В остальном за вас это сделает **FastAPI**. + +## Обновление зависимостей + +Теперь мы обновим наши зависимости. + +Мы хотим получить значение `current_user` *только* если этот пользователь активен. + +Поэтому мы создаем дополнительную зависимость `get_current_active_user`, которая, в свою очередь, использует в качестве зависимости `get_current_user`. + +Обе эти зависимости просто вернут HTTP-ошибку, если пользователь не существует или неактивен. + +Таким образом, в нашей конечной точке мы получим пользователя только в том случае, если он существует, правильно аутентифицирован и активен: + +=== "Python 3.10+" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py310.py!} + ``` + +=== "Python 3.9+" + + ```Python hl_lines="58-66 69-74 94" + {!> ../../../docs_src/security/tutorial003_an_py39.py!} + ``` + +=== "Python 3.8+" + + ```Python hl_lines="59-67 70-75 95" + {!> ../../../docs_src/security/tutorial003_an.py!} + ``` + +=== "Python 3.10+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="56-64 67-70 88" + {!> ../../../docs_src/security/tutorial003_py310.py!} + ``` + +=== "Python 3.8+ без Annotated" + + !!! tip "Подсказка" + Предпочтительнее использовать версию с аннотацией, если это возможно. + + ```Python hl_lines="58-66 69-72 90" + {!> ../../../docs_src/security/tutorial003.py!} + ``` + +!!! info "Дополнительная информация" + Дополнительный заголовок `WWW-Authenticate` со значением `Bearer`, который мы здесь возвращаем, также является частью спецификации. + + Любой HTTP с кодом состояния 401 "UNAUTHORIZED" должен также возвращать заголовок `WWW-Authenticate`. + + В случае с токенами-носителями (наш случай) значение этого заголовка должно быть `Bearer`. + + На самом деле этот дополнительный заголовок можно пропустить, и все будет работать. + + Но он приведен здесь для соответствия спецификации. + + Кроме того, могут существовать инструменты, которые ожидают его и могут использовать, и это может быть полезно для вас или ваших пользователей, сейчас или в будущем. + + В этом и заключается преимущество стандартов... + +## Посмотим в действии + +Откроем интерактивную документацию: http://127.0.0.1:8000/docs. + +### Аутентификация + +Нажмите кнопку "Авторизация". + +Используйте учетные данные: + +Пользователь: `johndoe` + +Пароль: `secret` + + + +После авторизации в системе вы увидите следующее: + + + +### Получение собственных пользовательских данных + +Теперь, используя операцию `GET` с путем `/users/me`, вы получите данные пользователя, например: + +```JSON +{ + "username": "johndoe", + "email": "johndoe@example.com", + "full_name": "John Doe", + "disabled": false, + "hashed_password": "fakehashedsecret" +} +``` + + + +Если щелкнуть на значке замка и выйти из системы, а затем попытаться выполнить ту же операцию еще раз, то будет выдана ошибка HTTP 401: + +```JSON +{ + "detail": "Not authenticated" +} +``` + +### Неактивный пользователь + +Теперь попробуйте с неактивным пользователем, пройдите аутентификацию: + +Пользователь: `alice` + +Пароль: `secret2` + +И попробуйте использовать операцию `GET` с путем `/users/me`. + +Вы получите ошибку "Inactive user", как тут: + +```JSON +{ + "detail": "Inactive user" +} +``` + +## Резюме + +Теперь у вас есть инструменты для реализации полноценной системы безопасности на основе `имени пользователя` и `пароля` для вашего API. + +Используя эти средства, можно сделать систему безопасности совместимой с любой базой данных, с любым пользователем или моделью данных. + +Единственная деталь, которой не хватает - это то, что она еще не является фактически "защищенной". + +В следующей главе вы увидите, как использовать библиотеку безопасного хеширования паролей и токены JWT.