はじめに
自分用のWebアプリでFlaskとSQLAlchemyを使う機会があったので、基本部分だけアウトプットします。
今回はUserテーブルでユーザー作成、認証、削除のみ実装します。
割とムリヤリ設計が多いです。
SQLAlchemyとは
pythonでよく使われているORマッパー。
オブジェクト指向が理解できてれば使えると思います。
環境
$ python -v
Python 3.11.0
Flask==3.0.0
SQLAlchemy==2.0.20
ディレクトリ構造
project_folder/
├ models/ # SQLAlchemyのモデルを入れる
├ static/ # jsやらCSSやらを入れる
| └ js/
| └ css/
├ templates/ # htmlを入れる
| └ index.html
| └ register.html
| └ login.html
| └ mypage.html
└ app.py # flaskとか書く
models.py
まずは、SQLAlchemy用のモデルを作ります。
from sqlalchemy import create_engine, Column, CHAR, DateTime, INTEGER, VARCHAR
from datetime import datetime
from sqlalchemy.orm import scoped_session, sessionmaker, declarative_base
engine = create_engine("mysql://USERNAME:PASSWORD@HOSTNAME/DB_name") # SQLiteなら"sqlite://ファイルパス"
db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) # セッションを作成してトランザクション管理出来るようにします。
Base = declarative_base() # ベースとなるクラスを継承します
class User(Base):
__tablename__ = "users" # テーブル名
__table_args__ = {
"mysql_default_charset": "utf8mb4",
"mysql_collate": "utf8mb4_general_ci",
}
id = Column(INTEGER, primary_key=True, autoincrement=True) # idを主キーに設定、オートインクリメントをオンにします。
username = Column(VARCHAR(256), nullable=False)
email = Column(VARCHAR(256), nullable=False)
hashed_password = Column(CHAR(64), nullable=False) # パスワードをSALTでハッシングするので、SALTとハッシュ済みパスワードを両方保存します。
salt = Column(CHAR(64), nullable=False)
createdAt = Column(DateTime, nullable=False, default=datetime.now) # 作成された日付を保存
updatedAt = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now) # updateされた日付を保存
# 各カラムの初期化設定
def __init__(self, username=None, email=None, hashed_password=None, salt=None):
self.username = username
self.email = email
self.hashed_password = hashed_password
self.salt = salt
Base.metadata.create_all(engine)
Base.query = db_session.query_property()
app.py
SQLAlchemyを保存する際のポイント
Userクラスにid(オートインクリメント), username, email, hashed_password, new_salt, createdAt(default設定済み), updatedAt(default設定済み)
カラムがあり、ここにデータを保存する場合、
User(username, email, hashed_password, new_salt)
の形式で記述します。
オートインクリメントや、default値の設定がされているカラムは特段入れたいデータが無い限り設定する必要はありません。
また、クラス記述時に決めたカラムとUser(data, data)
に記述するデータの順番は対応しているので、User(email, username)
のように書くとusername
カラムにemail
が、email
カラムにusername
が入ります。
認証設計
メールアドレス
・ユーザー名
・パスワード
でユーザーを登録し、メールアドレス
とパスワード
を入力してログインする仕様になっています。
また、ユーザー登録時にランダム生成されたSALTキーを含めてハッシュ化する仕様にしています。
ログイン認証時はデータベースに保存されたstored_salt
を用いて復号化します。
ユーザー名
で認証するシステムでも出来ると思います。
実コード
from models.models import User, db_session
from flask import Flask, render_template, request, session
from hashlib import sha256
import secrets
app = Flask(__name__, static_url_path="/static")
app.secret_key = secrets.token_hex(24)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = user # ここと
email = example@example.com # ここはフォームから取得するなりして置き換えてください
password = "examplepassword"
new_salt = secrets.token_hex(32)
hashed_password = sha256((username + password + new_salt).encode("UTF-8")).hexdigest()
user = User(username, email, hashed_password, new_salt)
db_session.add(user)
db_session.commit()
session["username"] = username
session["is_logged_in"] = True
return redirect("mypage")
return render_template("register.html")
@app.route("/delete", methods=["POST"]) # /deleteにPOSTするとユーザーが削除される
def delete():
username = session["username"] # ここではセッションに保存されたユーザーを削除する
# GETメソッドを有効にし、フォームから取得したユーザーを削除することも出来る
if username is not None:
user = User.query.filter_by(username=username).first()
db_session.delete(user)
db_session.commit()
session.pop("username", None)
session.pop("is_logged_in", False)
return redirect("/")
@app.route("/login", methods=["POST", "GET"]) # ログイン
def login():
if "is_logged_in" in session and session["is_logged_in"] is True:
return redirect("mypage") # ログイン済みならmypageへ
if request.method == "POST":
email = "example@example.com"
user = User.query.filter_by(email=email).first() # Emailでユーザー検索クエリを飛ばす
if user:
username = user.username
password = form.password.data
stored_salt = user.salt
hashed_password = sha256((username + password + stored_salt).encode("UTF-8")).hexdigest() # type: ignore
if user.hashed_password == hashed_password:
session["username"] = username
session["is_logged_in"] = True
return redirect("mypage")
else:
return redirect("login") # ログインが失敗したら戻す
# else以下にflashを設定すればエラーメッセージで表示もできる
else:
return redirect("login")
return render_template("login.html")
@app.route("/logout")
def logout():
session.pop("username", None) # session.popで指定したsessionを削除できる
session.pop("is_logged_in", False)
return redirect("/")
@app.route("/mypage")
def mypage():
if "is_logged_in" in session and session["is_logged_in"] is True:
username = session["username"]
return render_template("mypage.html", username=username, is_logged_in=True) # jinjaにusernameを渡しているので、html側でユーザー名に応じた条件分岐などができる
else:
return redirect("login")
if __name__ == "__main__":
app.run(debug=True, host="localhost", port=8080)
その他
今回sessionでムリヤリ作りましたが、ユーザー認証やログイン処理などはFlask_Loginを使ったほうがいいと思います。
HTMLやらCSS、JavaScriptはお好みで書いてください。
Flaskの基本はこちらの記事がおすすめです。
Flaskを利用したWebアプリでのCSRFの実装はこちらをどうぞ。