Xây base project api bằng flask - phần 2
Bài đăng này đã không được cập nhật trong 4 năm
Để tiếp nối tiếp bài viết ở phần 1 https://viblo.asia/p/xay-base-project-api-bang-flask-bJzKmPLP59N, hôm nay mình sẽ tiếp tục viết tiếp phần 2 của bài Xây base project api bằng flask
.
Nội dung phần này gồm có:
- Security and Authentication (Api Login, Logout)
- Unit test
- Sử dụng make để tạo alias cho câu lệnh
Security and Authentication
Let’s create a model blacklist Token for storing blacklisted tokens. In the models package, create a blacklist.py file with the following content:
Tạo một model blacklistToken
, trong models package này tạo một file là blacklist.py
, file này có nội dung như sau:
from .. import db
import datetime
class BlacklistToken(db.Model):
"""
Token Model for storing JWT tokens
"""
__tablename__ = 'blacklist_tokens'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
token = db.Column(db.String(500), unique=True, nullable=False)
blacklisted_on = db.Column(db.DateTime, nullable=False)
def __init__(self, token):
self.token = token
self.blacklisted_on = datetime.datetime.now()
def __repr__(self):
return '<id: token: {}'.format(self.token)
@staticmethod
def check_blacklist(auth_token):
# check whether auth token has been blacklisted
res = BlacklistToken.query.filter_by(token=str(auth_token)).first()
if res:
return True
else:
return False
Import dòng sau vào manage.py rồi chạy migration:
`from app.main.model import blacklist` # import vào manage.py
python manage.py db migrate --message 'add blacklist table'
python manage.py db upgrade
Tiếp theo tạo file blacklist_service.py
trong service module:
from app.main import db
from app.main.model.blacklist import BlacklistToken
def save_token(token):
blacklist_token = BlacklistToken(token=token)
try:
# insert the token
db.session.add(blacklist_token)
db.session.commit()
response_object = {
'status': 'success',
'message': 'Successfully logged out.'
}
return response_object, 200
except Exception as e:
response_object = {
'status': 'fail',
'message': e
}
return response_object, 200
update users
model với các function như dưới:
...
import datetime
import jwt
from app.main.model.blacklist import BlacklistToken
from ..config import key
...
Thêm functions encoding
và decoding
:
def encode_auth_token(self, user_id):
"""
Generates the Auth Token
:return: string
"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1, seconds=5),
'iat': datetime.datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
key,
algorithm='HS256'
)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
Decodes the auth token
:param auth_token:
:return: integer|string
"""
try:
payload = jwt.decode(auth_token, key)
is_blacklisted_token = BlacklistToken.check_blacklist(auth_token)
if is_blacklisted_token:
return 'Token blacklisted. Please log in again.'
else:
return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
Mọi việc chuẩn bị đã xong, bây giờ chúng ta sẽ tạo api login và logout.
Trước tiền cần thêm dto
cho users:
//dto.py - file này đã được tạo ở phần 1.
class AuthDto:
api = Namespace('auth', description='authentication related operations')
user_auth = api.model('auth_details', {
'email': fields.String(required=True, description='The email address'),
'password': fields.String(required=True, description='The user password '),
})
Tạo auth_helper.py
trong module service:
from app.main.model.user import User
from ..service.blacklist_service import save_token
class Auth:
@staticmethod
def login_user(data):
try:
# fetch the user data
user = User.query.filter_by(email=data.get('email')).first()
if user and user.check_password(data.get('password')):
auth_token = user.encode_auth_token(user.id)
if auth_token:
response_object = {
'status': 'success',
'message': 'Successfully logged in.',
'Authorization': auth_token.decode()
}
return response_object, 200
else:
response_object = {
'status': 'fail',
'message': 'email or password does not match.'
}
return response_object, 401
except Exception as e:
print(e)
response_object = {
'status': 'fail',
'message': 'Try again'
}
return response_object, 500
@staticmethod
def logout_user(data):
if data:
auth_token = data.split(" ")[1]
else:
auth_token = ''
if auth_token:
resp = User.decode_auth_token(auth_token)
if not isinstance(resp, str):
# mark the token as blacklisted
return save_token(token=auth_token)
else:
response_object = {
'status': 'fail',
'message': resp
}
return response_object, 401
else:
response_object = {
'status': 'fail',
'message': 'Provide a valid auth token.'
}
return response_object, 403
Cuối cùng tạo auth_controller.py
: với nội dung sau:
from flask import request
from flask_restplus import Resource
from app.main.service.auth_helper import Auth
from ..util.dto import AuthDto
api = AuthDto.api
user_auth = AuthDto.user_auth
@api.route('/login')
class UserLogin(Resource):
"""
User Login Resource
"""
@api.doc('user login')
@api.expect(user_auth, validate=True)
def post(self):
# get the post data
post_data = request.json
return Auth.login_user(data=post_data)
@api.route('/logout')
class LogoutAPI(Resource):
"""
Logout Resource
"""
@api.doc('logout a user')
def post(self):
# get auth token
auth_header = request.headers.get('Authorization')
return Auth.logout_user(data=auth_header)
Update nội dung trong __init__.py
của app
package với nội dung sau:
# app/__init__.py
from flask_restplus import Api
from flask import Blueprint
from .main.controller.user_controller import api as user_ns
from .main.controller.auth_controller import api as auth_ns
blueprint = Blueprint('api', __name__)
api = Api(blueprint,
title='FLASK RESTPLUS API BOILER-PLATE WITH JWT',
version='1.0',
description='a boilerplate for flask restplus web service'
)
api.add_namespace(user_ns, path='/user')
api.add_namespace(auth_ns)
đến bước này bạn chạy lệnh python manage.py run
và mở http://127.0.0.1:5000
lên sẽ thấy 2 api login và logout xuất hiện trong docs
Trước khi test được chúng ta sửa một chút phần user_service.py
đã được tạo trong phần 1, thêm vào nội dung sau
//user_service.py
def generate_token(user):
try:
# generate the auth token
auth_token = user.encode_auth_token(user.id)
response_object = {
'status': 'success',
'message': 'Successfully registered.',
'Authorization': auth_token.decode()
}
return response_object, 201
except Exception as e:
response_object = {
'status': 'fail',
'message': 'Some error occurred. Please try again.'
}
return response_object, 401
Và cùng file user_service.py
này sửa hàm save_new_user
, replace block
response_object = {
'status': 'success',
'message': 'Successfully registered.'
}
return response_object, 201
thành
return generate_token(new_user)
Unit test
Chúng ta sẽ viết unit test cho api login và logout vừa tạo ở trên
Tạo file test_auth.py
trong test package
với nội dung như sau:
import unittest
import json
from app.test.base import BaseTestCase
def register_user(self):
return self.client.post(
'/user/',
data=json.dumps(dict(
email='example@gmail.com',
username='username',
password='123456'
)),
content_type='application/json'
)
def login_user(self):
return self.client.post(
'/auth/login',
data=json.dumps(dict(
email='example@gmail.com',
password='123456'
)),
content_type='application/json'
)
class TestAuthBlueprint(BaseTestCase):
def test_registered_user_login(self):
""" Test for login of registered-user login """
with self.client:
# user registration
user_response = register_user(self)
response_data = json.loads(user_response.data.decode())
self.assertTrue(response_data['Authorization'])
self.assertEqual(user_response.status_code, 201)
# registered user login
login_response = login_user(self)
data = json.loads(login_response.data.decode())
self.assertTrue(data['Authorization'])
self.assertEqual(login_response.status_code, 200)
def test_valid_logout(self):
""" Test for logout before token expires """
with self.client:
# user registration
user_response = register_user(self)
response_data = json.loads(user_response.data.decode())
self.assertTrue(response_data['Authorization'])
self.assertEqual(user_response.status_code, 201)
# registered user login
login_response = login_user(self)
data = json.loads(login_response.data.decode())
self.assertTrue(data['Authorization'])
self.assertEqual(login_response.status_code, 200)
# valid token logout
response = self.client.post(
'/auth/logout',
headers=dict(
Authorization='Bearer ' + json.loads(
login_response.data.decode()
)['Authorization']
)
)
data = json.loads(response.data.decode())
self.assertTrue(data['status'] == 'success')
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()
Route protection and Authorization
chúng ta đã tạo các enpoint api, bao gồm cả auth, nhưng những enpoint này chưa được bảo vệ, users chưa cần login vẫn có thể call được, phần này sẽ viết decorators
để kiểm tra auth token để bảo vệ api của chúng ta
Trong auth_helper.py
thêm static function kiểm tra authen token
@staticmethod
def get_logged_in_user(new_request):
# get the auth token
auth_token = new_request.headers.get('Authorization')
if auth_token:
resp = User.decode_auth_token(auth_token)
if not isinstance(resp, str):
user = User.query.filter_by(id=resp).first()
response_object = {
'status': 'success',
'data': {
'user_id': user.id,
'email': user.email,
'admin': user.admin,
'registered_on': str(user.registered_on)
}
}
return response_object, 200
response_object = {
'status': 'fail',
'message': resp
}
return response_object, 401
else:
response_object = {
'status': 'fail',
'message': 'Provide a valid auth token.'
}
return response_object, 401
Tiếp tục tạo file decorator.py
trong package util
from functools import wraps
from flask import request
from app.main.service.auth_helper import Auth
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
data, status = Auth.get_logged_in_user(request)
token = data.get('data')
if not token:
return data, status
return f(*args, **kwargs)
return decorated
def admin_token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
data, status = Auth.get_logged_in_user(request)
token = data.get('data')
if not token:
return data, status
admin = token.get('admin')
if not admin:
response_object = {
'status': 'fail',
'message': 'admin token required'
}
return response_object, 401
return f(*args, **kwargs)
return decorated
Như vậy là toàn bộ api của chúng ta đã được bảo vệ
Bonus
sử dụng make
để rút ngắn câu lệnh cần gõ (tạo alias). bạn cần cài đặt make
trên máy tính của bạn
Tạo một file có tên là Makefile
trong root folder của project:
.PHONY: clean system-packages python-packages install tests run all
clean:
find . -type f -name '*.pyc' -delete
find . -type f -name '*.log' -delete
system-packages:
sudo apt install python-pip -y
python-packages:
pip install -r requirements.txt
install: system-packages python-packages
tests:
python manage.py test
run:
python manage.py run
all: clean install tests run
- make install : cài đặt system-packages and python-packages
- make clean : cleans up the app
- make tests : chạy tất cả test
- make run : starts the application
- make all : performs clean-up, installation , run tests , and starts the app.
Bạn có thể tham khảo toàn bộ nội dung code trong repo này:
https://github.com/cosmic-byte/flask-restplus-boilerplate
Tham khảo
All rights reserved