MyTetra Share
Делитесь знаниями!
Аутентификация в 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 запроса токена защищено аутентификацией по логину/паролю, а дальше можно использовать аутентификацию по токену.










 
MyTetra Share v.0.59
Яндекс индекс цитирования