Blog officiel du projet Web Apps Conception
layout: post title: “Comment développer une API Rest en Python ?” date: 2024-09-28 12:30:00 +0002 tags: python flask
REST (Representational State Transfer) ou RESTful est un style d’architecture permettant de construire des applications (Web, Intranet, Web Service). Il s’agit d’un ensemble de conventions et de bonnes pratiques à respecter et non d’une technologie à part entière. L’architecture REST utilise les spécifications originelles du protocole HTTP.
Flask-RESTful est une extension pour Flask qui ajoute la prise en charge de la création rapide d’API REST. Il s’agit d’une abstraction légère qui fonctionne avec vos ORM/bibliothèques existantes. Flask-RESTful encourage les meilleures pratiques avec une configuration minimale. Si vous connaissez Flask, Flask-RESTful devrait être facile à maîtriser.
pip install flask-restful
Dans ce post, nous n’allons pas nous attarder sur la documentation technique du module Flask-RESTful, mais plutôt aller à l’essentiel par un exemple.
from flask import Flask
from flask_restful import reqparse, abort, Api, Resource
app = Flask(__name__)
api = Api(app)
TODOS = {
'todo1': {'task': 'build an API'},
'todo2': {'task': '?????'},
'todo3': {'task': 'profit!'},
}
def abort_if_todo_doesnt_exist(todo_id):
if todo_id not in TODOS:
abort(404, message="Todo {} doesn't exist".format(todo_id))
parser = reqparse.RequestParser()
parser.add_argument('task')
# Todo
# shows a single todo item and lets you delete a todo item
class Todo(Resource):
def get(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
return TODOS[todo_id]
def delete(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
del TODOS[todo_id]
return '', 204
def put(self, todo_id):
args = parser.parse_args()
task = {'task': args['task']}
TODOS[todo_id] = task
return task, 201
# TodoList
# shows a list of all todos, and lets you POST to add new tasks
class TodoList(Resource):
def get(self):
return TODOS
def post(self):
args = parser.parse_args()
todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
todo_id = 'todo%i' % todo_id
TODOS[todo_id] = {'task': args['task']}
return TODOS[todo_id], 201
##
## Actually setup the Api resource routing here
##
api.add_resource(TodoList, '/todos')
api.add_resource(Todo, '/todos/<todo_id>')
if __name__ == '__main__':
app.run(debug=True)
💡 Le code de cet exemple est disponible sur Github.
$ python api.py
* Serving Flask app 'api'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 105-130-942
$ curl http://localhost:5000/todos
{
"todo1": {
"task": "build an API"
},
"todo2": {
"task": "?????"
},
"todo3": {
"task": "profit!"
}
}
$ curl http://localhost:5000/todos/todo3
{
"task": "profit!"
}
$ curl http://localhost:5000/todos/todo2 -X DELETE -v
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> DELETE /todos/todo2 HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 204 NO CONTENT
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 11:30:03 GMT
< Content-Type: application/json
< Connection: close
<
* Closing connection 0
$ curl http://localhost:5000/todos -d '{ "task":"something new" }' -X POST -H "Content-Type: application/json" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> POST /todos HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 26
>
* upload completely sent off: 26 out of 26 bytes
< HTTP/1.1 201 CREATED
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 11:37:29 GMT
< Content-Type: application/json
< Content-Length: 32
< Connection: close
<
{
"task": "something new"
}
* Closing connection 0
$ curl http://localhost:5000/todos/todo3 -d '{ "task":"something different" }' -X PUT -H "Content-Type: application/json" -v
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> PUT /todos/todo3 HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 32
>
* upload completely sent off: 32 out of 32 bytes
< HTTP/1.1 201 CREATED
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 11:40:06 GMT
< Content-Type: application/json
< Content-Length: 38
< Connection: close
<
{
"task": "something different"
}
* Closing connection 0
$ curl http://localhost:5000/todos
{
"todo1": {
"task": "build an API"
},
"todo3": {
"task": "something different"
},
"todo4": {
"task": "something new"
}
}
Afin de sécuriser votre application Flask, il est nécessaire de comprendre le fonctionnement d’une authentification basic JWT (JSON Web Token), ce qui permet l’échange sécurisé de jetons entre le serveur et les clients. Cette sécurité de l’échange se traduit par la vérification de l’intégrité et de l’authenticité des données. Elle s’effectue par l’algorithme HMAC ou RSA.
Le site JWT.io permet de décoder, vérifier et générer JWT.
Flask-JWT-Extended ajoute non seulement la prise en charge de l’utilisation des jetons Web JSON (JWT) à Flask pour protéger les vues, mais également de nombreuses fonctionnalités utiles (et facultatives) intégrées pour faciliter l’utilisation des jetons Web JSON. Ceux-ci incluent :
pip install flask-jwt-extended
Dans ce post, nous n’allons pas nous attarder sur la documentation technique du module Flask-JWT-Extended, mais plutôt aller à l’essentiel par un exemple.
from flask import Flask
from flask import jsonify
from flask import request
from flask_jwt_extended import create_access_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager
app = Flask(__name__)
# Setup the Flask-JWT-Extended extension
app.config["JWT_SECRET_KEY"] = "super-secret" # Change this!
jwt = JWTManager(app)
# Create a route to authenticate your users and return JWTs. The
# create_access_token() function is used to actually generate the JWT.
@app.route("/login", methods=["POST"])
def login():
username = request.json.get("username", None)
password = request.json.get("password", None)
if username != "test" or password != "test":
return jsonify({"msg": "Bad username or password"}), 401
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
# Protect a route with jwt_required, which will kick out requests
# without a valid JWT present.
@app.route("/protected", methods=["GET"])
@jwt_required()
def protected():
# Access the identity of the current user with get_jwt_identity
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
if __name__ == "__main__":
app.run()
💡 Le code de cet exemple est disponible sur Github.
$ python api.py
* Serving Flask app 'api'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
$ curl http://localhost:5000/protected -v
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /protected HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 401 UNAUTHORIZED
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 13:39:15 GMT
< Content-Type: application/json
< Content-Length: 39
< Connection: close
<
{"msg":"Missing Authorization Header"}
* Closing connection 0
Pour accéder à une vue protégée jwt_required, vous devez envoyer le JWT avec chaque demande.
$ curl http://localhost:5000/login -d '{ "username":"test", "password":"test" }' -X POST -H "Content-Type: application/json" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> POST /login HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 40
>
* upload completely sent off: 40 out of 40 bytes
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 13:40:18 GMT
< Content-Type: application/json
< Content-Length: 349
< Connection: close
<
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYxNzIxOCwianRpIjoiZDE5OGVjZWYtYTNkMC00YzAzLWI3OGEtMDQ5NDBkODEzNmEyIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MTcyMTgsImNzcmYiOiIyZjdhOGNkOC1mNzMwLTQ5MjEtYjE1NC03MzBiZDk1Zjg5NWMiLCJleHAiOjE3Mjc2MTgxMTh9.dhMvJ9Osu2ddAxXQerDzqIzO7madi887txr8ybstDZ0"}
* Closing connection 0
Par défaut, cela se fait avec une entête d’autorisation qui ressemble à : Authorization: Bearer <access_token>
$ export JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYxNzIxOCwianRpIjoiZDE5OGVjZWYtYTNkMC00YzAzLWI3OGEtMDQ5NDBkODEzNmEyIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MTcyMTgsImNzcmYiOiIyZjdhOGNkOC1mNzMwLTQ5MjEtYjE1NC03MzBiZDk1Zjg5NWMiLCJleHAiOjE3Mjc2MTgxMTh9.dhMvJ9Osu2ddAxXQerDzqIzO7madi887txr8ybstDZ0"
$ curl http://localhost:5000/protected -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" -v
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /protected HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYxNzIxOCwianRpIjoiZDE5OGVjZWYtYTNkMC00YzAzLWI3OGEtMDQ5NDBkODEzNmEyIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MTcyMTgsImNzcmYiOiIyZjdhOGNkOC1mNzMwLTQ5MjEtYjE1NC03MzBiZDk1Zjg5NWMiLCJleHAiOjE3Mjc2MTgxMTh9.dhMvJ9Osu2ddAxXQerDzqIzO7madi887txr8ybstDZ0
> Content-Type: application/json
>
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 13:47:25 GMT
< Content-Type: application/json
< Content-Length: 24
< Connection: close
<
{"logged_in_as":"test"}
* Closing connection 0
Afin de sécuriser notre API Flask-RESTful, appliquons la sécurisation JWT à notre exemple :
from flask import Flask
from flask import jsonify
from flask import request
from flask_restful import reqparse, abort, Api, Resource
from flask_jwt_extended import create_access_token
from flask_jwt_extended import get_jwt_identity
from flask_jwt_extended import jwt_required
from flask_jwt_extended import JWTManager
app = Flask(__name__)
# Setup the Flask-JWT-Extended extension
app.config["JWT_SECRET_KEY"] = "super-secret" # Change this!
jwt = JWTManager(app)
api = Api(app)
TODOS = {
'todo1': {'task': 'build an API'},
'todo2': {'task': '?????'},
'todo3': {'task': 'profit!'},
}
def abort_if_todo_doesnt_exist(todo_id):
if todo_id not in TODOS:
abort(404, message="Todo {} doesn't exist".format(todo_id))
parser = reqparse.RequestParser()
parser.add_argument('task')
# Todo
# shows a single todo item and lets you delete a todo item
class Todo(Resource):
@jwt_required()
def get(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
return TODOS[todo_id]
@jwt_required()
def delete(self, todo_id):
abort_if_todo_doesnt_exist(todo_id)
del TODOS[todo_id]
return '', 204
@jwt_required()
def put(self, todo_id):
args = parser.parse_args()
task = {'task': args['task']}
TODOS[todo_id] = task
return task, 201
# TodoList
# shows a list of all todos, and lets you POST to add new tasks
class TodoList(Resource):
@jwt_required()
def get(self):
return TODOS
@jwt_required()
def post(self):
args = parser.parse_args()
todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
todo_id = 'todo%i' % todo_id
TODOS[todo_id] = {'task': args['task']}
return TODOS[todo_id], 201
##
## Actually setup the Api resource routing here
##
api.add_resource(TodoList, '/todos')
api.add_resource(Todo, '/todos/<todo_id>')
# Create a route to authenticate your users and return JWTs. The
# create_access_token() function is used to actually generate the JWT.
@app.route("/login", methods=["POST"])
def login():
username = request.json.get("username", None)
password = request.json.get("password", None)
if username != "test" or password != "test":
return jsonify({"msg": "Bad username or password"}), 401
access_token = create_access_token(identity=username)
return jsonify(access_token=access_token)
# Protect a route with jwt_required, which will kick out requests
# without a valid JWT present.
@app.route("/protected", methods=["GET"])
@jwt_required()
def protected():
# Access the identity of the current user with get_jwt_identity
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
if __name__ == '__main__':
app.run(debug=True)
💡 Le code de cet exemple est disponible sur Github.
$ python api-jwt.py
* Serving Flask app 'api-jwt'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 105-130-942
$ curl http://localhost:5000/todos
{
"msg": "Missing Authorization Header"
}
📝 Nous pouvons constater que nous n’avons plus accès directement à la route
/todos
.
Pour accéder à une vue protégée jwt_required, vous devez envoyer le JWT avec chaque demande :
$ curl http://localhost:5000/login -d '{ "username":"test", "password":"test" }' -X POST -H "Content-Type: application/json" -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> POST /login HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 40
>
* upload completely sent off: 40 out of 40 bytes
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 14:37:44 GMT
< Content-Type: application/json
< Content-Length: 354
< Connection: close
<
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYyMDY2NCwianRpIjoiMDVkNzMxOGEtZjdmMC00MzQ5LTk5ZjgtNjk3NTAxMjMwMmE3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MjA2NjQsImNzcmYiOiIyYmJjY2JjMi1kZTk0LTQ0OGQtYTEyYS0wZDM4MzQxYjJjNDIiLCJleHAiOjE3Mjc2MjE1NjR9.UBOgw3I_lm48xcnO2hO1CA3XT6D6HDOWxX0knG8bDHU"
}
* Closing connection 0
Avec l’intégration de la Token dans l’entête d’autorisation, vous pouvez accéder à la ressource demandée :
$ export JWT="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYyMDY2NCwianRpIjoiMDVkNzMxOGEtZjdmMC00MzQ5LTk5ZjgtNjk3NTAxMjMwMmE3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MjA2NjQsImNzcmYiOiIyYmJjY2JjMi1kZTk0LTQ0OGQtYTEyYS0wZDM4MzQxYjJjNDIiLCJleHAiOjE3Mjc2MjE1NjR9.UBOgw3I_lm48xcnO2hO1CA3XT6D6HDOWxX0knG8bDHU"
$ curl http://localhost:5000/todos -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" -v
* Trying ::1...
* TCP_NODELAY set
* connect to ::1 port 5000 failed: Connexion refusée
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> GET /todos HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.0
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTcyNzYyMDY2NCwianRpIjoiMDVkNzMxOGEtZjdmMC00MzQ5LTk5ZjgtNjk3NTAxMjMwMmE3IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InRlc3QiLCJuYmYiOjE3Mjc2MjA2NjQsImNzcmYiOiIyYmJjY2JjMi1kZTk0LTQ0OGQtYTEyYS0wZDM4MzQxYjJjNDIiLCJleHAiOjE3Mjc2MjE1NjR9.UBOgw3I_lm48xcnO2hO1CA3XT6D6HDOWxX0knG8bDHU
> Content-Type: application/json
>
< HTTP/1.1 200 OK
< Server: Werkzeug/2.2.3 Python/3.7.3
< Date: Sun, 29 Sep 2024 14:39:22 GMT
< Content-Type: application/json
< Content-Length: 150
< Connection: close
<
{
"todo1": {
"task": "build an API"
},
"todo2": {
"task": "?????"
},
"todo3": {
"task": "profit!"
}
}
* Closing connection 0