Медленный ответ Flask при использовании Flask-security

На моей нынешней работе мне довелось быть частью бэкэнд-команды, которая занимается созданием API. Затем API должен быть передан в приложение JavaScript и должен быть достаточно быстрым (100 мс или около того). Однако это не так.

После некоторого профилирования мы выяснили, что нас сдерживает аутентификация токена в Flask-security (см. MWE).

MWE

import flask
from flask_security import Security, SQLAlchemyUserDatastore, UserMixin, RoleMixin, auth_required
from flask_sqlalchemy import SQLAlchemy

app = flask.Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/database.sqlite3'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['WTF_CSRF_ENABLED'] = False
app.config['SECURITY_TOKEN_AUTHENTICATION_HEADER'] = 'Authorization'
app.config['SECURITY_PASSWORD_HASH'] = 'pbkdf2_sha512'
app.config['SECURITY_PASSWORD_SALT'] = b'secret'
app.config['SECRET_KEY'] = "super_secret"

db = SQLAlchemy(app)

roles_users = db.Table('roles_users',
                       db.Column('user_id', db.Integer(), db.ForeignKey('user.id')),
                       db.Column('role_id', db.Integer(), db.ForeignKey('role.id')))


class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))


class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))


user_datastore = SQLAlchemyUserDatastore(db, User, Role)

# Setup Flask-Security
security = Security(app, user_datastore)

db.drop_all()
db.create_all()

admin_role = Role(**{'name': 'admin', 'description': 'Admin role'})
db.session.add(admin_role)
db.session.commit()

user_datastore.create_user(email='[email protected]', password='test', active=True, roles=[Role.query.first()])
db.session.commit()


@app.route('/')
@auth_required('basic', 'token')
def hello():
    return flask.jsonify({'hello': 'world'})


if __name__ == '__main__':
    app.run(debug=True)

Время базовой аутентификации

Тайминги идеальные (менее 100 мс), но это не то, как мы должны это делать.

time curl http://127.0.0.1:5000/ -u "[email protected]:test"
{
  "hello": "world"
}


real    0m0.076s
user    0m0.008s
sys     0m0.006s

Время аутентификации токена

Получение токена в порядке.

curl -H "Content-Type: application/json" -X POST -d '{"email":"[email protected]","password":"test"}' http://127.0.0.1:5000/login

{
    "meta": {
        "code": 200
    }, 
    "response": {
        "user": {
          "authentication_token": "WyIxIiwiJDUkcm91bmRzPTUzNTAwMCRFRUpLRFNONlB2L1hzL2lRJDhMWFZvZlpLMmVoa1BVdWtpRlhUR1lvNEJ3T3FjS3dKMVhVWGlOczRwZDMiXQ.DOLjcQ.oBrT4gr1m49rISyxhaj9Lxu1VNk", 
          "id": "1"
        }
    }
}

Но чем запрос ужасно медленный. Тайминги в 20 раз медленнее.

time curl "http://127.0.0.1:5000/?auth_token=WyIxIiwiJDUkcm91bmRzPTUzNTAwMCRFRUpLRFNONlB2L1hzL2lRJDhMWFZvZlpLMmVoa1BVdWtpRlhUR1lvNEJ3T3FjS3dKMVhVWGlOczRwZDMiXQ.DOLjcQ.oBrT4gr1m49rISyxhaj9Lxu1VNk"
{
  "hello": "world"
}

real    0m2.371s
user    0m0.005s
sys     0m0.006s

Что с этим???

Я знаю, что Flask-security объединяет несколько других пакетов безопасности flask (Flask-login, Flask-WTF, ...).

  1. Вы знаете, что может быть причиной? (это Flask-security или Flask-login или что-то глубже?)
  2. Кажется, что медленный алгоритм хеширования работает для каждого запроса. Однако, возможно, нет необходимости делать это каждый раз. Должно быть достаточно только сохранить токен и проверить, совпадает ли входящий токен с сохраненным. Есть ли способ сделать это так (либо с Flask-security, либо без)?
  3. Могу ли я настроить приложение (app.config) по-другому, чтобы оно работало быстрее (по-прежнему используя авторизацию по токену)?
  4. Есть ли обходной путь (по-прежнему используется Flask-security)?
  5. Я сам напишу? Это Flask-security сдерживает нас?
  6. Кто-нибудь знает об этом?

Я опубликовал это как проблему на GitHub.


