[PYTHON] Implémentation de l'écran de l'administrateur DB par Flask-Admin et Flask-Login

introduction

Pour implémenter quelque chose comme un écran d'administration Django à l'aide de Flask, il est pratique d'utiliser une bibliothèque appelée Flask-Admin. Cependant, si vous utilisez simplement Flask-Admin tel quel, vous pouvez entrer dans l'écran administrateur sans saisir de mot de passe (sans vous connecter), ce qui est très vulnérable en termes de sécurité. Dans cet article, nous utiliserons Flask-Admin et Flask-Login pour ** implémenter un écran d'administrateur de base de données avec une fonction de connexion **.

Cet article

Source de référence

Puisque la quantité d'informations est un peu petite, c'est un article que j'ai essayé d'augmenter les informations de l'explication en japonais. Ceux qui parlent anglais ou qui ne sont pas doués pour les mots redondants peuvent voir le site de référence à partir du lien ci-dessus.

À propos de MVC

Model-View-Controller Vous devez en savoir un peu plus sur le modèle. Parce que dans cet article, j'utiliserai des mots comme modèle et contrôleur. Je pense que l'article ci-dessous sera utile. À propos du modèle MVC

Environnement de l'écrivain

Ubuntu20.04LTS
MySQL 8.0.21
Python3.8.5

Flask==1.1.2
Flask-Admin==1.5.6
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.4
mysqlclient==2.0.1

Code source complet

Soudain, je vais vous montrer tout le code source final pour ceux qui n'ont pas le temps. Une explication détaillée est ci-dessous.

from flask import Flask, abort, jsonify, render_template, request, redirect, url_for
from wtforms import form, fields, validators
import flask_admin as admin
import flask_login as login
from flask_admin.contrib import sqla
from flask_admin import helpers, expose
from flask_admin.contrib.sqla import ModelView
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username


class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('Le nom d'utilisateur ou le mot de passe est différent.')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('Le nom d'utilisateur ou le mot de passe est différent.')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('Le même nom d'utilisateur existe.')


def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>Pour ne pas créer de compte<a href="' + url_for('.register_view') + '">cliquez ici</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()
  
  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>Si vous avez déjà un compte<a href="' + url_for('.login_view') + '">Cliquez ici pour vous identifier</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))


init_login()
admin = admin.Admin(app, 'Écran administrateur', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))


@app.route("/", methods=['GET'])
def index():
  return "Hello, World!"

if __name__ == "__main__":
  app.run()

Se connecter à DB

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

Comme il existe de nombreux articles en japonais sur Internet, l'explication ici est omise.

Créer un modèle pour un compte administrateur

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username

login est votre nom d'utilisateur. Il définit le nom d'utilisateur et le mot de passe pour se connecter à l'écran administrateur. La raison pour laquelle chaque méthode a un décorateur de propriété est d'obtenir des informations telles que si vous vous êtes déjà connecté ou non lorsque vous écrivez le processus de connexion ultérieurement. Si vous voulez en savoir plus sur le décorateur immobilier ↓ Propriétés

Créer un contrôleur


class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('Le nom d'utilisateur ou le mot de passe est différent.')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('Le nom d'utilisateur ou le mot de passe est différent.')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('Le même nom d'utilisateur existe.')

C'est un contrôleur qui décrit le traitement lorsque des entrées sont reçues du formulaire de la vue (écran de connexion ou écran d'enregistrement de compte administrateur). Ce à quoi vous devez faire attention ici est la classe LoginForm.

check_password_hash(user.password, self.password.data)

est. Ceci est pratique qui compare le mot de passe réel qui est haché et enregistré et la valeur hachée entrée à partir de l'écran de connexion, et renvoie True s'ils correspondent.

Bien que cela ne soit pas recommandé, si le mot de passe est stocké en texte brut dans la base de données, l'expression conditionnelle doit être utilisée.

if user.password != self.password.data:

Je pense que vous devriez le changer en.

Créer une vue


def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>Pour ne pas créer de compte<a href="' + url_for('.register_view') + '">cliquez ici</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()
  
  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>Si vous avez déjà un compte<a href="' + url_for('.login_view') + '">Cliquez ici pour vous identifier</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))

