Taula de continguts

APIs amb Django

Les API o Application Programming Interface ens permeten accedir de forma controlada a les dades d'una aplicació web, típicament a través del format JSON, tot i que inicialment es fes servir XML.

Es basen en l'arquitectura software REST. Aquesta estableix estàndards per a HTTP que faciliten la interoperativitat, en particular:

Referències teoria i documentació:

, , , , , , , ,


APIs simples amb JsonResponse

Abans que res aclarir que per fer crides AJAX no cal tenir instal·lat Django API REST.

Aquestes crides AJAX es poden resoldre amb una simple JsonResponse.

Per exemple, seguint el tutorial de Django «polls», podem implementar una view que ens retorni les dades de les Question en format JSON d'aquesta manera. Per distingir de les views HTML farem un arxiu api.py que sabem que retornarà objectes JSON:

api.py
from django.http import JsonResponse
from polls.models import *
 
#...
 
def get_questions(request):
    jsonData = list( Question.objects.all().values() )
    return JsonResponse({
            "status": "OK",
            "questions": jsonData,
        }, safe=False)

Observa bé què cal per poder serialitzar els objectes del model:

jsonData = list( Question.objects.all().values() )
  1. Tenim un queryset de Questions
  2. Seleccionem només els valors amb .values()
  3. Transformem en una llista amb list( … )

Recorda que abans caldrà que implementis un endpoint, per exemple a /get_questions dins de urls.py:

urls.py
from django.urls import path
 
from . import views, api
 
urlpatterns = [
    path('', views.index, name='index'),
    #...
    path('get_questions', api.get_questions, name='get_questions'),
]

I podrem obtenir les dades des del browser o bé amb una crida per la shell:

$ curl localhost:8000/polls/get_questions
{"questions": [{"id": 1, "question_text": "Qu\u00e8 vols per sopar?", "pub_date": "2022-02-17T09:42:57Z"}, {"id": 2, "question_text": "Qui guanyar\u00e0 la lliga?", "pub_date": "2022-02-17T09:43:13Z"}]}

Si voleu un pretty print del JSON a la línia de comandes podeu fer:

$ curl localhost:8000/polls/get_questions | python3 -m json.tool

I per acabar-ho d'adobar, afegim colors amb pygmentize -l json:

$ curl localhost:8000/polls/get_questions | python3 -m json.tool | pygmentize -l json

El browser sempre és un tiro segur molt còmode:


Protegint els endpoints

Clar que d'aquesta manera el nostre «endpoint» quedarà exposat a qualsevol persona que vulgui accedir a la nostra BD. Si volem fer que només les persones loguejades puguin accedir a les dades de l'aplicació, podem protegir la nostra view molt simplement amb un decorator, en concret el @login_required decorator:

views.py
from django.contrib.auth.decorators import login_required
 
# ...
 
@login_required
def get_questions(request):
    qs = Question.objects.all().values()
    return JsonResponse({
            "status": "OK",
            "questions":list(qs)
        }, safe=False)

Comprova que via shell i la comanda curl ja no puc accedir. En canvi, amb el navegador, si em logo abans sí que podré accedir a les dades.

Decorator @user_passes_test

Existeixen altres decorators que ens faciliten fer un «tunning» més fi de les nostres views. En particular pots mirar el decorator user_passes_test per assegurar-te que l'usuari logat compleix amb certes condicions, com pertànyer a cert grup d'usuaris amb determinats privilegis.


Treballant APIs amb Django REST framework

El mètode de crear views amb JsonResponse vist abans ens permetrà crear APIs accessibles per la pròpia web a través d'AJAX, però no ens facilitarà accedir des d'aplicacions externes com aplicacions mòbils o altres webs.

La Django REST framework ens permetrà obrir les dades a altres aplicacions i dispositius amb seguretat.

Pots seguir el quickstart per agafar una idea ràpida de com utilitzar el REST Framework.

