Start_python’s diary

ふたり暮らし

アラフィフ夫婦のフリーランスプラン

Flask-Loginを使わずにログイン画面を実装する(Blueprintsでパス指定やSECRET_KEYを設置する方法)

f:id:Start_python:20200131094148p:plain

http://startpython.pythonanywhere.com/

こちらから実際に操作できますのでお試しください。(ユーザー名・パスワードは普段お使いのもの以外にしてください。セキュリティーは保証できません。)

 

はじめに

Webアプリに必要になることが多い「ログイン機能」を勉強していきます。複数のWebアプリを実行するために「Blueprints」を使用しましたが、結構行き詰ったところがありました。同じような苦労をした方にも参考になればうれしいです。

 

動作環境

Windows10
Python 3.7.5
Flask 1.1.1

作業フォルダ/
 ├ blog/
 │  ├ templates/
 │  │  └ blog/
 │  │      ├ base.html
 │  │      ├ top.html
 │  │      ├ newcomoer.html
 │  │      └ index.html
 │  ├ models/
 │  │  ├ __init__.py
 │  │  ├ database.py
 │  │  ├ models.py
 │  │  └ wiki.db
 │  ├ __init__.py
 │  └ server.py
 ├ templates/
 │  └ index.html
 ├ key.py
 └ main.py

 

1.ログイン画面と登録画面を作成

「作業フォルダ/blog/templates/blog」にHTMLファイルを作成します。

base.html(共通のテンプレート)

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <title>お試し日記</title>
  <code src="https://code.jquery.com/jquery-3.2.1.slim.min.j" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></code>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.j" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.cs" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">

</head>
<body>
<header>
  <div class="navbar navbar-dark box-shadow" style="background-color:#4169e1;">
    <div class="container d-flex justify-content-between">
      <a href="/blog/" class="navbar-brand d-flex align-items-center">
        <strong>お試し日記</strong>
      </a>
    </div>
  </div>
</header>
<div class="container">

{% block body %}{% endblock %}
</div>
</body>

top.html(ログイン画面)

{% extends "blog/base.html" %}
{% block body %}
 
<h1>ログイン</h1>
{% if status == "user_notfound" %}
<p>ユーザが見つかりません。新規登録してください</p>
{% elif status == "wrong_password" %}
<p>パスワードが間違っています</p>
{% elif status == "logout" %}
<p>ログアウト完了</p>
{% endif %}
<form action="/blog/login" method="post">
    <input type="text" name="user_name" placeholder="user name">
    <input type="password" name="password" placeholder="password">
    <input class="btn btn-primary" type="submit" value="Login">
</form>
<a href="/blog/newcomer">新規登録はこちら</a>
 
{% endblock %}

newcomer.html(新規登録画面)

{% extends "blog/base.html" %}
{% block body %}
 
<h1>新規登録</h1>
    <a href="/blog/top">ログイン画面に戻る</a>
    {% if status == "exist_user" %}
    <p>そのユーザは既に登録されています。</p>
    {% endif %}
    <form action="/blog/registar" method="post">
        <input type="text" name="user_name" placeholder="user name">
        <input type="password" name="password" placeholder="password">
        <input class="btn btn-primary" type="submit" value="新規登録">
    </form>
{% endblock %}

index.html(ログイン中の画面)

{% extends "blog/base.html" %}
{% block body %}
 
<p>ようこそ {{name}} さん</p>
<a href="/blog/logout">ログアウトする </a>
<a href="/blog/create">新しく日記を書く </a>
<table class="table table-striped table-hover">
  <tr>
    <th>id</th>
    <th>日時</th>
    <th>title</th>
    <th>date</th>
    <th> </th>
  </tr>
 
</table>
 
{% endblock %}

レイアウトの部分については次回勉強したいと思いますので、今回は参考サイト様からそのままお借りしています。

 

2.SQLAlchemyを使ってSQLiteのサーバーを作成する

ユーザー情報(ユーザー名、パスワード)を保存しておくデータベースを構築します。

SQLAlchemyモジュールをインストール

インストールがまだの場合は、コマンドプロンプトから下記を実行します。

pip install SQLAlchemy

「作業フォルダ/blog/models」にpyファイルを作成します。

__init__.py

 

中身は空のままです。「__init__.py」は同じフォルダ内のpyファイルをモジュールとして呼び出す際(インポートして使うため)に必要です。別のpyファイルから利用できるようなります。(前回より理解が深まった部分)

database.py(データベース情報)

from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from sqlalchemy.ext.declarative import declarative_base
import os

#パスを定義してwiki.dbを作成します。
databese_file = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'wiki.db')
engine = create_engine('sqlite:///' + databese_file, convert_unicode=True)

