【Python&Flask】パスワード管理アプリを作ってみる①【一番簡単なログイン画面】

技術

Flask再び

PythonでWebアプリケーションを作るFlaskというフレームワークがあって、以前少しだけ試しに触って見たことがあります。

『Docker & Flaskな環境構築(1) - Flask開発環境を作って触ってみた編 -』
Flask初体験「今度新たに入るプロジェクトがwebアプリなんですが、webアプリ開発が初めてなので教えてほしい」後輩からそう言われて、聞いてみるとPyt…
『Docker & Flaskな環境構築(2) - uWSGIとNginxで運用環境を作ってみた編』
今回のテーマは uWSGI前回、Flaskの開発環境をDocker上に作りました。今回は前回のサンプルアプリを実際の運用を想定した環境で動かしてみます。という…

その時は本当にちょっと触ってみたって感じで終わってしまったんですが、今度はそれなりにちゃんと何かを作ってみようかなと思い立ちました。

で、当然「何を作るか」ってところが肝心なわけですが、実は以前からちょっと困っていることがあります。

それは パスワード多すぎ問題 です。

このところ新たなサービスのアカウントを作る機会が何回かありました。
そのたびに覚えておかないといけないアカウントとパスワードが増えていきました。

もちろんパスワードの使いまわしはどんなサービスであっても推奨されません。
でも実際には10を超えるともう覚えていられませんし、新しいパスワードを考えるのにも限界があります。
結局使いまわしてしまったり、メモを残したりしちゃうことになるわけです。

実はずっと以前からパスワード管理には頭を悩ませていました。

ということで、パスワード管理のサービスを作ってみようかなと思います。
もちろん世の中にはすでに同じようなサービスがあって利用することも考えましたが、作ってもそこまで難しくなさそうだし、お試しに作ってみることにしました。

まずは開発環境を作ろう

開発環境を作ると言っても、以前と同じことをするだけなんですが。

一応考えているのは、Python3.10.5、Flask2.1.3、PostgreSQL14.4という構成で、Docker上に作ります。(Flask2.1.3は2022年7月13日時点の最新バージョンです)
つまりバージョンこそ違えど以前試した時と同じ構成ということです。

フォルダ構成はこちら。

init.sqlにはテーブルのCreate文を実装する予定ですが、テーブル設計は後回しにして、今はひとまず空のままにしておきます。

Dockerfiledocker-compose.ymlは下記のように作ります。
(詳細は以前の記事とほとんど同じなので割愛 ^^;)

# Baseイメージ
FROM python:3.10.5

# Flaskのインストール
RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org flask

# psycopg2(PostgreSQLクライアント)のインストール
RUN pip install psycopg2
version: '3.9'
services:
  db:
    image: postgres:14.4
    container_name: pass_mng_db
    restart: always
    environment:
      POSTGRES_USER: 'dev' # DBのユーザー名(=DB名)
      POSTGRES_PASSWORD: 'pass' # DBのパスワード
    volumes:
      - .\init_db:/docker-entrypoint-initdb.d    
  flask:
    build: .
    container_name: pass_mng_dev
    restart: always
    volumes:
      - .\app:/usr/local/src/work
    ports:
      - 5000:5000
    tty: true
    depends_on:
      - db

dockerコンテナの起動はpass_mngディレクトリで下記のコマンドを実行するだけです。

docker-compose up -d

起動したDockerコンテナには VSCode のRemoteDevelopment拡張でアクセスします。
RemoteDevelopment でコンテナに入ったら”/usr/local/src/work”ディレクトリを開いておきます。

あと、デバッグできるようにコンテナ内でPython拡張もインストールしておくと便利です。

以上で開発環境はひとまず完成です。

とりあえずログイン画面だけ作ってみる

最低限必要なのはログイン画面、データを登録する画面、そして登録したデータを確認できる画面の3つです。
ここで言うデータというのは、twitterなどのサービス名とそのアカウント&パスワードです。

本来はこのパスワード管理サービス自体のアカウント作成なんかも必要だと思いますが、今回はなしです。
将来的に考えることにしましょう。

では早速ログイン画面のデザインから。
サービス自体にアカウントがないのでパスワードを入力するだけの画面です。

これ以上ないくらいシンプルな画面になりました。

FlaskはMTVという設計モデルが採用されているフレームワークでJinja2というテンプレートエンジンが使われています。
テンプレートエンジンはHTMLをテンプレートとして定義し、動的にレンダリングしてくれます。

