将 Flask-SocketIO 与 Flask-Login 和 HTTP Basic Auth 结合使用

Using Flask-SocketIO with Flask-Login and HTTP Basic Auth

我正在尝试实现一个非常简单的内部监控网页。它应该显示一些数据,这些数据通过 socketio 实时更新。服务器在后台运行一个线程,该线程获取数据并将其转发给客户端。

我想用登录表单保护页面。为了简单起见,我选择了 HTTP Basic Auth,主要是因为我不想设计登录表单。

我做了以下事情:

这是整个工作示例:

## Standard imports, disregard them
import functools
import gevent

## Otherwise I'm getting KeyError on shutdown
import gevent.monkey
gevent.monkey.patch_all()

from flask import Flask, request, Response
from flask.ext.login import LoginManager, UserMixin, login_required, current_user
from flask.ext.socketio import SocketIO

## To see the logging.debug call in socketio.on('connect')
import logging
logging.getLogger().setLevel(logging.DEBUG)

## App configuration
app = Flask(__name__)
app.debug = True
app.config['SECRET_KEY'] = 'a long and random string'

login_manager = LoginManager()
login_manager.init_app(app)
socketio = SocketIO(app)

## This thing sends updates to the client
class BackgroundThread(gevent.Greenlet):
    def run(self):
        while True:
            socketio.emit(
                'my event',
                {'my field': 'my data'},
                namespace='/my-namespace'
            )
            gevent.sleep(2)

## Not bothering with a database
class User(UserMixin):
    users = {
        u'1': (u'myname', u'mypass')
    }

    def __init__(self, username, password):
        self.username = username
        self.password = password

    def get_id(self):
        return u'1'

    @classmethod
    def get_by_username(cls, requested_username):
        for username, password in cls.users.itervalues():
            if username == requested_username:
                return User(username, password)
        return None

## From https://flask-socketio.readthedocs.org/en/latest/
def authenticated_only(f):
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        if not current_user.is_authenticated():
            request.namespace.disconnect()
        else:
            return f(*args, **kwargs)
    return wrapped

## The password is checked here
@login_manager.request_loader
def load_request(request):
    auth = request.authorization

    if auth is not None:
        username, password = auth['username'], auth['password']
        user = User.get_by_username(username)
        if user is not None and user.password == password:
            return user
    return None

## From http://flask.pocoo.org/snippets/8/
@login_manager.unauthorized_handler
def http_basic_auth():
    return Response(
    'Could not verify your access level for that URL.\n'
    'You have to login with proper credentials', 401,
    {'WWW-Authenticate': 'Basic realm="Login Required"'})


@app.route('/')
@login_required
def index():
    return "My page"  # in real code this is actually a render_template call


@socketio.on('connect', namespace='/my-namespace')
@authenticated_only
def test_connect():
    logging.debug('Client connected: {.username}.'.format(current_user))


if __name__ == '__main__':
    thread = BackgroundThread()
    thread.start()

    socketio.run(app)

UPD:在可预见的未来,我将成为唯一的用户,所以我主要担心是否可以拦截和解密流量,或通过发送数据未经身份验证的 Websocket 连接。

Is this setup secure, provided that I use HTTPS with a self-signed certificate?

您将用户密码以明文形式存储在您的数据库中(我知道,您还没有数据库,但我想您最终会有一个?)。如果您的数据库遭到黑客攻击,那么您的用户会讨厌您,尤其是那些使用相同密码进行在线银行业务的用户。您应该将散列密码存储在数据库中以保护它们免受黑客攻击。查看 Flask-Bcrypt 或 Werkzeug 中的密码散列函数。

使用 HTTPS 很好,但由于您也在使用 WebSocket,因此您需要评估通过套接字连接的数据是否也需要加密。

self-signed 证书不是一个好主意,因为浏览器无法验证其真实性,因此他们会(正确地)建议您的用户远离您的网站。

The Flask-Login docs stress that to actually login the user, I have to explicitly call login_user. I don't do that and yet I can log in. How is that possible?

让用户登录的想法是您不必 re-authenticate 他们发送的每个请求。 login_user只是记录用户登录到session。在后续请求中 Flask-Login 将在 session 中找到用户,因此不需要调用您的回调再次进行身份验证。

在您的情况下,您使用的是 HTTP 基本身份验证。浏览器将在每个请求中发送 Authorization header,并且由于 Flask-Login 从未在 session 中找到任何内容,它总是调用您的回调,每次都会对用户进行身份验证。我看不出这有什么问题,但如果你想避免不断验证用户的努力(特别是在你添加密码散列后,这是 CPU 密集的),你可能需要考虑调用 login_user 功能使事情变得更有效率。

更新:所以你声称你打算在代码中保留以纯文本形式编写的用户列表。这是一个非常非常糟糕的主意。您希望努力确保客户端和服务器之间传输的数据安全,因此您还应该在如何存储密码方面采取良好的安全措施。

我看到,在您是唯一用户的小型网站的代码中拥有密码的最大风险是您错误地暴露了代码。例如,如果您想将您的代码置于版本控制之下,那么除了在服务器上运行的副本之外,您还将在那里拥有一份密码副本(另一个可能被黑客入侵的地方)。如果您还对脚本进行了备份,它也会在那里。

所以帮自己一个忙,不要在代码中写密码。至少,在启动时从环境变量中读取它。