init_login()
admin = admin.Admin(app, 'Écran administrateur', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

C'est un peu comme le faire normalement avec Flask.

Ce à quoi vous devez faire attention ici est la classe MyModelView. MyModelView hérite de sqla.ModelView et remplace la méthode is_accessible. (J'ai besoin de) La méthode is_accessible renvoie si l'utilisateur est déjà connecté. En remplaçant simplement la méthode is_accessible, vous pourrez définir des règles de contrôle d'accès dans une classe de vue ultérieure (ici, la classe MyAdminIndexView).

init_login()
admin = admin.Admin(app, 'Écran administrateur', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

Définit quelle classe de vue est réellement utilisée dans quel modèle.

Écrire du HTML

Cela n'a aucun sens sans HTML. Créez un répertoire de modèles dans le répertoire racine du projet et créez des fichiers et des répertoires avec la structure suivante.

templates/
    admin/
        index.html
    my_master.html
    index.html

my_master.html

{% extends 'admin/base.html' %}

{% block access_control %}
{% if current_user.is_authenticated %}
<div class="btn-group pull-right">
  <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
    <i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
  </a>
  <ul class="dropdown-menu">
    <li><a href="{{ url_for('admin.logout_view') }}">Se déconnecter</a></li>
  </ul>
</div>
{% endif %}
{% endblock %}

Sur l'écran après la connexion, si vous appuyez sur l'ID utilisateur, un bouton déroulant apparaîtra.

templates/index.html

<html>
  <body>
    <div>
      <a href="{{ url_for('admin.index') }}">Go to admin!</a>
    </div>
  </body>
</html>

N'importe quelle page d'index est bien. Je vais l'écrire de manière appropriée.

templates/admin/index.html

{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">

    <div>
        {% if current_user.is_authenticated %}
        <h1>Écran d'administration de Civichat</h1>
        <p class="lead">
Agréé
        </p>
        <p>
Vous pouvez gérer les données à partir de cet écran. Si vous souhaitez vous déconnecter/admin/Veuillez accéder à la déconnexion.
        </p>
        {% else %}
        <form method="POST" action="">
            {{ form.hidden_tag() if form.hidden_tag }}
            {% for f in form if f.type != 'CSRFTokenField' %}
            <div>
            {{ f.label }}
            {{ f }}
            {% if f.errors %}
            <ul>
                {% for e in f.errors %}
                <li>{{ e }}</li>
                {% endfor %}
            </ul>
            {% endif %}
            </div>
            {% endfor %}
            <button class="btn" type="submit">Terminé</button>
        </form>
        {{ link | safe }}
        {% endif %}
    </div>

    <a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i>Revenir</a>
</div>
{% endblock body %}

C'est comme une page d'index sur l'écran d'administration après la connexion.

Je souhaite restreindre l'IP en plus de l'authentification par mot de passe

En premier lieu, je pense qu'il est nécessaire de restreindre l'accès par adresse IP avant d'atteindre le formulaire de connexion. Je pense que l'article ci-dessous sera utile.

Restriction IP avec Flask

finalement

Si vous avez des erreurs, veuillez nous en informer dans les commentaires.

Recommended Posts

Implémentation de l'écran de l'administrateur DB par Flask-Admin et Flask-Login
Explication et mise en œuvre de SocialFoceModel
Introduction et mise en œuvre de JoCoR-Loss (CVPR2020)
Explication et implémentation de l'algorithme ESIM
Introduction et mise en œuvre de la fonction d'activation
Résumé de l'implémentation de base par PyTorch
Explication et mise en œuvre du perceptron simple
[Python] Implémentation de la méthode Nelder – Mead et sauvegarde des images GIF par matplotlib
Dérivation de la distribution t multivariée et implémentation de la génération de nombres aléatoires par python
Implémenter un modèle avec état et comportement (3) - Exemple d'implémentation par décorateur
Mise en œuvre et expérience de la méthode de clustering convexe
Implémentation de SVM par méthode de descente de gradient probabiliste
Explication et implémentation de l'algorithme Decomposable Attention
Résumé de la méthode de connexion par DB de SQL Alchemy
[Python] Comparaison de la théorie de l'analyse des composants principaux et de l'implémentation par Python (PCA, Kernel PCA, 2DPCA)
Comparaison d'exemples d'implémentation de k-means de scikit-learn et pyclustering
Approximation de bas rang des images par HOSVD et HOOI
Calcul des indicateurs techniques par TA-Lib et pandas
Implémentation de l'arbre TRIE avec Python et LOUDS
Apprentissage parallèle du deep learning par Keras et Kubernetes
Apprentissage profond appris par mise en œuvre (segmentation) ~ Mise en œuvre de SegNet ~
Explication de la distance d'édition et de l'implémentation en Python