Exercici:

  1. Aplica el quickstart del Django REST Framework al tutorial «polls» oficial de Django.
  2. Crea serializers per als objectes User, Question i Choice.
  3. Realitza requests des de la línia de comandes amb curl per a llistar els diferents tipus objectes (llista complerta i detalls d'un en particular).


Autenticació i autorització

Una part important per a tractar una API és la autenticació (comprovació de identitat) i autorització (quins permisos té aquell usuari identificat).

Django REST Framework authentication reference.

Els dos models d'autenticació més importants són:


BasicAuthentication

Només recomanada per testing, i no per producció, ja que estem enviant sempre la contrasenya per validar-nos. Per defecte ja ve activada per a la configuració estàndard del REST Framework.

El típic exemple de request seria aquest:

curl -u admin:admin123 http://127.0.0.1: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://127.0.0.1:8000/api/users/

Per obtenir el hash en base64 es pot fer:

echo -n "admin:admin123" | openssl base64

O en la versió amb indentació o «pretty print» (això és una implementació particular de Django, en altres no funciona):

curl -H 'Accept: application/json; indent=4' -u admin:admin123 http://127.0.0.1:8000/api/users/

Exercici:

  1. Cerca com ha de ser una query per a enregistrar un nou usuari.
  2. Comprova que si no ens autentiquem no ens deixa enregistrar-lo.
    • Quin status code ens dóna amb l'error d'autenticació? (Utilitza la comanda amb el flag curl -i per visualitzar els headers amb la info).
  3. Comprova què passa si intentem insertar un User amb un username preexistent.
    • Quin status code ens dóna amb l'error?


TokenAuthentication

Aquest és el mètode estàndard per a producció. La idea és que no enviem la contrasenya contínuament, sinó que enviem un token. Per obtenir aquest sí que caldrà emprar la contrasenya, però només un cop mentre que en BasicAuthentication s'envia sempre augmentant les probabilitats de que algú pugui intervenir la contrasenya.

El TokenAuthentication estarà igualment exposat a robatori de sessió o session hijacking, però al menys no comprometem la contrasenya que és una dada sensible. Per tenir un grau mínim de seguretat i no estar exposat a robatoris de sessió és imprescindible emprar sempre HTTPS.

Seguim doc oficial: crear endpooint o URL per obtenir Token.

urls.py
from rest_framework.authtoken import views as apiviews
urlpatterns += [
    path('api-token-auth/', apiviews.obtain_auth_token)
]

Assegura't de tenir a settings.py:

settings.py
INSTALLED_APPS = [
    #...
    'rest_framework',
    'rest_framework.authtoken',
]
 
REST_FRAMEWORK = {
    #...
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication',
    ]
}

Per aconseguir el Token:

curl -X POST -d 'username=admin&password=admin123' http://127.0.0.1:8000/api-token-auth/

Per utilitzar el Token:

curl http://127.0.0.1:8000/api/example/ -H 'Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b'


Objectes aniuats

Exercici amb objectes aniuats:

  1. Crea els serializers per al tutorial oficial Django «polls», per als objectes Question i Choice.
  2. Configura el QuestionSerializer per tal que tingui aniuat («nested») les Choice, de manera que quan demanem una (o vàries) Question ens surti també la info de les seves Choice internes.


URLs personalitzades amb TokenAuthentication del REST framework

Si volem fer les nostres pròpies URLs per a la API i volem afegir-hi algun dels mètdodes d'autenticació del REST framework, ho podem fer així:

from rest_framework.authentication import TokenAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view, authentication_classes, permission_classes
from django.http import JsonResponse
 
@api_view(['GET'])
@authentication_classes([TokenAuthentication, BasicAuthentication])
@permission_classes([IsAuthenticated])
def pin_request(request):
    return JsonResponse({
        "usuari":request.user.username,
        "PIN":3142
        })
 
urlpatterns += [
    path('api/pin_request', pin_request )
]