Crear una Api Restful en Flask con autenticación requerida

- Andrés Cruz

In english
Crear una Api Restful en Flask con autenticación requerida

Una Api Rest es una estructura en base a funciones que son muy empleadas hoy en dia para comunicar sistemas o distintas aplicaciones, cada recurso que podemos crear son simplemente funciones que a la final son traducidas mediante URLs que son consumidas vía HTTP y los métodos del mismo:

  1. Get Obtener
  2. Post Crear
  3. Put o Patch Actualizar
  4. Delete Borrar

Por lo tanto, dependiendo del método HTTP nosotros podemos dar una respuesta distinta, siempre respetando el propósito de cada tipo de petición HTTP.

Herramientas necesarias

Así que, para trabajar con este paquete, lo primero que necesitamos es nuestra aplicacion en Flask y luego instalar el paquete para el Restful mediante:

pip install flask-restful

Ahora ya estamos listos para poder emplear este paquete.

Vamos a crear un par de clases por cada entidad que queramos hacer gestionables es decir realizar el CRUD sobre algún modelo que recuerda que un modelo es nuestra conexión con una tabla en la base de datos.

Antes de comenzar

Recuerda que aquí no vamos a tratar como trabajar con SQLAlchemy para realizar las conexiones sobre nuestra base de datos con nuestro proyecto en Flask, simplemente vamos a realizar las consultas tipo CRUD para operar la base de datos.

Seguramente te estás preguntando porqué necesitamos dos clases y no solo una, la respuesta se debe a la definición de en las rutas, por lo tanto vamos a tener:

  1. Una clase para los métodos que NO necesiten de una pk o id para poder operar
  2. Una clase para los métodos que SÍ necesiten de una pk o id para poder operar

Por lo tanto las rutas y configuración que hacemos quedan así:

from flask_restful import Api
from proyect_app.rest.controllers import BookApi, BookApiId
 
api = Api(app) #, decorators=[csrf.exempt]
api.add_resource(BookApi, '/api/book')
api.add_resource(BookApiId, '/api/book/<string:id>')

Así que vamos a crear una para trabajar sin los IDs o PKs; como puedes suponer, esto serían para los métodos de Post, para crear un registro y para get, para obtener TODOS los registros.

Vamos a trabajar con el siguiente modelo:

class Category(db.Model):
   __tablename__ = 'categories'
   id = db.Column(db.Integer, primary_key=True)
   name = db.Column(db.String(255))

Funcionalidades adicionales sobre nuestro paquete

Nuestro paquete para lograr una Api Restful, trae varios elementos interesantes que nos facilitan bastante la vida para crear este tipo de APIs tipo CRUD; 

Obtener los datos del request

El primer elemento sería un sencillo mecanismo para obtener los datos del request fácilmente mediante un diccionario:

Conversión automática de objeto a diccionario

Otro elemento muy interesante es que nosotros tenemos un decorador llamado marshal_with que nos permite indicar de una que campos queremos devolver del modelo con el cual estamos trabajando cada vez que retornamos el objeto; para eso primero tenemos que definir la estructura que sostenga lo anteriormente comentado:

resource_fields = {
   'id': fields.Integer,
   'name' : fields.String
}

Este decorador también recibe de manera opcional el envoltorio del json que va a devolver que podemos configurar mediante el envelope.

Otras funcionalidades 

Vamos a crear una clase auxiliar que nos permitirá agregar algunas funcionalidades adicionales sobre las clases tipo rest; por ejemplo, la de protección con login.

Y aquí definir nuestros métodos

Método tipo get

   @marshal_with(resource_fields, envelope="categorias")
   def get(self):
       return Category.query.all()

Con esta función vamos a obtener todos los registros de nuestra base de datos:

Método tipo post

   @marshal_with(resource_fields, envelope="categoria")
   def post(self):
      
       parser = reqparse.RequestParser()
       parser.add_argument('name',required=True, help="No mandastes el nombre")
       args = parser.parse_args()
 
       c = Category(args['name'])
       db.session.add(c)
       db.session.commit()
 
       return c

Aquí vamos a trabajar para crear un registro, así que vamos a crear otra clase que SI van a trabajar con los IDs:

Método tipo get

   @auth.login_required
   @marshal_with(resource_fields, envelope="categoria")
   def get(self, id):
       return self.abort_if_doesnt_exist(id,False)

Aquí vamos a devolver un registro por su identificador:

Método tipo path o put

   @marshal_with(resource_fields, envelope="categoria")
   def patch(self, id):
 
       parser = reqparse.RequestParser()
       parser.add_argument('name', required=True, help="No mandastes el nombre")
       args = parser.parse_args()
       c = self.abort_if_doesnt_exist(id, False)
       c.name = args['name']
       db.session.add(c)
       db.session.commit()
 
       return self.abort_if_doesnt_exist(c.id,False)