<!DOCTYPE html>
{% set APP_NAME = "PASSマネ" %}
<html>
    <head>
        <title>{{title}} - {{APP_NAME}}</title>
        <link rel="stylesheet" href="/static/css/style.css">
    </head>
    <body>
        <div class="header">
            <div class="content">
                <label style="margin-top: auto; margin-bottom: auto;">{{APP_NAME}}</label>
            </div>
        </div>
        <div class="main">
            {% block main %}
            {% endblock %}
        </div>
    </body>
</html>

共通レイアウトとして「_layout.html」を作っています。
といってもデザイン的な共通部分は画面上部のヘッダ(黒いところ)だけで、あとは<title> と cssを取り込んでいる <link> タグなどの <head> タグ部分の共通化だけです。
{% block main %}から{% endblock %}の部分に各画面のコンテンツが埋め込まれることになります。

ログイン画面自体のレイアウトはこうなりました。

{% extends '_layout.html' %}
{% block main %}
<h1 style="margin-top: 150px;">ログイン</h1>
<form style="margin-top: 80px; text-align: center;" method="post">
    <div>
        <label>パスワード</label>
        <input type="password" name="password" required>
    </div>
    <button class="primary">ログイン</button>
    <div>
        <label class="error">{{message}}</label>
    </div>
</form>
{% endblock %}

1行目の{% extends '_layout.html' %}で _layout.htmlを共通レイアウトとして指定していて、{% block main %}から{% endblock %}の部分が_layout.htmlに埋め込まれる部分になります。

<form>タグ内には「パスワード」テキストボックスと「ログイン」ボタンを定義しています。
パスワードを入力してログインボタンを押すとログインしてトップページへ遷移するわけです。

あとはcssですが、デザインを調整しているだけなので細かい説明は抜きで、すべて記載します。

body {
    font-size: 25px;
    margin: 0;
}
body input {
    font-size: 25px;
}
body button {
    font-size: 20px;
}

div.header {
    height: 50px;
    font-size: 30px;
    background-color: black;
    display: flex;
    color: whitesmoke;
}
div.header .content {
    max-width: 1000px;
    width: 100%;
    height: 100%;
    margin: auto;
    display: flex;
}

div.main {
    max-width: 1000px;
    display: contents;
}

@media only screen and (max-width: 1200px) {
    div.header .content {
        max-width: 800px;
    }        
    div.main {
        max-width: 800px;
    }
}

h1 {
    font-size: 40px;
    text-align: center;
    margin-top: 50px;
    margin-bottom: 20px;
}

label {
    font-weight: bold;
}

input[type="text"],
input[type="password"] {
    border-radius: 3px;
}

button {
    border-radius: 5px;
    padding: 8px 20px;
    min-width: 200px;
    margin: 20px;
}

button.primary {
    border-color: #6495ff;
    background-color: cornflowerblue;
    color: white;
}

label.error {
    color: red;
}

これでログイン画面のテンプレートはできたので、次はルーティングを制御するView部分のPythonコードを実装します。

MethodViewクラスでView実装

以前の記事ではデコレーターを使った書き方でViewを実装していましたが、今回はMethodViewクラスを使った実装を試してみることにしました。

from flask import request, render_template, redirect, session
from flask.views import MethodView

class LoginView(MethodView):
    __PASSWORD__ = "pass"

    def get(self):
        return render_template("login.html", title="ログイン")

    def post(self):
        form = request.form
        if form["password"] == LoginView.__PASSWORD__:
            # ログイン成功
            session["login"] = True
            return redirect("/")
        else:
            # ログイン失敗
            return render_template("login.html", title="ログイン", message="パスワードが間違っています"), 401

MethodViewクラスを継承したLoginViewクラスを作りました。

MethodViewクラスの派生クラスではget()post()put()delete()といったメソッド名を実装することで、それぞれGET、POST、PUT、DELETEのリクエストを受けて実行されるようになります。
LoginViewではGET、POSTのリクエストを処理するget()メソッドとpost()メソッドだけ実装しています。

get()ではrender_template()を使って、先ほど作ったlogin.htmlテンプレートをレスポンスするようにしています。

post()ではログイン画面で「ログイン」ボタンが押された時のログイン処理を実装しました。
ログイン画面では<form>タグ内にログインに必要な情報(パスワード)を定義したので、「ログイン」ボタンが押された時はフォームデータがpostされてきます。

フォームデータはrequest.formで取得できます。
画面で入力されたパスワードをフォームデータから取得して検証しているのはコードを見ればわかると思います。

今回は簡単に実装するために有効なパスワードを「pass」固定にしました。
もちろん普通はそんなことしません。
将来、アカウント管理を考える時に処理を見直すつもりです。

