|
|||||||
Аутентификация в REST API на Flask
Время создания: 11.01.2018 11:08
Автор: br0ke
Текстовые метки: python, flask, api, rest, restful, python-restful, auth, authentication, authoriazation, httpauth, http, flask-httpauth, basic, digest
Раздел: Информационные технологии - Python - Библиотеки - Flask
Запись: and-semakin/mytetra_data/master/base/1515650921dnk0yq96no/text.html на raw.githubusercontent.com
|
|||||||
|
|||||||
Статья, которой я пользовался: https://blog.miguelgrinberg.com/post/restful-authentication-with-flask (тоже должна быть сохранена в MyTetra). Репозиторий, которым я пользовался как примером: https://github.com/joshfriend/flask-restful-demo REST API строится с использованием библиотеки Flask-RESTful. HTTP Basic Auth Самый простой способ -- использовать Basic HTTP аутентификацию. Пользователь с каждым запросом должен будет в заголовках запроса передавать логин и пароль (в открытом виде, поэтому обязательно нужно использовать HTTPS). На сервере будут проверяться логин/пароль, после чего доступ к API будет разрешаться или запрещаться. Модель пользователя с нужными методами: # project/models/user.py class User(SurrogatePK, Model): __tablename__ = "users" login = db.Column(db.String, nullable=False, info={'verbose_name': 'Логин'}) password_hash = db.Column(db.String, nullable=False, info={'verbose_name': 'Хэш пароля'}) name = db.Column(db.String, nullable=False, default='', info={'verbose_name': 'Имя'}) def __init__(self, login, password, name, **kwargs): db.Model.__init__(self, login=login, password=password, name=name, **kwargs) self.login = login @property def password(self): return None @password.setter def password(self, password): self.set_password(password) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, value): return check_password_hash(self.password_hash, value) @classmethod def get_by_login(cls, login): return cls.query.filter_by(login=login).first() def __repr__(self): return '<User {}>'.format(self.login) Теперь используем библиотеку Flask-HTTPAuth. Данной библиотеке нужно указать метод для проверки правильности пароля при помощи специального декоратора: # project/__init__.py: app = Flask(__name__) auth = HTTPBasicAuth() # project/api/auth.py import functools from flask import g from project import auth from project.models.user import User @auth.verify_password def verify_password(login, password): """Validate user passwords and store user in the 'g' object""" # try to authenticate with login/password user = User.query.filter_by(login=login).first() if not user or not user.check_password(password): return False g.user = user return True Теперь мы можем защитить нужный ресурс API с помощью аутентификации: # project/api/test.py from flask_restful import Resource from project.api import api from project import auth class Test(Resource): @auth.login_required def get(self): return {'test': 'test'} api.add_resource(Test, '/test') Данный подход имеет свои минусы. 1) Логин с паролем необходимо передавать при каждом запросе (небезопасно). 2) Клиент (веб-приложение) должен хранить у себя логин с паролем в открытом виде (небезопасно). HTTP Basic Auth via token Поэтому прикрутим аутентификацию при помощи токена. Пользователю будет выдаваться токен, полученный при помощи приптографического преобразования его идентификатора. Токен имеет срок действия. Пользователь может предоставить этот токен и пройти аутентификацию. Реализуем это снова при помощи Basic, только теперь токен будет передаваться вместо логина, а пароль будет проигнорирован, поэтому может быть любым. Добавляем новые методы в модель пользователя: # project/models/user.py from werkzeug.security import generate_password_hash, check_password_hash from itsdangerous import (TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired) from project import app, db from project.database import ( Model, SurrogatePK, ) class User(SurrogatePK, Model): __tablename__ = "users" login = db.Column(db.String, nullable=False, info={'verbose_name': 'Логин'}) password_hash = db.Column(db.String, nullable=False, info={'verbose_name': 'Хэш пароля'}) name = db.Column(db.String, nullable=False, default='', info={'verbose_name': 'Имя'}) def __init__(self, login, password, name, **kwargs): db.Model.__init__(self, login=login, password=password, name=name, **kwargs) self.login = login @property def password(self): return None @password.setter def password(self, password): self.set_password(password) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, value): return check_password_hash(self.password_hash, value) def generate_auth_token(self, expiration=3000): s = Serializer(app.config['SECRET_KEY'], expires_in=expiration) return s.dumps({'id': self.id}) @staticmethod def verify_auth_token(token): s = Serializer(app.config['SECRET_KEY']) try: data = s.loads(token) except SignatureExpired: return None # valid token, but expired except BadSignature: return None # invalid token user = User.query.get(data['id']) return user @classmethod def get_by_login(cls, login): return cls.query.filter_by(login=login).first() def __repr__(self): return '<User {}>'.format(self.login) Обновляем метод проверки пароля: # project/api/auth.py import functools from flask import g, abort from project import auth from project.models.user import User from flask_restful import Resource from project.api import api @auth.verify_password def verify_password(login_or_token, password): """Validate user passwords and store user in the 'g' object""" # first try to authenticate by token user = User.verify_auth_token(login_or_token) if not user: # try to authenticate with login/password user = User.query.filter_by(login=login_or_token).first() if not user or not user.check_password(password): return False g.user = user return True def self_only(func): @functools.wraps(func) def wrapper(*args, **kwargs): if kwargs.get('login', None): if g.user.login != kwargs['login']: abort(403) if kwargs.get('user_id', None): if g.user.id != kwargs['user_id']: abort(403) return func(*args, **kwargs) return wrapper class AuthToken(Resource): @auth.login_required def get(self): token = g.user.generate_auth_token() return {'token': token.decode('ascii')} api.add_resource(AuthToken, '/getToken') Вот главный файл, который управляет API: # project/api/__init__.py from flask import Blueprint from flask_restful import Api, fields api_blueprint = Blueprint('api', __name__, url_prefix='/api') api = Api(api_blueprint) # Marshaled fields for links in meta section link_fields = { 'prev': fields.String, 'next': fields.String, 'first': fields.String, 'last': fields.String, } # Marshaled fields for meta section meta_fields = { 'page': fields.Integer, 'per_page': fields.Integer, 'total': fields.Integer, 'pages': fields.Integer, 'links': fields.Nested(link_fields) } from . import auth from . import user from . import test Сделаем несколько тестовых запросов: [br0ke] ~/g/docs-flask (restful)> http http://127.0.0.1:5000/api/test HTTP/1.0 401 UNAUTHORIZED Content-Length: 19 Content-Type: text/html; charset=utf-8 Date: Thu, 11 Jan 2018 04:59:39 GMT Server: Werkzeug/0.12.2 Python/3.5.2 WWW-Authenticate: Basic realm="Authentication Required" Unauthorized Access [br0ke] ~/g/docs-flask (restful)> http --auth=admin1:admin1 http://127.0.0.1:5000/api/test HTTP/1.0 200 OK Content-Length: 23 Content-Type: application/json Date: Thu, 11 Jan 2018 04:59:49 GMT Server: Werkzeug/0.12.2 Python/3.5.2 { "test": "test" } [br0ke] ~/g/docs-flask (restful)> http --auth=admin1:wrong_pass http://127.0.0.1:5000/api/test HTTP/1.0 401 UNAUTHORIZED Content-Length: 19 Content-Type: text/html; charset=utf-8 Date: Thu, 11 Jan 2018 04:59:53 GMT Server: Werkzeug/0.12.2 Python/3.5.2 WWW-Authenticate: Basic realm="Authentication Required" Unauthorized Access [br0ke] ~/g/docs-flask (restful)> http --auth=admin1:admin1 http://127.0.0.1:5000/api/getToken HTTP/1.0 200 OK Content-Length: 142 Content-Type: application/json Date: Thu, 11 Jan 2018 05:00:13 GMT Server: Werkzeug/0.12.2 Python/3.5.2 { "token": "eyJpYXQiOjE1MTU2NTA3NTgsImFsZyI6IkhTMjU2IiwiZXhwIjoxNTE1NjUzNzU4fQ.eyJpZCI6Mn0.8nOHdN6fAGtfTSKV-BpsixGjC0r7SdVFdpAMBM_FbYI" } [br0ke] ~/g/docs-flask (restful)> http --auth=eyJpYXQiOjE1MTU2NTA3NTgsImFsZyI6IkhTMjU2IiwiZXhwIjoxNTE1NjUzNzU4fQ.eyJpZCI6Mn0.8nOHdN6fAGtfTSKV-BpsixGjC0r7SdVFdpAMBM_FbYI:wrong_pass http://127.0.0.1:5000/api/test HTTP/1.0 200 OK Content-Length: 23 Content-Type: application/json Date: Thu, 11 Jan 2018 06:06:15 GMT Server: Werkzeug/0.12.2 Python/3.5.2 { "test": "test" } Таким образом, API запроса токена защищено аутентификацией по логину/паролю, а дальше можно использовать аутентификацию по токену. |
|||||||
Так же в этом разделе:
|
|||||||
|
|||||||
|