person KrysotL    schedule 07.11.2017    source источник
comment
Хороший вопрос, но, пожалуйста, не упоминайте здесь сроки — ваша аудитория почти полностью состоит из добровольцев, и они здесь на досуге.   -  person halfer    schedule 07.11.2017
comment
@POLOSTutorials: если вы видите в вопросе объявление о перекрестной публикации, не удаляйте его. Кросс-постинг может показаться несколько нетерпеливым, но необъявленный кросс-постинг намного хуже, и OP может получить за это отрицательные голоса, даже если первая версия правильно признала кросс-постинг.   -  person halfer    schedule 07.11.2017
comment
KrysotL: Я не вижу кросс-пост вопроса, но, пожалуйста, убедитесь, что вы вернетесь сюда, чтобы не тратить время полезного человека в будущем, который может непреднамеренно помочь вам с чем-то, что уже решено в другом месте.   -  person halfer    schedule 07.11.2017
comment
@halfer Спасибо за правки. PythonistaCafe — частный форум, поэтому я удалил его. Тем не менее, я добавил перекрестную публикацию в проблему github, которую я открыл.   -  person KrysotL    schedule 07.11.2017
comment
@KrysotL есть новости по этому поводу? Вам удалось как-то обойти это?   -  person Carl    schedule 12.12.2017
comment
Что ж, мы уходим от flask-security (подробнее в github issue comment< /а>)   -  person KrysotL    schedule 03.01.2018


Ответы (3)


Токены аутентификации подписаны с помощью itsdangerous и вашего SECRET_KEY. Таким образом, Flask-Security (и вы) можете быть уверены, что содержимое не было подделано. Проверить это быстро. Внутри токена находится user_id и хешированная версия (уже хэшированного) пароля пользователя. user_id нельзя считать уникальным, поскольку в некоторых БД значения первичного ключа могут использоваться повторно, поэтому Flask-Security требуется некоторый uniquifier, чтобы быть уверенным, что токен соответствует правильному пользователю. Он выбрал пароль пользователя. Теперь вы не хотите возвращать хешированный пароль пользователя в токене (помните - токены подписаны, а не зашифрованы) - поэтому FS решила еще раз хешировать (уже хешированный) пароль. Чтобы проверить токен, он проверяет, был ли он подписан нами, затем извлекает user_id и хешированный пароль и сравнивает их с паролем, хранящимся в БД. Хеширование паролей по своей природе является медленным, и это является причиной медленных запросов. В духе @acidjunk, https://github.com/jwag956/flask-security (мой форк Flask-Security) реализовал решение этой проблемы, просто добавив новое поле в модель пользователя, которое может действовать как uniquifier (по умолчанию используется uuid). Это приводит к простой проверке на равенство, а не к хешу.

person jwag    schedule 24.08.2019
comment
Если вы все еще используете неподдерживаемый Flask-Security: рассмотрите возможность использования этого форка. Он исправляет пару ошибок, активно поддерживается, имеет лучшую документацию и добавляет новые функции (например, 2FA). - person acidjunk; 01.07.2020

Что бы это ни стоило, активно (по состоянию на ноябрь 2019 г.) разрабатываемый форк Flask Security утверждает, что решил эту проблему с выпуском 3.3.0:

https://github.com/jwag956/flask-security/blob/master/CHANGES.rst#version-330

person Puchatek    schedule 06.11.2019

Я тоже столкнулся с этим. Это хеширование и, насколько мне известно, часть философии Flask-Security. Когда вы меняете пароль, токен немедленно становится недействительным: я все еще не уверен, что это хорошее требование/функция, но я уже построил вокруг него целую экосистему. Поскольку у меня есть SPA, который выполняет много запросов, я не мог жить с дополнительными 1-3 секундами на каждый запрос: я также не хотел использовать менее безопасный метод хеширования для входа в систему.

Итак: я добавил второй токен в БД, который действителен в течение 30 минут и может быть проверен намного быстрее.

Сценарий: пользователь входит в систему с обычными функциями Flask-Security. Доступна дополнительная конечная точка REST, доступная только с медленным токеном, который вернет новый «токен быстрой аутентификации» и сохранит его представление MD5 () в базе данных и установит отметку времени во втором столбце. Я использую этот "Quick-Authentication-Token" для всех запросов REST уровня приложения, я только что добавил для него новый декоратор.

Все действия пользователя, такие как смена пароля, обновление настроек и т. д., по-прежнему обрабатываются при обычном входе в систему. Запросы REST с меньшим влиянием на безопасность аутентифицируются с помощью токена быстрой аутентификации. Через 30 минут SPA повторно получит новый токен быстрой аутентификации с исходным токеном аутентификации.

Я надеюсь, что это ясно. Доказательство концепции можно найти здесь: https://github.com/acidjunk/improviser/blob/master/improviser/security.py

person acidjunk    schedule 09.08.2019
comment
С форком flask security в Flask-Security-Too проблема также решена. - person acidjunk; 07.01.2020