ログインに成功したらセッションにログイン済みであることを記録して、ルートパス(「/」)へ移動しています。

ログインに失敗した場合はステータスコードは 401(Unauthorized)エラーを指定した上で、エラーメッセージと共にもう一度login.htmlテンプレートをレスポンスします。

以上でログイン画面のViewは実装できたので、後は app.py でURLとLoginViewクラスを紐づければOKです。

from flask import Flask, redirect, session
from loginview import LoginView
from datetime import timedelta

app = Flask(__name__)

# セッション準備
app.secret_key = 'AopWe-To$eu44r0l'
app.permanent_session_lifetime = timedelta(minutes=20) 

# ルーティングの登録
def register_routes():
    app.add_url_rule("/login", view_func=LoginView.as_view("login"))

@app.route("/")
def index():
    if "login" in session and session["login"] == True:
        return "<h1>ログイン成功</h1>"
    else:
        return redirect("/login")

@app.route("/logout")
def logout():
    session["login"] = False
    return redirect("/login")

register_routes()

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0")

app.py のポイントは2つです。

1つ目はapp.secret_keyapp.permanent_session_lifetimeでセッションを利用するための準備をしてることです。
app.secrete_key ではセッションを暗号化するためのキーを設定していて、app.permanent_session_lifetime はセッションの有効期限を設定しています。
(暗号化キーも有効期限も任意の値を設定しています)

セッションは loginview.py で”ログイン済み”の情報を記録するために利用していましたね。
app.py の 15~20 行目でルートパスがリクエストされた時のViewが実装しました。
ここで"login" in session and session["login"] == Trueというコードでセッションから “ログイン済み” の情報を取得して、ログイン済みを判定しています。

ポイントの2つ目はapp.add_url_ruleで「/login」というURLとLoginViewクラスを紐づけていることです。
ここで紐づけを登録しておくことで「https://xxxxx/login」というURLに対して、GETメソッドをリクエストすればLoginViewクラスのget()が実行され、POSTメソッドをリクエストすればLoginViewクラスのpost()が実行される、というわけです。

最後に動作確認

ここまで出来たら動かしてみましょう。
実行方法はいくつかあって”/usr/local/src/work”で下記コマンドのいずれを実行しても動きます。

flask run -h 0.0.0.0
python app.py

ただ、Debug実行したい場合はlaunch.jsonを作る必要があります。
launch.jsonを作るのはVSCodeのPython拡張がインストールされていれば簡単です。

DEBUGビューを開いてCreate a launch.jsonリンクをクリック。
Python → Flask の順に選択し launch.json を作成します。

作成されたlaunch.jsonは基本的にデフォルトのままでOKですが、ホストからのアクセスを受け付けられるように、argsに「–host=0.0.0.0」だけ追加します。

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Flask",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "app.py",
                "FLASK_ENV": "development"
            },
            "args": [
                "run",
                "--no-debugger",
                "--host=0.0.0.0" // 追加
            ],
            "jinja": true,
            "justMyCode": true
        }
    ]
}

launch.json ができたら F5 キーで Debug実行できます。

実行後「http://127.0.0.1:5000」にアクセスすると、「http://127.0.0.1:5000/login」にリダイレクトされてログイン画面が表示されます。
ログイン画面ではパスワードに「pass」と入力すると再び「http://127.0.0.1:5000」にリダイレクトされて、「ログイン成功」と表示されるはずです。

まとめ

今回は『パスワード管理アプリを作ってみる』の1回目ということでログイン画面を実装してみました。
おそらく、一番簡単なログイン画面になったんではないでしょうかw

パスワードがプログラムにそのまま記載されていることの是非はもちろんあるでしょう。
アカウント管理を省略しているので、簡素化できている部分ももちろんあります。

ただ、とりあえず「パスワードを知らないとアクセスできない」という機能だけであれば、たったこれだけの実装でも実現できます。
運用に耐えうるかどうかはまた別問題ですが。

今回の実装を参考にされる方は問題が起きても責任は負えませんので自己責任でよろしくお願いします。

最後に今回作ったアプリケーションのここまでのフォルダ構成を紹介します。

今後はパスワード管理機能のメインとなるアカウントとパスワードを登録・確認できる画面を実装していく予定です。
気になる方はぜひ楽しみにしていてください。

追記(2022-08-27):
パスワード管理アプリを作ってみる②【DB接続と非同期APIで作るメイン画面】
を公開しました。

コメント

タイトルとURLをコピーしました