fastapi/docs/ru/docs/tutorial/security/simple-oauth2.md

21 KiB
Raw Blame History

Простой 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 непосредственно в качестве аргументов ключ-значение, что эквивалентно:

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 можно найти в документации к Дополнительным моделям{.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, вы получите данные пользователя, например:

{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false,
  "hashed_password": "fakehashedsecret"
}

Если щелкнуть на значке замка и выйти из системы, а затем попытаться выполнить ту же операцию еще раз, то будет выдана ошибка HTTP 401:

{
  "detail": "Not authenticated"
}

Неактивный пользователь

Теперь попробуйте с неактивным пользователем, пройдите аутентификацию:

Пользователь: alice

Пароль: secret2

И попробуйте использовать операцию GET с путем /users/me.

Вы получите ошибку "Inactive user", как тут:

{
  "detail": "Inactive user"
}

Резюме

Теперь у вас есть инструменты для реализации полноценной системы безопасности на основе имени пользователя и пароля для вашего API.

Используя эти средства, можно сделать систему безопасности совместимой с любой базой данных, с любым пользователем или моделью данных.

Единственная деталь, которой не хватает - это то, что она еще не является фактически "защищенной".

В следующей главе вы увидите, как использовать библиотеку безопасного хеширования паролей и токены JWT.