Método tipo delete

   def delete(self, id):
 
       c = self.abort_if_doesnt_exist(id, False)
 
       db.session.delete(c)
       db.session.commit()
 
       return {'msj':'ok'}

Finalmente, todo el código de ambas clases:

 
from flask import request
from flask_restful import Resource, abort, reqparse, fields, marshal_with
from flask_httpauth import HTTPBasicAuth
 
from my_app.product.models import Category
from my_app.auth.model.user import User
from my_app import user_manager
from my_app import db
 
resource_fields = {
   'id': fields.Integer,
   'name' : fields.String
}
 
auth = HTTPBasicAuth()
 
@auth.verify_password
def verify_password(username, password):
   user = User.query.filter_by(username=username).first()
 
   if not user or not user_manager.verify_password(password_hash=user.password, password=password):
       return False
   return True
 
class Base:
   def category_to_json(self,category):
       return {
           'id': category.id,
           'name': category.name,
       }
 
   def abort_if_doesnt_exist(self, id, json=True):
       category = Category.query.get(id)
       if category is None:
           abort(404, message="Categoría {} no existe".format(id))
       if json:
           return self.category_to_json(category)
 
       return category
 
class CategoryRestFul(Resource,Base):
 
   @auth.login_required
   @marshal_with(resource_fields, envelope="categoria")
   def get(self, id):
       return self.abort_if_doesnt_exist(id,False)
 
   @marshal_with(resource_fields, envelope="categoria")
   def patch(self, id):
 
       parser = reqparse.RequestParser()
       parser.add_argument('name', required=True, help="No mandastes el nombre")       args = parser.parse_args()
 
       c = self.abort_if_doesnt_exist(id, False)
 
       c.name = args['name'] #request.form
 
       db.session.add(c)
       db.session.commit()
 
       return self.abort_if_doesnt_exist(c.id,False)
 
   def delete(self, id):
 
       c = self.abort_if_doesnt_exist(id, False)
 
       db.session.delete(c)
       db.session.commit()
 
       return {'msj':'ok'}
 
 
class CategoryRestFulList(Resource,Base):
 
   @marshal_with(resource_fields, envelope="categorias")
   def get(self):
       return Category.query.all()
 
   @marshal_with(resource_fields, envelope="categoria")
   def post(self):
      
       parser = reqparse.RequestParser()
       parser.add_argument('name',required=True, help="No mandastes el nombre")
       args = parser.parse_args()
 
       c = Category(args['name'])
       db.session.add(c)
       db.session.commit()
 
       return c
 
class CategoryRestFulListSearch(Resource,Base):
 
   @marshal_with(resource_fields, envelope="categoria")
   def get(self):
 
       parser = reqparse.RequestParser()
       parser.add_argument('search',required=True, help="Tienes que especificar la busqueda")
       args = parser.parse_args()
 
       return Category.query.filter(Category.name.like('%{0}%'.format(args['search']))).all()

Puntos a tener en cuenta

Como puedes ver, las operaciones que hacemos en la Rest Api no tienen nada de raro, algunas validaciones, operaciones sobre la base de datos y poco más, pero como puedes ver, tenemos un mecanismo bastante práctico para manipular u obtener los datos de nuestro usuario; los datos que nos pasa vía un formulario, una petición axio, o cualquier Api que emplees para mandar datos vía HTTP, los obtenemos mediante la clase de RequestParser; lo ideal de esto, es que nosotros podemos definir validaciones en la eficiente de la función add_argument y por lo tanto podemos estar 100 por ciento seguro que los datos que estamos trabajando son válidos.

Autenticación requerida en la Restful con Flask

Como puedes ver, estamos empleando una función que a la final funciona como decorador sobre nuestros recursos Rest para hacer la autenticación requerida sobre nuestra Rest Api; esto es un paquete de un tercero que puedes adaptar a otro tipo de servicios pero que funciona bastante bien con Flask Restful; simplemente tenemos que definir una función base en la cual realizamos las operaciones sobre nuestro modelo de usuarios, sea lo que sea que quieras verificar en la fase de autenticación requerida de la Rest Api lo puedes definir en esa función.

En este ejemplo tenemos un sencillo ejemplo en el cual buscamos primero por el username y su existe luego verificamos la contraseña que están en formato hash en la base de datos, por lo tanto, es una función que se encarga de hacer un login manual, y que empleamos perfectamente en nuestra Rest Api como autenticación requerida.

@auth.verify_password
def verify_password(username, password):
   user = User.query.filter_by(username=username).first()
 
   if not user or not user_manager.verify_password(password_hash=user.password, password=password):
       return False
   return True

Recuerda que para emplear este paquete de autenticación tienes que instalarlo con:

pip install Flask-HTTPAuth
Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.