committed by
GitHub
1 changed files with 272 additions and 0 deletions
@ -0,0 +1,272 @@ |
|||
# Простая авторизация по протоколу OAuth2 с токеном типа Bearer |
|||
|
|||
Теперь, отталкиваясь от предыдущей главы, добавим недостающие части, чтобы получить безопасную систему. |
|||
|
|||
## Получение `имени пользователя` и `пароля` |
|||
|
|||
Для получения `имени пользователя` и `пароля` мы будем использовать утилиты безопасности **FastAPI**. |
|||
|
|||
Протокол OAuth2 определяет, что при использовании "аутентификации по паролю" (которую мы и используем) клиент/пользователь должен передавать поля `username` и `password` в полях формы. |
|||
|
|||
В спецификации сказано, что поля должны быть названы именно так. Поэтому `user-name` или `email` работать не будут. |
|||
|
|||
Но не волнуйтесь, вы можете показать его конечным пользователям во фронтенде в том виде, в котором хотите. |
|||
|
|||
А ваши модели баз данных могут использовать любые другие имена. |
|||
|
|||
Но при авторизации согласно спецификации, требуется использовать именно эти имена, что даст нам возможность воспользоваться встроенной системой документации API. |
|||
|
|||
В спецификации также указано, что `username` и `password` должны передаваться в виде данных формы (так что никакого JSON здесь нет). |
|||
|
|||
### Oбласть видимости (scope) |
|||
|
|||
В спецификации также говорится, что клиент может передать еще одно поле формы "`scope`". |
|||
|
|||
Имя поля формы - `scope` (в единственном числе), но на самом деле это длинная строка, состоящая из отдельных областей видимости (scopes), разделенных пробелами. |
|||
|
|||
Каждая "область видимости" (scope) - это просто строка (без пробелов). |
|||
|
|||
Обычно они используются для указания уровней доступа, например: |
|||
|
|||
* `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`: |
|||
|
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[4,78] *} |
|||
|
|||
`OAuth2PasswordRequestForm` - это класс для использования в качестве зависимости для *функции обрабатывающей эндпоинт*, который определяет тело формы со следующими полями: |
|||
|
|||
* `username`. |
|||
* `password`. |
|||
* Необязательное поле `scope` в виде большой строки, состоящей из строк, разделенных пробелами. |
|||
* Необязательное поле `grant_type`. |
|||
|
|||
/// tip | Подсказка |
|||
По спецификации OAuth2 поле `grant_type` является обязательным и содержит фиксированное значение `password`, но `OAuth2PasswordRequestForm` не обеспечивает этого. |
|||
|
|||
Если вам необходимо использовать `grant_type`, воспользуйтесь `OAuth2PasswordRequestFormStrict` вместо `OAuth2PasswordRequestForm`. |
|||
/// |
|||
|
|||
* Необязательное поле `client_id` (в нашем примере он не нужен). |
|||
* Необязательное поле `client_secret` (в нашем примере он не нужен). |
|||
|
|||
/// info | Дополнительная информация |
|||
Форма `OAuth2PasswordRequestForm` не является специальным классом для **FastAPI**, как `OAuth2PasswordBearer`. |
|||
|
|||
`OAuth2PasswordBearer` указывает **FastAPI**, что это схема безопасности. Следовательно, она будет добавлена в OpenAPI. |
|||
|
|||
Но `OAuth2PasswordRequestForm` - это всего лишь класс зависимости, который вы могли бы написать самостоятельно или вы могли бы объявить параметры `Form` напрямую. |
|||
|
|||
Но, поскольку это распространённый вариант использования, он предоставляется **FastAPI** напрямую, просто чтобы облегчить задачу. |
|||
/// |
|||
|
|||
### Использование данных формы |
|||
|
|||
/// tip | Подсказка |
|||
В экземпляре зависимого класса `OAuth2PasswordRequestForm` атрибут `scope`, состоящий из одной длинной строки, разделенной пробелами, заменен на атрибут `scopes`, состоящий из списка отдельных строк, каждая из которых соответствует определенному уровню доступа. |
|||
|
|||
В данном примере мы не используем `scopes`, но если вам это необходимо, то такая функциональность имеется. |
|||
/// |
|||
|
|||
Теперь получим данные о пользователе из (ненастоящей) базы данных, используя `username` из поля формы. |
|||
|
|||
Если такого пользователя нет, то мы возвращаем ошибку "неверное имя пользователя или пароль". |
|||
|
|||
Для ошибки мы используем исключение `HTTPException`: |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[3,79:81] *} |
|||
|
|||
### Проверка пароля |
|||
|
|||
На данный момент у нас есть данные о пользователе из нашей базы данных, но мы еще не проверили пароль. |
|||
|
|||
Давайте сначала поместим эти данные в модель Pydantic `UserInDB`. |
|||
|
|||
Ни в коем случае нельзя сохранять пароли в открытом виде, поэтому мы будем использовать (пока что ненастоящую) систему хеширования паролей. |
|||
|
|||
Если пароли не совпадают, мы возвращаем ту же ошибку. |
|||
|
|||
#### Хеширование паролей |
|||
|
|||
"Хеширование" означает: преобразование некоторого содержимого (в данном случае пароля) в последовательность байтов (просто строку), которая выглядит как тарабарщина. |
|||
|
|||
Каждый раз, когда вы передаете точно такое же содержимое (точно такой же пароль), вы получаете точно такую же тарабарщину. |
|||
|
|||
Но преобразовать тарабарщину обратно в пароль невозможно. |
|||
|
|||
##### Зачем использовать хеширование паролей |
|||
|
|||
Если ваша база данных будет украдена, то у вора не будет паролей пользователей в открытом виде, только хэши. |
|||
|
|||
Таким образом, вор не сможет использовать эти же пароли в другой системе (поскольку многие пользователи используют одни и те же пароли повсеместно, это было бы опасно). |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[82:85] *} |
|||
|
|||
#### Про `**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`, содержащая наш токен доступа. |
|||
|
|||
В этом простом примере мы нарушим все правила безопасности, и будем считать, что имя пользователя (username) полностью соответствует токену (token) |
|||
|
|||
/// tip | Подсказка |
|||
В следующей главе мы рассмотрим реальную защищенную реализацию с хешированием паролей и токенами <abbr title="JSON Web Tokens">JWT</abbr>. |
|||
|
|||
Но пока давайте остановимся на необходимых нам деталях. |
|||
/// |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[87] *} |
|||
|
|||
/// tip | Подсказка |
|||
Согласно спецификации, вы должны возвращать JSON с `access_token` и `token_type`, как в данном примере. |
|||
|
|||
Это то, что вы должны сделать сами в своем коде и убедиться, что вы используете эти JSON-ключи. |
|||
|
|||
Это практически единственное, что нужно не забывать делать самостоятельно, чтобы следовать требованиям спецификации. |
|||
|
|||
Все остальное за вас сделает **FastAPI**. |
|||
/// |
|||
|
|||
## Обновление зависимостей |
|||
|
|||
Теперь мы обновим наши зависимости. |
|||
|
|||
Мы хотим получить значение `current_user` *только* если этот пользователь активен. |
|||
|
|||
Поэтому мы создаем дополнительную зависимость `get_current_active_user`, которая, в свою очередь, использует в качестве зависимости `get_current_user`. |
|||
|
|||
Обе эти зависимости просто вернут HTTP-ошибку, если пользователь не существует или неактивен. |
|||
|
|||
Таким образом, в нашем эндпоинте мы получим пользователя только в том случае, если он существует, правильно аутентифицирован и активен: |
|||
|
|||
{* ../../docs_src/security/tutorial003_an_py310.py hl[58:66,69:74,94] *} |
|||
|
|||
/// info | Дополнительная информация |
|||
Дополнительный заголовок `WWW-Authenticate` со значением `Bearer`, который мы здесь возвращаем, также является частью спецификации. |
|||
|
|||
Ответ сервера с HTTP-кодом 401 "UNAUTHORIZED" должен также возвращать заголовок `WWW-Authenticate`. |
|||
|
|||
В случае с bearer-токенами (наш случай) значение этого заголовка должно быть `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": "[email protected]", |
|||
"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>. |
Loading…
Reference in new issue