FastApi: авторизированный вход

Из предыдущей статьи у нас есть некий вызов API после выполнения которого у нас на руках будет некий сессионый ключ.

Попробуем теперь используя этот сессионый ключ, запросить некую информацию о пользователе. Для этого добавим в схему auth.py классы:

class info_class(BaseModel):
    username : Optional[str] = Field(None, title="Имя пользователя", example="Вася Пупкин")
    phone    : Optional[str] = Field(None, title="Номер телефона", example="+79222347594")
    email    : Optional[str] = Field(None, title="Email пользователя", example="dowiurefh@mail.ru")

class info_out(BaseModel):
    error     : Optional[bool]  = Field(..., title="Результат выполнения запроса", example="false")
    comment   : Optional[str]   = Field(None, title="Текст ошибки", example="Пользователь удалён")
    result    : Optional[info_class]

Следующим шагом будет защитить доступ к остальным компонентам API, от доступа без сессионного ключа. Для этого воспользуемся классом FastApi security и добавим в требования заголовка запроса тег авторизации.

Получим итоговый роутинг auth.py:

# -*- coding: utf-8 -*-
"""Different helper-functions to work with users."""
import json
import fastapi;
from fastapi import APIRouter,Depends
from app.schemas import auth as auth_models
from pydantic import BaseModel

token_key = fastapi.security.APIKeyHeader(name="Authorization")

class Token(BaseModel):
    token: str

# получить текущий токен
def get_current_token(oauth_header: str = fastapi.Security(token_key)):
    #print(f"Token: {oauth_header}")
    oauth_header=oauth_header.replace("Token ","")
    return oauth_header

router = APIRouter()

@router.post('/auth',response_model=auth_models.auth_out,tags=["auth"]) # заданы исходящие параметры
async def auth(into = Depends(auth_models.auth_in)): # заданы входящие параметры
    # если параметры не заданы
    if into.password==None: into.password="NONE"
    if into.login == None: into.password = "NONE"
    hash="12345678"
    # сформируем ответ
    out=auth_models.auth_out(hash=hash)
    return out

@router.post('/GetInfo',response_model=auth_models.info_out,tags=["auth"]) # заданы исходящие параметры
async def GetInfo(token: Token=Depends(get_current_token)): # заданы входящие параметры
    print(f"Токен: {token}")
    if token!="12345678":
        print("Токен не верен!")
        return auth_models.info_out(error=True,comment="Токен не найден")
    # сформируем ответ

    return auth_models.info_out(error=False,result={"username":"Pavel","email":"vasya@mail.ru"})

В результате мы можем увидеть в документации появившийся «замочек»:

При попытке выполнения запроса без авторизации, соответственно получим ошибку аторизации:

Not authenticated

Настройка внешнего доступа к API

В предыдущей статье был рассмотрен запуск сервера и доступ к нему. Одно НО, доступ этот осуществляется или с локального ПК (в main.py), как вы видели присутствует строчка:

uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info")

Если host поменять например на 0.0.0.0, сервер «из вне» будет конечно доступен, но по нестандартному порту и не по защищенному протоколу. Однако есть способ «завернуть» весь трафик в https через проксирование в apache/ngnix.

Для apache необходимо установить модуль proxy:

sudo a2enmod proxy proxy_http
sudo service apache2 restart

И в настройки VirtualHost сайта добавить:

ProxyPass / http://127.0.0.1:8000
ProxyPassReverse / http://127.0.0.1:8000

Для ngnix к сожалению не могу дать примера, т.к. обычно не использую.

FastApi: основы

Зачем нужно

FastApi — это фреймворк позволяющий создавать «самодокументирующиеся API». Т.е. и реализация и документация пишется прямо в коде. Причем FastApi при помощи вебинтерфейса предоставляет еще и возможность тестирования получившегося API.

Подготовка

Для работы потребуется установленные пакеты fastapi и uvicorn :

pip3 install fastapi
pip3 install uvicorn

uvicorn требуется чтобы запустить веб сервер на определенном порту, для взаимодействия с фреймворком fastapi. Далее его можно проксировать через apache или ngnix, В основном для того, чтобы к api можно было обращаться из вне не только по IP но и по DNS имени. Ну и чтобы обернуть в https. Хотя консольный uvicorn позволяет может запускаться и по https протоколу, если указать ему сертификаты. Но не пробовал, ибо было не нужно.

