Taula de continguts

APIs amb Django Ninja

Django Ninja és una llibreria per a fer APIs alternativa a la clàssica Django REST Framework (que podeu veure a l'article Django API) i fortament inspirada en la molt adoptada recentment FastAPI.

El principal avantatge és certa simplicitat per crear els endpoints.

Referències:

, , , , , , , , ,


Quickstart

És relativament fàcil acostar-se a Django Ninja amb la seva documentació:


Autenticació

Destaquem dos mètodes d'autenticació molt estandarditzats:

Es poden implementar les dues amb aquest codi:

api.py
from django.contrib.auth import authenticate as django_authenticate
from ninja import NinjaAPI, Schema
from ninja.security import HttpBasicAuth, HttpBearer
from typing import List, Optional
from .models import *
import secrets
 
api = NinjaAPI()
 
# Autenticació bàsica
class BasicAuth(HttpBasicAuth):
    def authenticate(self, request, username, password):
        user = django_authenticate(username=username, password=password)
        if user:
            # Genera un token simple
            token = secrets.token_hex(16)
            user.auth_token = token
            user.save()
            return token
        return None
 
# Autenticació per Token Bearer
class AuthBearer(HttpBearer):
    def authenticate(self, request, token):
        try:
            user = Usuari.objects.get(auth_token=token)
            return user
        except Usuari.DoesNotExist:
            return None
 
# Endpoint per obtenir un token, accés amb BasicAuth
# amb o sense "trailing slash"
@api.get("/token", auth=BasicAuth())
@api.get("/token/", auth=BasicAuth())
def obtenir_token(request):
    return {"token": request.auth}
 
# Exemple d'endpoint per llistar els llibres, accés amb TokenAuth
class LlibreOut(Schema):
    id: int
    titol: str
    editorial: Optional[str]
 
@api.get("/llibres/", response=List[LlibreOut], auth=AuthBearer())
def get_llibres(request):
    qs = Llibre.objects.all()
    return qs


Ampliació del model d'usuari

Per tal que la Token Auth funcioni, ens caldrà que l'usuari emmagatzemi els tokens en algun lloc.

Es pot fer de diverses maneres, una d'elles és afegir la columna auth_token al model Usuari que cal personalitzar, derivant de AbstractUser.

Ull, perquè aquest canvi tindrà implicacions profundes i ens obligarà a esborrar la base de dades i recomençar l'historial de migracions. Ho veurem mes tard.

Comencem per canviar el model d'usuari i el redefinim a models.py:

models.py
from django.contrib.auth.models import AbstractUser
 
class Usuari(AbstractUser):
    auth_token = models.CharField(max_length=32,blank=True,null=True)
    # + altres atributs que es vulguin afegir...

I a settings.py (ULL, canviar <myapp> pel nom de la vostra app):

settings.py
AUTH_USER_MODEL = '<myapp>.Usuari'

Per tal que els canvis es guardin a la base de dades caldrà crear i executar les migracions adequades:

(env) $ ./manage.py makemigrations
(env) $ ./manage.py migrate

Com que hem modificat l'usuari del sistema, ara no es visualitzarà a l'admin panel. Per visualitzar-lo correctament, apliqueu aquest UsuariAdmin a admin.py:

admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
 
class UsuariAdmin(UserAdmin):
    fieldsets = UserAdmin.fieldsets + (
            ("Altres dades (API auth)", {
                'fields': ('auth_token',),
            }),
    )
    readonly_fields = ["auth_token",]
 
admin.site.register(Usuari,UsuariAdmin)

Afegir canvis en el model d'usuari quan un projecte ja té dades de producció és problemàtic, com a mínim. Es recomana sempre crear un model d'usuari personalitzat (derivat d'AbstractUser) des del principi del projecte per poder afegir funcionalitats com aquesta (afegir token a l'usuari) quan sigui necessari.

Si no tenim dades de producció, el més fàcil és esborrar la base de dades i arxius de migracions, i tornar a crear-los.

(env) $ rm db.sqlite3
(env) $ rm -rf <myapp>/migrations
(env) $ ./manage.py makemigrations <myapp>
(env) $ ./manage.py migrate

Una possible alternativa, per tal de no haver de crear un model d'usuari personalitzat, és crear una taula de tokens a part, i relacionar-la amb el User estàndard de Django. Igualment es sol recomanar prioritàriament particularitzar el model d'usuari, ja que les situacions en les que interessarà ampliar-lo son molt comunes.


Mes curl

El típic exemple de request amb Basic Authentication seria:

curl -u admin:admin123 http://localhost:8000/api/users/

De vegades ens pot interessar una alternativa enviant les dades codificades en base64 en els headers:

curl -H 'Authorization:Basic YWRtaW46YWRtaW4xMjM=' http://localhost:8000/api/users/

Per obtenir el hash en base64 es pot fer:

echo -n "admin:admin123" | base64

Per aconseguir un pretty print del JSON es pot fer amb la utilitat jq

curl -u admin:admin123 http://localhost:8000/api/users/ | jq

Per instal·lar jq es pot fer:

sudo apt install jq


CORS : cridar l'API des de dominis creuats

Passa sovint que el desplegament d'una API és independent del frontend, on es pot utilitzar altres frameworks com React o AngularJS.

Una mesura bàsica de seguretat és que el navegador web es negui a fer crides a APIs d'un altre dominis per evitar possibles injeccions de codi que treguin les nostres dades personals cap a dominis externs.

Disposem d'una eina Django CORS Headers amb la que podem avisar Django que hi ha determinats dominis que sí poden fer crides a l'API.

(env) $ pip install django-cors-headers

Bàsicament caldrà instal·lar el plugin django-cors-headers i ajustar les variables a settings.py: