🌐 Add Russian translation for `docs/ru/docs/tutorial/security/simple-oauth2.md`

This commit is contained in:
Andrukhov Aleksandr 2023-11-06 15:11:11 +03:00
parent 480620372a
commit a33d968767
1 changed files with 433 additions and 0 deletions

View File

@ -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 "Подсказка"
В следующей главе мы рассмотрим реальную защищенную реализацию с хешированием паролей и токенами <abbr title="JSON Web Tokens">JWT</abbr>.
Но пока давайте остановимся на необходимых нам деталях.
=== "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`.
На самом деле этот дополнительный заголовок можно пропустить, и все будет работать.
Но он приведен здесь для соответствия спецификации.
Кроме того, могут существовать инструменты, которые ожидают его и могут использовать, и это может быть полезно для вас или ваших пользователей, сейчас или в будущем.
В этом и заключается преимущество стандартов...
## Посмотим в действии
Откроем интерактивную документацию: <a href="http://127.0.0.1:8000/docs" class="external-link" target="_blank">http://127.0.0.1:8000/docs</a>.
### Аутентификация
Нажмите кнопку "Авторизация".
Используйте учетные данные:
Пользователь: `johndoe`
Пароль: `secret`
<img src="/img/tutorial/security/image04.png">
После авторизации в системе вы увидите следующее:
<img src="/img/tutorial/security/image05.png">
### Получение собственных пользовательских данных
Теперь, используя операцию `GET` с путем `/users/me`, вы получите данные пользователя, например:
```JSON
{
"username": "johndoe",
"email": "johndoe@example.com",
"full_name": "John Doe",
"disabled": false,
"hashed_password": "fakehashedsecret"
}
```
<img src="/img/tutorial/security/image06.png">
Если щелкнуть на значке замка и выйти из системы, а затем попытаться выполнить ту же операцию еще раз, то будет выдана ошибка HTTP 401:
```JSON
{
"detail": "Not authenticated"
}
```
### Неактивный пользователь
Теперь попробуйте с неактивным пользователем, пройдите аутентификацию:
Пользователь: `alice`
Пароль: `secret2`
И попробуйте использовать операцию `GET` с путем `/users/me`.
Вы получите ошибку "Inactive user", как тут:
```JSON
{
"detail": "Inactive user"
}
```
## Резюме
Теперь у вас есть инструменты для реализации полноценной системы безопасности на основе `имени пользователя` и `пароля` для вашего API.
Используя эти средства, можно сделать систему безопасности совместимой с любой базой данных, с любым пользователем или моделью данных.
Единственная деталь, которой не хватает - это то, что она еще не является фактически "защищенной".
В следующей главе вы увидите, как использовать библиотеку безопасного хеширования паролей и токены <abbr title="JSON Web Tokens">JWT</abbr>.