Еще разик акцентирую внимание: uvicorn может поставляться в виде пакета python или в виде консольной утилиты. По сути это два разных способа поднятия вебсервера для fastapi. Результат будет один. Только в случае использования пакета python — внутри скрипта нужно будет реализовывать запуск uvicorn

Первый шаг

Как происходит работа внешнего пользователя с API? Обычно он авторизуется по логину-паролю, получает «сессионный ключ» и далее работает уже с ним. Что собственно и попробуем реализовать. Сначала на минимальном примере.

В FastApi используется модель pedantic, т.е. на входе функций реализуемых API должно попадать данные согласно заранее построенной схеме. Вызов функции, осуществляется при помощи роутинга в URL.

Например реализуем вызов функции API авторизации по URL вида:

http://127.0.0.1:8000/v1/auth

Запрос к URL будет выполнятся при помощи POST, с параметрами login и password

Создадим следующую структуру файлов и папок:

В файле main.py реализуем запуск сервера uvicorn и начальные настройки для запуска фреймворка FastAPI.

В файле /app/routers/auth.py реализуем логику ответа на вызов URL с параметрами POST

В файле /app/schemas/auth.py — создадим проверку входящих и исходящих параметров вызова согласно схеме pedantic

main.py:

import uvicorn
from fastapi import FastAPI
from app.routers import auth

tags_metadata = [
    {"name": "auth", "description": "Получение ключа по логину, паролю"},
]

app = FastAPI(title="API documentation", description="Какое то описание ",version="0.0.1", openapi_tags=tags_metadata, )

app.include_router(auth.router, prefix="/v1")

if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info")

Схема auth.py:

# -*- coding: utf-8 -*-

from typing import Optional
from pydantic import BaseModel,Field

class auth_out(BaseModel):
    """ Что Выходит """
    hash: str = Field(..., title="Сессионый ключ полученый по логину - паролю", example="iauwhfpseurh e")

class auth_in(BaseModel):
    """ Что входит """
    login: Optional [str] = Field(..., title="Логин пользователя", example="Например Вася")
    password: Optional [str] =Field(..., title="Пароль пользователя", example="Например IUHID#U$HI")

Роутер auth.py:

# -*- coding: utf-8 -*-
"""Different helper-functions to work with users."""
import json

from fastapi import APIRouter,Depends
from app.schemas import auth as auth_models

router = APIRouter()

@router.post('/auth',response_model=auth_models.auth_out,tags=["auth"]) # заданы исходящие параметры
async def auth(into = Depends(auth_models.auth_in)): # заданы входящие параметры
    # если параметры не заданы
    if into.password==None: into.password="NONE"
    if into.login == None: into.password = "NONE"
    hash="eopwhdfweruieruiopfhp[ruifhp[eruifhperf8yareuifh[ay8wedfper8"
    # сформируем ответ
    out=auth_models.auth_out(hash=hash)
    return out

Ну вот собственно и всё. Минимальный рабочий каркас собран. Осталось попробовать его запустить и посмотреть результат в браузере. Для этого в консоли запустим:

python main.py

Далее открываем браузер по url http://127.0.0.1:8000/docs или (http://127.0.0.1:8000/redoc) и можем наблюдать уже сформированную готовую к употреблению документацию:

Причем работоспособность вызовов можно попробовать прямо сейчас.

В текущем случае мы запустили сервер uvicorn из скрипта python, но есть вариант запуска сервера и из консоли. Какие преимущества имеет запуск из консоли, мне не очень пока очевидны. Но факт есть факт. Скрипт можно запустить и так:

uvicorn main:app --port 8000 --workers 10

В данном случае запустили 10 воркеров FastApi. Если использовать ключ —reload, то сервер будет отслеживать изменение файлов и перезапускать сам себя. Это удобно для моментальной отладки

Внимание! Ключи —reload и —workers одновременно не работают.

Настоящие программисты не используют Паскаль

