Web API Python com Flask, Restful e Token
Um pequeno servidor em Python, com uma camada simples de segurança
A vantagem de utilizar uma Web API para garantir o acesso e manipulação de dados de uma aplicação possibilita controlar a disponibilidade, segurança e formato dos dados
Fazendo uso de alguns pacotes Python para controlar as Requisições/Respostas, JSON, REST, JWT e Criptografia Hash podemos entregar de maneira fácil e rápida essa API
Ambiente
Usando um ambiente virtual para facilitar o controle de dependências do projeto, detalhamento disponível em Virtual Environment
pip install pipenv
Iniciar o ambiente virtual, onde o desenvolvimento será isolado, para gerenciar as dependências
Dentro da pasta do projeto onde os fontes serão criados
pipenv shell
Dentro do pipenv, os comandos ignoram os pacotes no ambiente externo, sendo necessário instalar os que serão utilizados
pipenv install flask flask-restful flask-jwt-extended passlib
Flask
O pacote do Flask possibilita escutar uma porta para garantir uma aplicação Web
Criar um arquivo app.py com o conteúdo abaixo, esse nome é um dos padrões do Flask, aconselhável seu uso
import os
from flask import Flask
app = Flask(__name__)
@app.route('/')
def start():
return 'Uma resposta flask'
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port)
Como pode ser visto ele está importando a biblioteca Flask, escutando a porta 5000 e retornando o texto 'Uma resposta flask'
Assim podemos executar o aplicativo com o comando
flask run
Acessando pelo navegador o endereço http://localhost:5000/ vai aparecer a mensagem "Uma resposta flask" mostrando que o ambiente está completamente funcional
REST
De forma simplicitada uma REST faz referência a um CRUD através de requisições HTTP, onde o servidor e cliente possui uma comunicação completa sem a necessidade de armazenar informações
- POST: Inserir uma ou várias instâncias
- GET: Consultar instâncias
- PUT: Atualizar as informações de uma ou várias instâncias
- DELETE: Deletar uma instância
Aplicando a leitura das requisições dos HTTP
from flask_restful import Resource, Api
api = Api(app)
class response(Resource):
def post(self):
return {'post':'Resposta de POST'}
def get(self):
return {'get':'Resposta de GET'}
def put(self):
return {'put':'Resposta de PUT'}
def delete(self):
return {'delete':'Resposta de DELETE'}
api.add_resource(response, '/response')
Atualizar a execução do sistema podemos verificar a resposta do processo novo
Testando
Para testar a execução podemos utiliza o PostMan ou pelo pacote request do Python, que pode ser instalada pelo PyPi com o comando
pip install requests
Através do shell do Python, comando python no shell, carregamos os objetos para cada tipo de requisição
from requests import put, get, post, delete
No trecho abaixo realizamos as requisições informando a URL completa, com a função .json() forçando uma resposta em json
post('http://localhost:5000/response').json()
# {'post':'Resposta de POST'}
put('http://localhost:5000/response').json()
# {'post':'Resposta de PUT'}
get('http://localhost:5000/response').json()
# {'get':'Resposta de GET'}
delete('http://localhost:5000/response').json()
# {'delete':'Resposta de DELETE'}
JSON
Uma notação de objetos projetada para o JavaScript, por sua facilitada foi a adotada largamente para armazenamento e trafego de documentos estruturados e sem esquemas, por exemplo um trecho de uma receita de bolo
{
'name':'cake',
'categories':[
{'name':'sweet'},
{'name':'wheat'}
],
'steps':[
{'ingredient':'sugar'},
{'ingredient':'flour'}
]
}
Usando o Flask podemos ler os dados contidos no corpo do JSON e imprimir no servidor
from flask import request
class readJSON(Resource):
def post(self):
data = request.json
print('Name: ' + data['name'])
print('First Category: ' + data['categories'][0]['name'])
print('Steps')
for step in data['steps']:
print('Ingredient: ' + step['ingredient'])
api.add_resource(readJSON, '/readjson')
Realizando um teste pela biblioteca request, fazendo um post informando o conteúdo através do parâmetro json e visualizando a impressão na parte do servidor
post('http://localhost:5000/readjson', json={'name':'cake', 'categories':[{'name':'sweet'}, {'name':'wheat'}], 'steps':[{'ingredient':'sugar'}, {'ingredient':'flour'}]}).json()
# Print do lado da aplicação
# Name: cake
# First Category: sweet
# Steps
# Ingredient: sugar
# Ingredient: flour
Token
Na definição de token temos símbolo, marca ou sinal, uma maneira de garantir um acesso a algo ao mostrar
Podemos limitar o acesso de algumas áreas da API exigindo a apresentação de um token, esse podendo ser concedido somente em certos aspectos por exemplo com o uso de um usuário e senha
No trecho abaixo importamos o pacote flask_jwt_extended do jwt_extended e com @jwt_required
limitamos o acesso ao método POST
from flask_jwt_extended import jwt_required
class response(Resource):
@jwt_required
def post(self):
return {'post':'Resposta de POST'}
Como resultado ao realizar a requisição POST sem o token, o sistema bloqueia com o erro interno Missing Authorization Header e retornando para o cliente Internal Server Error
post('http://localhost:5000/v1.0/posts').json()
# {'message': 'Internal Server Error'}
Para disponibilizar o token usamos a chave secreta <Chave Secreta JWT>
, cada sistema deve ter a sua própria
Podemos saber quem realizou a requisição usando o comando get_jwt_identity()
para ler a identidade do usuário, mas antes precisamos gerar o token com essa identidade pelo create_access_token
from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity, create_access_token
app.config['JWT_SECRET_KEY'] = '<Chave Secreta JWT>'
jwt = JWTManager(app)
class token(Resource):
def post(self):
data = request.json
if (('admin' == data['user']) & ('123' == data['pass'])):
current_user = get_jwt_identity()
access_token = create_access_token(identity = current_user)
return {'token': access_token}
else:
return {}
Com o trecho implantado, conseguimos acesso ao token com com um GET, informando o "user" e "pass" como "admin" e "123" respectivamente
post('http://localhost:5000/token', json={'user':'admin', 'pass':'123'}).json()
# {'token': '<token gerado>'}
A API retorna o token, uma sequência de caracteres que deve ser usada no header como uma autenticação Bearer, fazendo uso do pacote request novamente temos acesso ao POST da API
headers = {"Authorization":"Bearer <token gerado>"}
post('http://localhost:5000/v1.0/posts', headers=headers).json()
# {'post':'Resposta de POST'}
Criptografia Hash
No trecho anterior usamos como exemplo o usuário "admin" e senha "123", mas uma boa prática é evitar o uso de Text Plain para senhas uma ótima maneira é descaracterizar o conteúdo, com uma função hash
from passlib.hash import pbkdf2_sha256 as sha256
sha256.hash('123')
# <Sequência de caracteres Hash>
Alterando a validação de usuário e senha, substituindo o retorno do da função anterior em "<Sequência de caracteres Hash>"
if (('admin' == data['user']) & sha256.verify(data['pass'], '<Sequência de caracteres Hash>')):
Claro que este trecho funciona melhor com uma base de dados consultando o nome do usuário e a senha hash armazenada
Compilando
Para facilitar o código completo gerado nas etapas anteriores que devem conter no app.py para rodar com o 'flask run'
Não esquecendo de alterar a <Chave Secreta JWT>
para sua chave particular
import os
from flask import Flask, request
from flask_restful import Resource, Api
from flask_jwt_extended import JWTManager, jwt_required, get_jwt_identity, create_access_token
from passlib.hash import pbkdf2_sha256 as sha256
app = Flask(__name__)
api = Api(app)
app.config['JWT_SECRET_KEY'] = '<Chave Secreta JWT>'
jwt = JWTManager(app)
@app.route('/')
def start():
return 'Uma resposta do flask'
class response(Resource):
@jwt_required
def post(self):
return {'post':'Resposta de POST'}
@jwt_required
def put(self):
return {'put':'Resposta de PUT'}
@jwt_required
def get(self):
return {'get':'Resposta de GET'}
@jwt_required
def delete(self):
return {'delete':'Resposta de DELETE'}
class readJSON(Resource):
def post(self):
data = request.json
print('Name: ' + data['name'])
print('First Category: ' + data['categories'][0]['name'])
print('steps')
for step in data['steps']:
print(step['ingredient'])
class token(Resource):
def post(self):
data = request.json
if (('admin' == data['user']) & sha256.verify(data['pass'], '<Sequência de caracteres Hash>')):
current_user = get_jwt_identity()
access_token = create_access_token(identity = current_user)
return {'token': access_token}
else:
return {}
api.add_resource(token, '/token')
api.add_resource(readJSON, '/readjson')
api.add_resource(response, '/response')
if __name__ == '__main__':
port = int(os.environ.get("PORT", 5000))
app.run(host='0.0.0.0', port=port)