【Flask】ログイン機能を追加




## 設定方法
### LoginManager の設定

LoginManagerは、ユーザーのログイン状態を管理してくれる

```py
from flask_login import LoginManager

#インスタンス化
login_manager = LoginManager()
#アプリをログイン機能を紐付ける
login_manager.init_app(app)
#未ログインユーザーを転送する(ここでは'login'ビュー関数を指定)
login_manager.login_view = 'login'

#現在のログインユーザーの情報を保持し、必要なときに参照できるようになる。
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)
```
### モデルの設定
UserMixinでログインに必要な機能を継承する。
werkzeugでパスワードハッシュ化とチェック機能を追加。
```py
#UserMixin、ログイン・ログアウトで必要なライブラリをインポート
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
#パスワードをハッシュ化するライブラリをインポート
from werkzeug.security import check_password_hash, generate_password_hash
```

```py
#UserMixinをを継承
class User(db.Model, UserMixin):
  __tablename__ = 'user'
  id = db.Column(db.Integer, primary_key = True)
  email = db.Column(db.String(64), unique=True, index=True)
  username = db.Column(db.String(64), unique=True, index=True)
  password_hash = db.Column(db.String(128))

  #パスワードチェックする関数を追記
 　　def check_password(self, password):
    return check_password_hash(self.password_hash, password)
```

### ログイン用フォーム

```py
class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email(message='正しいメールアドレスを入力してください')])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('ログイン')
```

### ビュー関数
```py
@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        #フォーム入力したアドレスがDB内にあるか検索
        user = User.query.filter_by(email=form.email.data).first()
        if user is not None:
            #check_passwordはUserモデル内の関数
            if user.check_password(form.password.data):
                #ログイン処理。ログイン状態として扱われる。
                login_user(user)
                next = request.args.get('next')
                if next == None or not next[0] == '/':
                    next = url_for('user_maintenance')
                return redirect(next)
                
            else:
                flash('パスワードが一致しません')
        else:
            flash('入力されたユーザーは存在しません')

    return render_template('login.html', form=form)
```
未ログイン状態でログインが必要なページを訪れた場合は、クエリー文字列のあとログイン後に遷移するページとしてそのページのURLが表示される。
例)　"/register"がログイン必須のページだった場合、以下のURLが表示される。
/login?next=%2Fregister
　"%2F"は、"/"(スラッシュ)の意味

この場合、ログイン成功したら、"/register"に遷移する。

そのため、クエリー文字列に"next"がない、もしくはnextの1文字目が"/"ではない場合は、
ユーザー一覧画面へ遷移する。
"next"がない = 「通常どおり、ログイン状態でアクセスしようとした」　ので、ログイン後に本来リダイレクトすべきページに遷移する。

"next"がある = 「未ログインでログイン必須のページにアクセスしようとした」ので、そのユーザーがアクセスしようとしたページにリダイレクトさせる。

上記の条件分岐は以下のようなコードとなる。
```py
next = request.args.get('next')
if next == None or not next[0] == '/':
    next = url_for('user_maintenance')
return redirect(next)
```

### ログインページのテンプレート
```py
<h2>ログインページ</h2>
<div>
  <a href="{{ url_for('register' )}}">
      新規登録へ
  </a>
</div>

<form method="POST">
    <!-- CSRF対策(トークンが生成される) -->
  {{ form.hidden_tag() }}
  <div>
    {{ form.email.label }} {{ form.email() }}
    {% for error in form.email.errors %}
    <span style="color: red;">{{ error }}</span>
    {% endfor %}
  </div>
  <div>
    {{ form.password.label }} {{ form.password() }}
    {% for error in form.password.errors %}
    <span style="color: red;">{{ error }}</span>
    {% endfor %}
  </div>
    {{ form.submit() }}
</form>
```

### ログインユーザーを表示
is_authenticated はログインしているかどうかを判断する関数。
モデルにUserMixinを継承することで使えるようになる。
```py
  {% if current_user.is_authenticated %}

    <a class="nav-link" href="{{ url_for('logout') }}">ログアウト</a>

    {{ current_user.username }}

  {% endif %}
```

### 新規ユーザー登録時に、パスワードのハッシュ化
Python Property・・・クラスのインスタンスに保持するデータで、値の参照や変更方法を制限することが可能
```py
  #値の参照
  @property
  def password(self):
    raise AttributeError('password is not a readable attribute')
  #値の設定
  @password.setter
  def password(self, password):
      self.password_hash = generate_password_hash(password)
```
モデルクラスの初期設定の関数も修正。
"password_hash"から"hash"を取り除き、passwordはハッシュ化されていない状態で渡されることを明示する。
setterでパスワードを受け取ってハッシュ化して、その値を"password_hash"に入れられデータベースに登録されるという流れ。
```py
  def __init__(self, email, username, password):
    self.email = email
    self.username = username
    self.password = password
```

新規登録のビュー関数"register"も修正。
モデルクラスをインスタンス化する際の引数"password_hash"から"hash"を取り除く。
```py
@app.route("/register", methods=["GET", "POST"])
def register():
  form = RegistrationForm()
  
  if form.validate_on_submit():
    user = User(email=form.email.data, username=form.username.data, password=form.password.data)
    db.session.add(user)
    db.session.commit()
```

### ログアウト機能

```py
@app.route('/logout')
def logout():
    #現在のユーザーをログアウト状態にする
    logout_user()
    return redirect(url_for('login'))
```

### ログインユーザーのみアクセス可能にする

login_required　をインポート
```py
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required
```

@login_required　を付けることで、未ログインユーザーはログインページに転送されるようになる。
```py
@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))
```

以下のコードでリダイレクトするビュー関数と、エラーメッセージを定義する
```py
#ビュー関数"login"にリダイレクトするよう定義
login_manager.login_view = 'login'

# 未ログインユーザーにメッセージ表示
def localize_callback(*args, **kwarg):
    return 'このページにアクセスするには、ログインが必要です'
login_manager.localize_callback = localize_callback
```