Наткнулся в сети, и вспомнил что в 90-е зачитывался этим подобным на сидя своей EC-1841. Принесенным домой на дискетах! Никакого интернета..

    Настоящие программисты не используют Паскаль

        Источник:  DATAMATION  Jul.??  pp.207-209
        =============================================

                                            Ed Post
                                            Wilsonville, Orezon
» Читать далее

Flutter: отображение веб страницы в виджете

Работа с webview во Flutter не так проста как на «чистом» Java/Kotlin. В основном из-за того что, на pub.dev есть большое разнообразие собственно вапперов к webview, а потому выбрать рабочую и полноценную реализацию не так просто. Я например потратил с полдня, чтобы выяснить, что:

webview_flutter — не отображает страницы с «плохими» сертификатами SSL. Например самоподписанными, или с сертификатами с истёкшим сроком действия, или с сертификатом к которому нет доверия (теперь это все сайты госучреждений и некоторых банков). И нет никакого способа (или не нашел), заставить это сделать.

flutter_inappwebview — та же самая проблема что и в вышеуказанном плагине. Да, там есть ключ «игнорировать ошибки SSL», но он не работает

flutter_webview_plugin — выпущен 21 месяц назад, и при компиляции уже ругается на отсутствие поддержки Android 12. Хотя как раз этот плагин, работает со страницами с плохим SSL именно так как нужно

flutter_webview_plugin_ios_android — а вот этот плагин, это как я понял «подхваченый из ослабеших» рук разработчиков flutter_webview_plugin, и доработаный уже под Android 12. На нём мои поиски и закончились. Ниже минимальный пример для работы с ним:

import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin_ios_android/flutter_webview_plugin_ios_android.dart';
import 'dart:async';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
         '/': (_) => const MyHomePage(),
      }
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String selectedUrl = 'https://грибовы.рф';
  final flutterWebViewPlugin = FlutterWebviewPlugin();

  // при изменении url
  late StreamSubscription<String> _onUrlChanged;
  // при ошибке
  late StreamSubscription<WebViewHttpError> _onHttpError;
  // изменение процента загрузки страницы
  late StreamSubscription<double> _onProgressChanged;
  // поскролили вверх-вниз
  late StreamSubscription<double> _onScrollYChanged;
  // поскролили вправо-влево
  late StreamSubscription<double> _onScrollXChanged;

  @override
  void dispose() {
    _onUrlChanged.cancel();
    _onHttpError.cancel();
    _onProgressChanged.cancel();
    _onScrollXChanged.cancel();
    _onScrollYChanged.cancel();
    flutterWebViewPlugin.dispose();
    super.dispose();
  }

  @override
  void initState() {
    super.initState();
    flutterWebViewPlugin.close();

    // Слушатель изменения url страницы
    _onUrlChanged = flutterWebViewPlugin.onUrlChanged.listen((String url) {
      print("URL: $url");
    });
    // изменение прогресса загрузки
    _onProgressChanged =
        flutterWebViewPlugin.onProgressChanged.listen((double progress) {
            setState(() {
              print('onProgressChanged: $progress');
            });
        });

    _onScrollYChanged =
        flutterWebViewPlugin.onScrollYChanged.listen((double y) {
        });

    _onScrollXChanged =
        flutterWebViewPlugin.onScrollXChanged.listen((double x) {
        });


    _onHttpError =
        flutterWebViewPlugin.onHttpError.listen((WebViewHttpError error) {
          print("Ошибка загрузки:  ${error.code} ${error.url}");
        });


  }

  // перехват вызовов Javascript
  final Set<JavascriptChannel> jsChannels = [
    JavascriptChannel(
        name: 'Print',
        onMessageReceived: (JavascriptMessage message) {
          print(message.message);
        }),
  ].toSet();



  @override
  Widget build(BuildContext context) {
    return  WebviewScaffold(
        ignoreSSLErrors: true,
        url: selectedUrl,
        javascriptChannels: jsChannels,
        mediaPlaybackRequiresUserGesture: false,
        appBar: AppBar(
          title: const Text('Widget WebView'),
        ),
        withZoom: true,
        withLocalStorage: true,
        hidden: false,
        initialChild: Container(
          color: Colors.redAccent,
          child: const Center(
            child: Text('Ожидаем загрузки...'),
          ),
        ),
    );
  }
}
1 49 50 51 52 53 308