Следующим шагом будет защитить доступ к остальным компонентам 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"})
В результате мы можем увидеть в документации появившийся «замочек»:
При попытке выполнения запроса без авторизации, соответственно получим ошибку аторизации:
В предыдущей статье был рассмотрен запуск сервера и доступ к нему. Одно НО, доступ этот осуществляется или с локального ПК (в main.py), как вы видели присутствует строчка:
Если host поменять например на 0.0.0.0, сервер «из вне» будет конечно доступен, но по нестандартному порту и не по защищенному протоколу. Однако есть способ «завернуть» весь трафик в https через проксирование в apache/ngnix.
Для apache необходимо установить модуль proxy:
sudo a2enmod proxy proxy_http
sudo service apache2 restart
Однако вместо ожидаемой передачи заголовка, curl в упор возвращает ровно ничего:
Гугл помог выяснить, что ключевое слово Authorization зарезервированно для использования в Header. На https://stackoverflow.com было подсказано и направление куда копать. А именно, использовать обертку Security:
router = APIRouter()
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}")
return oauth_header
# выдаем список поддерживаемых версий
@router.get('/versions',tags=["Credentials"],responses={
200: {
"model": List[models.versions_info],
"description": "Return has code",
"headers": {"Authorization": {"description":"Токен участника для авторизации","type":"string"}}
},
401: {
"description": "Unauthorized status code.",
}
})
async def list_versions(request: Request,token: funcs.Token = Depends(funcs.get_current_token)): # запрос на получение поддерживаемых версий ПО
print(f"Токен:{token}")
if token==None: return Response(status_code=401, content="Unauthorized status code.", media_type="application/text")
out=[{"version": "2.1.1","url": "https://www.server.com/ocpi/2.1.1/"},{"version": "2.2","url": "https://www.server.com/ocpi/2.2/"}]
return Response(status_code=200,content=json.dumps(out), media_type="application/json", headers={"Authorization": "Token "+config.globals['mytoken']})
Долгое время был любителем по изобретать велосипед, и городил собственные реализации API для внешних систем. Да получалось. Да работало. Но если честно, всегда был в этих API бардак. Документации толковой не было, примеров соответственно тоже. Ну правда работал с этими API собственно практически один я, это и спасало от тухлых помидоров 😉
Решил попробовать таки в следующем своем проекте воспользоваться сторонними решениями для документирования и написания API. Выбор пал на FastApi для python. Собственно дальше просто «рыба» с основными фишками которую можно использовать далее расширяя.
Разберем файл main.py:
import uvicorn
from fastapi import FastAPI
from app.routers import hash
tags_metadata = [
{"name": "use-hash","description": "Создание хэша по логину, паролю и секрету (если есть)"},
{"name": "delete","description": "Удаление хэга по его *ID*.",},
]
app = FastAPI(title="OCPI API documentation",description="Инструментарий для работы по протоколу OCPI",
version="0.0.2",openapi_tags=tags_metadata,)
@app.on_event('startup')
async def startup():
print("Приложение стартовало")
@app.on_event('shutdown')
async def shutdown():
print("Приложение закрыто")
app.include_router(hash.router,prefix="/v1")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000, log_level="info")
Что происходит в этом скрипте? Сначала инициализируется ядро FastApi:
app = FastAPI(title="OCPI API documentation",description="Инструментарий для работы по протоколу OCPI",
version="0.0.2",openapi_tags=tags_metadata,)
Т.е. обявляем название API, описание и присваиваем разделы которые будут отображаться в документации http://127.0.0.1:8000/docs
Далее подключаем роутинг из файла /app/routers/hash.py:
app.include_router(hash.router,prefix="/v1")
Этих роутингов естественно может быть не ограниченное количество
Для того чтобы FastApi мог сформировать документацию, необходимо её чуть подготовить. За это отвечают «схемы». Например создадим схему /app/schemas/hash.py:
# -*- coding: utf-8 -*-
from typing import Optional
from pydantic import BaseModel,Field
class hash_out(BaseModel):
""" Что Выходит """
hash: str = Field(..., title="Хэш созданый на основе логина пароля", example="iauwhfpseurh e")
class hash_in(BaseModel):
""" Что входит """
login: Optional [str] = Field(..., title="Логин пользователя", example="Например Вася")
password: Optional [str]
class delete_param(BaseModel):
id : Optional [int]
class delete_answer(BaseModel):
result : Optional [str]
Т.е. создаем классы с переменными которые могут быть входящими или исходящими пакетами работы API.
Далее создаем непосредственно файл работы с роутом hash:
# -*- coding: utf-8 -*-
"""Different helper-functions to work with users."""
import json
import fastapi
from fastapi import APIRouter,Depends,Request,Header,Response
from app.schemas import hash as models
import hashlib
from typing import Union
router = APIRouter()
@router.post('/hash',response_model=models.hash_out,tags=["use-hash"]) # заданы исходящие параметры
async def hash(into : models.hash_in): # заданы входящие параметры
# если параметры не заданы
if into.password==None: into.password="NONE"
if into.login == None: into.password = "NONE"
hash=hashlib.sha1((into.login+into.password).encode('utf-8')).hexdigest()
# сформируем ответ
out=models.hash_out(hash=hash)
return out
Что у нас тут? А вот что: мы объявляем, что FastAPI должен перенаправлять весь вход по методу POST с URL /hash (на самом деле /v1/hash, т.к. роутин объявили с префиксом v1) в функцию async def hash. В качестве исходящих значений, в response_model указываем схему hash_out заданную в схемах выше. Так -же установим тэг tags=[«use-hash»], для того чтоб функция API была красиво подписана. В качестве входящих параметров функции hash, указываем соответственно модель с классом hash_in
Если хочется «перехватывать» не только POST запросы, но и GET, то вызов чуть изменится:
Т.е. мы изменили вызов на get и в функцию добавили Depends(), т.к. входящие параметры попадают не в виде в данном случае JSON, а в качестве параметров вида: /v1/hash?login=aaa&passowd=bbb, т.е. их нужно конвертировать предварительно в JSON
А что делать, если помимо входящих параметров из POST запроса (ну или GET/PUT/DELETE и т.д), необходимо еще и обработать пришедшие заголовки Headers? Для этого изменим объявление функции, добавив помимо модели, еще и заголовки:
Т.е. нам придется использовать не обычно используемый response_model, для указания класса со схемой ответа, а response, который позволяет указать шаблон при определенном коде ответа. В результате общий файл роута hash.py получается такой:
# -*- coding: utf-8 -*-
"""Different helper-functions to work with users."""
import json
import fastapi
from fastapi import APIRouter,Depends,Request,Header,Response
from app.schemas import hash as models
import hashlib
from typing import Union
router = APIRouter()
@router.post('/hash',response_model=models.hash_out,tags=["use-hash"]) # заданы исходящие параметры
async def hash(into : models.hash_in): # заданы входящие параметры
# если параметры не заданы
if into.password==None: into.password="NONE"
if into.login == None: into.password = "NONE"
hash=hashlib.sha1((into.login+into.password).encode('utf-8')).hexdigest()
# сформируем ответ
out=models.hash_out(hash=hash)
return out
@router.get('/hash',response_model=models.hash_out,tags=["use-hash"]) # заданы исходящие параметры
async def hash(into : models.hash_in=Depends()): # заданы входящие параметры.
# Внимание! Обращаяю внимание на Depends(), оно избавляет от ошибки "Request with GET/HEAD method cannot have body",
# т.е. так параметры ищутся не в body, а в самом запросе GET в виде параметров
# если параметры не заданы
if into.password==None: into.password="NONE"
if into.login == None: into.password = "NONE"
hash=hashlib.sha1((into.login+into.password).encode('utf-8')).hexdigest()
# сформируем ответ
out=models.hash_out(hash=hash)
return out
@router.put('/hash',response_model=models.hash_out,tags=["use-hash"]) # заданы исходящие параметры
async def hash(into : models.hash_in): # заданы входящие параметры
# если параметры не заданы
if into.password==None: into.password="NONE"
if into.login == None: into.password = "NONE"
hash=hashlib.sha1((into.login+into.password).encode('utf-8')).hexdigest()
# сформируем ответ
out=models.hash_out(hash=hash)
return out
@router.delete('/hash',response_model=models.delete_answer,tags=["delete"]) # заданы исходящие параметры
async def hash(into : models.delete_param): # заданы входящие параметры
# сформируем ответ
out=models.delete_answer(result="Ok")
return out
@router.get('/hash_header',tags=["use-hash"],responses={
200: {
"model": models.hash_out,
"description": "Return has code",
"headers": {
"Secret-Code-Out": {"description":"Secret code out","type":"string"}
}
}
}) # заданы исходящие параметры
async def hash(into : models.hash_in=Depends(),secret: Union[str, None] = Header(description="Наш секрет",alias="Secret-Code-In",default=None)):
hash = hashlib.sha1((into.login + into.password+secret).encode('utf-8')).hexdigest()
# сформируем ответ
out = models.hash_out(hash=hash)
return Response(status_code=200,content=json.dumps(out.__dict__), media_type="application/json", headers={"Secret-Code-Out": secret})
Что в принципе покрывает используемые в большинстве случаев кейсы использования