#データベースにアクセスするためにセッションを作成します。
db_session = scoped_session(sessionmaker(autocommit=False,
                                         autoflush=False,
                                         bind=engine))
#Baseオブジェクトを作った後、query_property()を使ってBaseオブジェクトに検索クエリを持たせます。
#後でデータベースから検索する時に楽らしい。
Base = declarative_base()
Base.query = db_session.query_property()

#データベースの初期化用です。初期化したいときに呼びます。
def init_db():
    import models.models
    Base.metadata.create_all(bind=engine)

models.py(テーブル定義)

from sqlalchemy import Column, Integer, String, Text, DateTime
from models.database import Base        # データベースの初期化用
#from blog.models.database import Base  # 初期化が終わればこちらへ変更
from datetime import datetime
 
 
class WikiContent(Base):
    __tablename__ = 'wikicontents'
    id = Column(Integer, primary_key=True)
    title = Column(String(128))
    # title = Column(String(128), unique=True)
    body = Column(Text)
    date = Column(DateTime, default=datetime.now())
 
    def __init__(self, title=None, body=None, date=None):
        self.title = title
        self.body = body
        self.date = date
 
    def __repr__(self):
        return '<Title %r>' % (self.title)
 
#Userクラスを追加。WikiContentの使い回し
class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, primary_key=True)
    user_name = Column(String(128))
    hashed_password = Column(String(128))
 
    def __init__(self, user_name=None, hashed_password=None):
        self.user_name = user_name
        self.hashed_password = hashed_password
 
    def __repr__(self):
        return '<Name %r>' % (self.user_name)

データベースの初期化

wiki.db」を作成します。コマンドプロンプトから下記を実行します。

cd 作業フォルダ
cd blog
python
from models.database import init_db
init_db()
quit()

modelsフォルダ内に「wiki.db」ファイルが出来ます。確認してみましょう。

※重要:「wiki.db」が作成されたら「models.py」の2行目の
from models.database import Base」を削除かコメント化して
from blog.models.database import Base」に変更かコメント化を解除してください。(他にいい方法があればいいのですが、、、最初だけなので。)

 

3.暗号化キー情報を作成

「作業フォルダ」にkey.pyファイルを作成します。中身は、セッション管理に利用するSECRET_KEYと、パスワードの暗号化の際に利用するSALTです。

key.py

SECRET_KEY = "任意の文字列"
SALT = "任意の文字列"

「任意の文字列」の部分をお好きな文字列(英数字のみ?)に変更してください。なるべく推測されにくいものにしましょう。

※この呼び出し方に苦労することになります。後ほど記載します。

 

4.メインプログラムの作成

「作業フォルダ」にpyファイルを作成します。コメントを多めにして解説は少なくします。

main.py(起動ファイル)

# Flaskとrender_template(HTMLを表示させるための関数)をインポート
from flask import Flask, render_template

# Flaskオブジェクトの生成
app = Flask(__name__)

# セキュリティーキーの設定(なりすましなどを防ぐため)
import key
app.secret_key = key.SECRET_KEY

# 前回作った基本的なWebページ(今回は使いません)
#from test.test_app import app1
#app.register_blueprint(app1)

# 今回使用するWebアプリ
from blog.server import app as app2
app.register_blueprint(app2, url_prefix="/blog")


#「/」へアクセスがあった場合に「index.html」を返す
@app.route('/')
def index():
    return render_template('index.html')


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

※セキュリティーキーの設定を「server.py」のほうでやろうとしてエラーが止まりませんでした。「main.py」でしか「app.config」の部分が触れないみたいです。
(ずっと「server.py」で考えていて、current_appを使ってapp.configを変更しようとしたり「server.py」の「app」の名前を変えたり、「main.py」でやろうと思い付くまでにすごく時間を使ってしまいました。)

前回より理解が深まった点

  • from blog.server import app as app2
    「as app2」を使うことによって、分割されたpyファイルで「app」のまま使えるようになりました。
  • app.register_blueprint(app2, url_prefix="/blog")
    「url_prefix="/blog"」を入れることによって、分割されたpyファイルでは「/」が「/blog」扱いされるようになります。(「/top」は「/blog/top」扱いに)

「作業フォルダ/blog」にpyファイルを作成します。

__init__.py

 

中身は空のままです。

server.py(Blueprintで分割された実行ファイル)

# Blueprint(pyファイルを分割するための関数)をインポート
from flask import Blueprint

#「app」を「Blueprint()」を使って定義
#「blog」の部分は、url_for("blog.top")で使用
app = Blueprint('blog', __name__, template_folder='templates')


# 必要なモジュールをインポート
import os
from datetime import datetime
from flask import Flask, session, redirect, url_for, render_template,request
#「/blog/models/models.py」の「WikiContent」と「User」を呼び出す
from blog.models.models import WikiContent,User
#「/blog/models/database.py」の「db_session」を呼び出す
from blog.models.database import db_session


# ハッシュ化するための関数をインポート(パスワードの暗号化用)
# 今回はsha256という暗号化方式を使用
import key
from hashlib import sha256


#「/」へアクセスがあった場合
@app.route('/')
def index():  # 一覧画面
    if "user_name" in session:
        # セッションがログイン状態であれば「blog/index.html」を返す
        name = session["user_name"]
        all_diary= WikiContent.query.all()
        return render_template('blog/index.html',name=name, all_diary=all_diary)
    else:
        # ログイン状態でなければ「/blog/top」ページへ移動
        return redirect(url_for("blog.top"))

#「/logout」へアクセスがあった場合「/blog/top」ページへ移動
@app.route("/logout")
def logout():
    session.pop("user_name", None)
    return redirect(url_for("blog.top",status="logout"))

#「/create」へアクセスがあった場合「準備中です」を表示
@app.route('/create')
def create():  # 新規作成
    return '準備中です'

#「/login」へアクセスがあった場合
@app.route("/login",methods=["post"])
def login():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合
        #「パスワード」が一致するか確認(「key.SALT」で暗号化)
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        if user.hashed_password == hashed_password:
            #「パスワード」が一致した場合「/blog/index」ページへ移動
            session["user_name"] = user_name
            return redirect(url_for("blog.index"))
        else:
            #「パスワード」が一致しない場合「/blog/top」ページへ移動
            return redirect(url_for("blog.top",status="wrong_password"))
    else:
        #「ユーザー名」が存在しなかった場合「/blog/top」ページへ移動
        return redirect(url_for("blog.top",status="user_notfound"))

#「/registar」へアクセスがあった場合
# 新規登録画面で「新規登録」ボタンが押された時
@app.route("/registar",methods=["post"])
def registar():
    # 入力された「ユーザー名」が既に存在するか確認
    user_name = request.form["user_name"]
    user = User.query.filter_by(user_name=user_name).first()
    if user:
        #「ユーザー名」が存在した場合「/blog/newcomer」ページへ移動
        return redirect(url_for("blog.newcomer",status="exist_user"))
    else:
        #「ユーザー名」が存在しなかった場合
        #「ユーザー名」「パスワード」(「key.SALT」で暗号化)を
        password = request.form["password"]
        hashed_password = sha256((user_name + password + key.SALT).encode("utf-8")).hexdigest()
        user = User(user_name, hashed_password)
        # データベースに追加して保存
        db_session.add(user)
        db_session.commit()
        session["user_name"] = user_name
        #「/blog/index」ページへ移動
        return redirect(url_for("blog.index"))

#「/top」へアクセスがあった場合「blog/top.html」を返す
@app.route("/top")
def top():
    status = request.args.get("status")
    return render_template("blog/top.html",status=status)

#「/newcomer」へアクセスがあった場合「blog/newcomer.html」を返す
@app.route("/newcomer")
def newcomer():
    status = request.args.get("status")
    return render_template("blog/newcomer.html",status=status)

前回より理解が深まった点

  • return render_template('blog/index.html')
    こちらの「blog」の部分は「main.py」の「app.register_blueprint(app2, url_prefix="/blog")」にある「/bog」です。
  • return redirect(url_for("blog.top"))
    こちらの「blog」の部分は「server.py」の「app = Blueprint('blog', __name__, template_folder='templates')」にある「blog」です。
  • import key
    「server.py」ではSALTしか使っていません。(「key.SALT」として利用)
    「main.py」ではSECRET_KEYしか使っていません。(「key.SECRET_KEY」として利用)両方のpyファイルでインポートします。

 

最後に「作業フォルダ/templates」にHTMLファイルを作成します。

index.html(違うフォルダにも「index.html」があるので注意です)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <title>ふたり暮らし メニュー画面</title>
  </head>
  <body>

    <h2>メインメニュー</h2>
    <p><a href= "/test_home.html" >ボタンでページを移動する</a></p>
    <p></p>
    <p><a href= "/blog" >ブログ(登録とログイン)</a></p>

  </body>
</html>

(「ボタンでページを移動する」は前回作ったWebページです。)

 

まとめ

Flask-Loginを使わないメリットはわかりませんが、ログイン機能をはじめて実装するときはシンプルなほうから勉強しようと考えてこのかたちになりました。これで新規登録とログインができるようになりましたが、課題がたくさん出てきました。たとえば現状のままだとユーザー名もパスワードも空のまま登録できてしまいます。

とりあえず一旦完成ということで、次回からはレイアウトを少しいじっていきます。

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村

 

こちらのサイトを参考にさせていただきました。

manecoco.com

qiita.com