8
8

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.

Flask ログイン機能 CRUDとセットで実装してみた

Last updated at Posted at 2022-11-02

Flaskでログイン機能を使ってみたいな、と思い、
CRUDアプリに追加する形で作成しました!!
CRUDは以下の記事を参考にしています。

手順

  1. パッケージの準備
  2. PythonにおけるDBの準備
  3. サインアップ機能
  4. ログイン機能
  5. ログアウト機能
  6. できることリスト(宿題的ななにか)
  7. コードの全容

パッケージの準備

今回、CRUDアプリを作成しているので、その後、新たに必要となるパッケージをAnacondaでインストールします。

% conda install flask-login

CRUDの記事では、pipを使用していますが
flask関連は基本的にAnacondaでインストールできます!

PythonにおけるDBの準備

app.py
from flask import Flask, redirect,render_template, request
from flask_sqlalchemy import SQLAlchemy
+ from flask_login import LoginManager, UserMixin
+ import os

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
+ app.config['SECRET_KEY'] = os.urandom(24)
db = SQLAlchemy(app)
+ login_manager = LoginManager()
+ login_manager.init_app(app)

class Tweet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(20), nullable=False)
    body = db.Column(db.String(140), nullable=False)

+ class User(UserMixin, db.Model):
+ 	id = db.Column(db.Integer, primary_key=True)
+ 	username = db.Column(db.String(50), nullable=False, unique=True)
+ 	password = db.Column(db.String(25))

+ @login_manager.user_loader
+ def load_user(user_id):
+     return User.query.get(int(user_id))

色々気になる箇所があると思いますが、順に解説します〜!
CRUDからの変更箇所はハイライトしてあります!

秘密鍵の登録

app.config['SECRET_KEY'] = os.urandom(24)
現在ユーザーがログイン中なのか、ログアウト状態なのかなどのセッション情報を管理するため、Flaskアプリのコンフィグレーションに秘密鍵(SECRET_KEY)を定義します。

ログインマネージャー

login_manager = LoginManager()
login_manager.init_app(app)

Flask-loginを使用する上で、最も重要(活躍する)なClass、
LoginManagerを使っています。

ログインマネージャには、IDからユーザを読み込む方法、ログインが必要なときにユーザをどこに送るかなど、アプリケーションとFlask-Loginを連携させるためのコードが含まれています。
Flask-login公式より

色々ありますが、要は色々と便利な仕事してくれるから定義しようね!くらいでOKです!
(詳しく知りたい方はURL方覗いてみましょう)

また、login_manager.init_app(app)では、アプリのインスタンス変数を用いて
ログイン周りの初期化を行なってくれます。

Userクラス

class User(UserMixin, db.Model):
	id = db.Column(db.Integer, primary_key=True)
	username = db.Column(db.String(50), nullable=False, unique=True)
	password = db.Column(db.String(25))

若干Railsのモデル周りと似ていますが、DBに関しては
Flask-alchemyと同様に、Classで定義します。
このClassは、以下の図と同じ意味です。
image.png

言語化すると、以下のような状態です。

  • ID 主キーの整数
  • ユーザ名 50文字までの文字列型、値は必ず入る、DB内で唯一である必要あり
  • パスワード 25文字までの文字列型

load_user

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

flask-loginでは、セッションに保存されているユーザIDから
ユーザオブジェクトをリロードするために使用されるので
上記のようにload_userを定義しています!

DBをコマンドで作成

以下のコマンドを実行してDB作成しましょう!
CRUDの時と同じですね!

ターミナル
% python
>>> from app import db
>>> db.create_all()
>>> exit()

面倒だなぁ、と思う人はシェルスクリプトを作ってみると良いですね!
例はこちらです🙌

create_db.sh
#!/bin/sh

python -c "from app import db; db.create_all()"

先ほどのコマンドを./create_db.sh
ターミナルに打つだけで実行できるようにしたものです!
(もっと効率化したい場合は、Makefileという手段もあります✨)

サインアップ機能

準備が長々とありましたが、ここからが本番です🔥
早速コードをお見せします!

app.py
from flask import Flask, redirect,render_template, request
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin
+ from werkzeug.security import generate_password_hash
import os

# ~~~importからdeleteの後まで変更がないので、省略~~~
@app.route('/tweets/<int:id>/delete',methods=['GET'])
def delete(id):
    tweet = Tweet.query.get(id)
    #投稿を削除
    db.session.delete(tweet)
    #削除を反映
    db.session.commit()
    return redirect('/')

+ @app.route('/signup', methods=['GET', 'POST'])
+ def signup():
+     if request.method == "POST":
+         username = request.form.get('username')
+         password = request.form.get('password')
+         # Userのインスタンスを作成
+         user = User(username=username, password=generate_password_hash(password, method='sha256'))
+         db.session.add(user)
+         db.session.commit()
+         return redirect('login')
+     else:
+         return render_template('signup.html')

htmlのページはtemplates配下に新規作成!

signup.html
{% extends 'application.html' %}
{% block content %}
<h1>ユーザ登録</h1>
<form method="POST">
    <label for="">ユーザ名</label>
    <input type="text" name="username">
    <label for="">パスワード</label>
    <input type="password" name="password">
    <input type="submit" value="新規登録">
</form>
{% endblock %}

signup関数はPOSTメソッドがリクエストされた時、ユーザを新規登録する関数です。
以下に、解説を載せます!

パスワードの扱い

今回、パスワードはそのまま保存するのではなく、
SHA256というハッシュ関数を利用してハッシュ値として保存しています。
SHA256は簡易的なブロックチェーンの実装にも利用されることがありますが、
SHA256について気になる方は、以下のサイトを見てみると良いかもしれません!

