使用Flask-security时缓慢的Flask响应

时间:2017-11-07 11:43:01

标签: python authentication flask flask-login 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='test@example.com', 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 "test@example.com:test"
{
  "hello": "world"
}


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

令牌认证时间

获取令牌是可以的。

curl -H "Content-Type: application/json" -X POST -d '{"email":"test@example.com","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-loginFlask-WTF,...)包装在一起。

  1. 你知道原因是什么吗? (是Flask-security还是Flask-login还是更深层次的东西?)
  2. 似乎慢速的哈希算法正在为每个请求运行。 但是,可能没有必要每次都这样做。 它应该足够了,只存储令牌并检查传入令牌是否与存储的令牌相同。 有没有办法这样做(使用Flask-security或不使用)?
  3. 我是否可以设置应用程序(app.config)以使其更快(仍然使用令牌身份验证。)?
  4. 是否有解决方法(仍在使用Flask-security)?
  5. 我要自己写吗?是Flask-security让我们退缩吗?
  6. 任何人都有这方面的线索?
  7. 我已在GitHub上将此问题交叉发布为问题。

3 个答案:

答案 0 :(得分:0)

我也遇到了这个问题。就我所知,这是散列,也是Flask-Security哲学的一部分。当您更改密码时,令牌将立即失效:我仍然不确定这是否是一个很好的要求/功能,但是我已经围绕它构建了一个完整的生态系统。因为我有一个SPA,可以处理很多请求,所以我不能忍受每个请求多1-3秒的时间:我也不想使用不太安全的哈希方法进行登录。

所以:我在数据库中添加了第二个令牌,该令牌的有效期为30分钟,并且可以更快地通过验证。

方案:用户使用常规的Flask-Security功能登录。有一个额外的REST终结点,该终结点仅与慢速令牌一起使用,将返回新的“ Quick-Authentication-Token”并将其MD5()表示形式存储在数据库中,并在第二列中设置时间戳。我对所有应用程序级别的REST请求都使用了此“快速身份验证令牌”,只是为其添加了一个新的装饰器。

所有用户内容(如更改密码,更新首选项等)仍由常规登录处理。通过Quick-Authentication-Token对安全性影响较小的REST请求进行身份验证。 30分钟后,SPA将重新获取带有原始身份验证令牌的新的快速身份验证令牌。

我希望这很清楚。概念证明可以在这里找到: https://github.com/acidjunk/improviser/blob/master/improviser/security.py

答案 1 :(得分:0)

使用危险的和您的SECRET_KEY对身份验证令牌进行签名。因此,Flask-Security(和您)可以确信内容没有被篡改。验证速度很快。 令牌内有user_id和用户密码(已哈希)的哈希版本。 user_id不能被认为是唯一的,因为在某些数据库中,主键值可以重用-因此Flask-Security需要一些唯一符,以确保令牌对应于正确的用户。它选择了用户的密码。现在,您不想在令牌中返回经过哈希处理的用户密码(请记住-令牌已签名,未加密)-因此FS选择了再次对(已哈希处理的)密码进行哈希处理。为了验证令牌,它会验证它是否由我们签名,然后取出user_id和hashed-password并将其与数据库中存储的密码进行比较。从设计上讲,密码散列很慢-这就是请求缓慢的原因。 与@acidjunk相似,https://github.com/jwag956/flask-security(我的Flask-Security分支)已经实现了解决方案,只需在Usermodel中添加一个可以用作唯一符的新字段即可(默认是使用uuid)。这样可以进行简单的相等检查,而不是哈希。

答案 2 :(得分:0)

对于积极(截至2019年11月)开发的Flask Security分支而言,它已用3.3.0版本解决了此问题:

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