4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

はじめに

自分用のWebアプリでFlaskとSQLAlchemyを使う機会があったので、基本部分だけアウトプットします。
今回はUserテーブルでユーザー作成、認証、削除のみ実装します。
割とムリヤリ設計が多いです。

SQLAlchemyとは

pythonでよく使われているORマッパー。
オブジェクト指向が理解できてれば使えると思います。

環境

$ python -v
Python 3.11.0
requirements.txt
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用のモデルを作ります。

models.py
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を用いて復号化します。

ユーザー名で認証するシステムでも出来ると思います。

実コード

app.py
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の実装はこちらをどうぞ。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?