DBの動き

DBの動きは、CRUDの時と同じですが
今回は、ユーザの情報をセッションに追加し
問題なければ、トランザクションをコミットするようになっています。

ログイン機能

app.py
from flask import Flask, redirect,render_template, request
from flask_sqlalchemy import SQLAlchemy
- from flask_login import LoginManager, UserMixin
+ from flask_login import LoginManager, UserMixin, login_user
- from werkzeug.security import generate_password_hash
+ from werkzeug.security import generate_password_hash, check_password_hash

# ~~~importからsignupの後まで変更がないので、省略~~~
@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        # Userのインスタンスを作成
        user = User(username=username, password=generate_password_hash(password, method='sha256'))
        db.session.add(user)
        db.session.commit()
        return redirect('login')
    else:
        return render_template('signup.html')

+ @app.route('/login', methods=['GET', 'POST'])
+ def login():
+     if request.method == "POST":
+         username = request.form.get('username')
+         password = request.form.get('password')
+         # Userテーブルからusernameに一致するユーザを取得
+         user = User.query.filter_by(username=username).first()
+         if check_password_hash(user.password, password):
+             login_user(user)
+             return redirect('/tweets')
+     else:
+         return render_template('login.html')

signup関数と流れはほとんど同じですが
若干注意点を以下にまとめています!

ユーザの取得方法

今回、ユーザはDBにある中で、名前を検索(filter_by)してヒットしたものの中で
一番上(first)にあるものを取得しています。
今回のDBでは、ユーザ名は主キーかつユニークなので
名前で取得しても問題はありません🙌

パスワードの検証

今回、パスワードの扱いとして、ハッシュ値で保存する形式を取っています。
そのため、check_password_hashでは入力として受け取ったパスワードを一度
ハッシュ値に変換後、DBから取得したユーザのパスワード(ハッシュ値)を比較します。
ハッシュ値が一致すれば、ログインできます✨

ログアウト機能

app.py
from flask import Flask, redirect,render_template, request
from flask_sqlalchemy import SQLAlchemy
- from flask_login import LoginManager, UserMixin, login_user
+ from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
from werkzeug.security import generate_password_hash, check_password_hash
import os

# ~~~importからloginの後まで変更がないので、省略~~~
@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        # Userテーブルからusernameに一致するユーザを取得
        user = User.query.filter_by(username=username).first()
        if check_password_hash(user.password, password):
            login_user(user)
            return redirect('/tweets')
    else:
        return render_template('login.html')

+ @app.route('/logout')
+ @login_required
+ def logout():
+     logout_user()
+     return redirect('login')

login_required

login_requiredは、ログインを必要とする関数の前に、
ルーティングと同じ感覚でつけることで、制限をつけることができます。
つまり、ログインしていないと実行できない機能を作ることに役立ちます✊

また、logout_userで、ログインの時と同様に
flask-loginがよしなにログアウトしてくれます!

できることリスト

ここまで色々とやってきましたが
記事が長くなってきたので
やってみると面白いかもな〜というリストを載せておきます!

実際に私が実装できたものを載せているので
トライしてみてください🔥

  1. DBのセッション・コミットに関するエラーハンドリング(初級)
  2. フラッシュを活用したアラート機能(初級)
  3. 投稿に作成時間・更新時間を追加する(中級)
  4. 投稿の編集・削除を投稿したユーザだけに限定する機能(中級〜上級)

コードの全容

app.py
from flask import Flask, redirect,render_template, request
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
from werkzeug.security import generate_password_hash, check_password_hash
import os

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SECRET_KEY'] = os.urandom(24)
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)

class Tweet(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(20), nullable=False)
    body = db.Column(db.String(140), nullable=False)

class User(UserMixin, db.Model):
	id = db.Column(db.Integer, primary_key=True)
	username = db.Column(db.String(50), nullable=False, unique=True)
	password = db.Column(db.String(25))

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

@app.route('/')
def top():
    return render_template('top.html')

@app.route('/tweets')
def index():
    tweets = Tweet.query.all()
    return render_template('tweets/index.html', tweets=tweets)

@app.route('/tweets/new',methods=['GET','POST'])
def create():
    if request.method == 'POST':
        # POSTメソッドの時の処理。
        title = request.form.get('title')
        body = request.form.get('body')
        tweet = Tweet(title=title,body=body)
        # DBに値を送り保存する
        db.session.add(tweet)
        db.session.commit()
        return redirect('/')
    else:
        # GETメソッドの時の処理
        return render_template('tweets/new.html')

@app.route('/tweets/<int:id>/edit',methods=['GET','POST'])
def update(id):
    tweet = Tweet.query.get(id)
    if request.method == 'GET':
        return render_template('tweets/edit.html',tweet=tweet)
    else:
        tweet.title = request.form.get('title')
        tweet.body = request.form.get('body')
        db.session.commit()
        return redirect('/')

@app.route('/tweets/<int:id>/delete',methods=['GET'])
def delete(id):
    tweet = Tweet.query.get(id)
    #投稿を削除
    db.session.delete(tweet)
    #削除を反映
    db.session.commit()
    return redirect('/')

@app.route('/signup', methods=['GET', 'POST'])
def signup():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        # Userのインスタンスを作成
        user = User(username=username, password=generate_password_hash(password, method='sha256'))
        db.session.add(user)
        db.session.commit()
        return redirect('login')
    else:
        return render_template('signup.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        # Userテーブルからusernameに一致するユーザを取得
        user = User.query.filter_by(username=username).first()
        if check_password_hash(user.password, password):
            login_user(user)
            return redirect('/tweets')
    else:
        return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect('login')

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

参考文献

8
8
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
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?