Flaskで作るSNS　Flask(flask_loginによるログイン処理)編

#TL;DL
Udemyの下記講座の受講記録

Python+FlaskでのWebアプリケーション開発講座！！～0からFlaskをマスターしてSNSを作成する～
https://www.udemy.com/course/flaskpythonweb/

この記事ではFlask_loginによるログイン処理について記載する。

##flask_login
Flaskアプリケーションでログイン処理をかんたんに実装することができるライブラリ
https://flask-login.readthedocs.io/en/latest/

###フォルダ構成
login_sample
├── flaskr
│   ├── \_\_init\_\_.py
│   ├── forms.py
│   ├── models.py
│   ├── templates
│   │   ├── _formhelpers.html
│   │   ├── base.html
│   │   ├── home.html
│   │   ├── login.html
│   │   ├── register.html
│   │   ├── user.html
│   │   └── welcome.html
│   └── views.py
└── setup.py

※ここではログイン処理に必要なもののみ抜粋して記載する。

###ライブラリインストール

```shell
(flaskenv) (base) root@e8cf64ce12e9:/home/venv/flaskenv/login_sample# pip install flask_login
```

###コード

**\_\_init\_\_.py**

・「login_maneager.login_view」にログイン処理のメソッドを登録する。ここではアプリケーション名をBlueprintで「app」という名称に設定しているので、「app.login」となる。
・「login_manager.login_message」にはリダイレクトされたときに表示するフラッシュメッセージを登録する。指定しない場合はデフォルトメセージが利用される。
・LoginManagerを利用する場合は、「login_manager.init_app([name])」メソッドで初期化してアプリケーションを登録する。

```python
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager

# Flask-LoginとFlaskアプリケーションをつなぐ処理。
login_manager = LoginManager()
# ログインする際に実行される処理　アプリケーション名.login
login_manager.login_view = 'app.login'
# ログイン画面にリダイレクトされた際に表示されるメッセージ
login_manager.login_message = 'ログインしてください'

base_dir = os.path.abspath(os.path.dirname(__name__))
# db作成
db = SQLAlchemy()
# Migration用インスタンス作成
migrate = Migrate()

def create_app():
    # Flaskアプリケーション作成
    app = Flask(__name__)

    # DB定義
    app.config['SECRET_KEY'] = 'mysite'
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
        os.path.join(base_dir, 'data.sqlite')
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

    # アプリにBlueprintオブジェクトを登録
    from flaskr.views import bp
    app.register_blueprint(bp)

    # DBを使用するアプリケーションを初期化
    db.init_app(app)

    # migrationするflaskアプリとDBを初期化
    migrate.init_app(app, db)

    # loginManagerを初期化
    login_manager.init_app(app)
    return app
```

**views.py**
・「@login_required」デコレータをつけることで、該当のメソッドを実行する前にログインしているか確認し、ログインしていなければメソッドは実行されずに\_\_init\_\_.pyでlogin_manager.login_viewに定義されているメソッドを実行する。
・login_user関数にユーザー名を渡すことでログイン処理を実行する。第2引数にremember=Trueを渡すことで、ブラウザを閉じてもsession情報を残す事ができる。
・「request.args.get('next')」メソッドでは、リクエスト元画面で遷移先として指定していたrouteを取得することができる。これにより、直接遷移先を再指定しなくても意図した画面に遷移することが可能となる。


```python
from flask import Blueprint, request, render_template, redirect, url_for
from flask_login import login_user, login_required, logout_user
from flaskr.forms import LoginForm, RegisterForm
from flaskr.models import User

bp = Blueprint('app', __name__, url_prefix='')

@bp.route('/')
def home():
    return render_template('home.html')

# login_requiredにより、login_userが実行されていない場合以下の関数は実行されない。
# ログインしていない場合は、__init__.pyのlogin_viewに指定されている処理に遷移する。
@bp.route('/welcome')
@login_required
def welcome():
    return render_template('welcome.html')

@bp.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm(request.form)
    if request.method == 'POST' and form.validate():
        user = User.select_by_email(form.email.data)

        if user and user.validate_password(form.password.data):
            # login_user関数にユーザー名を渡すことでログイン処理を実行する。
            # remember=Trueとすることで、ブラウザを閉じてもsession情報を残す事が可能。
            login_user(user, remember=True)
            # このログインメソッドを呼び出した処理が本来の遷移先として指定していた
            # ページ(url)を取得する。
            next = request.args.get('next')
            if not next:
                next = url_for('app.welcome')

            return redirect(next)
    return render_template('login.html', form=form)

```

**models.py**
ポイント
・「@login_manager.user_loader」デコレータをつけたload_userメソッドでユーザーIDを取得して返すことにより、ログイン済みユーザーであることを確認する。この処理で返されるユーザー情報（userオブジェクト）が、home.htmlで利用されている「current_user」となる。
・UserMixinはflask-loginで必要なものをまとめたクラスであり、class定義時に必ず継承する必要がある。

```python
from flaskr import db, login_manager  # __init__.pyからインポートされる
from flask_bcrypt import generate_password_hash, check_password_hash
from flask_login import UserMixin

# セッションに保存されたログインユーザーを返すためにtemplateから呼ばれる。
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

# UserMixinはFlask-Loginを利用するユーザーに必須のオブジェクトを定義したもの
class User(UserMixin, db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), index=True)
    password = db.Column(db.String(128))

    def __init__(self, email, username, password):
        self.email = email
        self.username = username
        self.password = generate_password_hash(password)

    def validate_password(self, password):
        return check_password_hash(self.password, password)

    def add_user(self):
        with db.session.begin(subtransactions=True):
            db.session.add(self)
        db.session.commit()

    @classmethod
    def select_by_email(cls, email):
        return cls.query.filter_by(email=email).first()
```


**login.html**

```html
{% from "_formhelpers.html" import render_field %}
{% extends "base.html" %}
{% block content %}
<!-- get_flashed_messages()でリダイレクトされた際にメッセージを表示する。
    表示するメッセージは__init__.py のlogin_manager.login_messageで定義 -->
{% for message in get_flashed_messages() %}
    {{ message }}
{% endfor%}
<form method='POST'>
    {{ form.csrf_token }}
    {{ render_field(form.email) }}
    {{ render_field(form.password) }}
    {{ form.submit()}}
</form>
{% endblock %}
```

**home.html**

```html
{% extends 'base.html' %}
{% block content %}
<div>
    {% if current_user.is_authenticated %}
    <p>ログイン済み {{ current_user.username }}</p>
    {% else %}
    <p>ログイン or 登録してください。</p>
    {% endif %}
</div>
{% endblock %}
```