flask-login
flask-loginを使ってログイン機能を実装しているのですが、内部の仕組みを知りたいと思ったので、ドキュメントやソースコードから仕組みを調べてみました!
Userクラスに以下のプロパティ/メソッドを定義しましょう
- is_authenticated
- is_active
- is_anonymous
- get_id()
UserMixinクラスを継承してもいいです。
flask_loginの初期化をしましょう
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "login"
userをロードするためのcallback functionを定義しましょう
@login_manager.user_loader
def load_user(userid):
return User(userid)
ここでload_userが受け取る引数は、Userクラスで定義したget_id()が返す値です。
必要に応じて、get_id()がuserをDBから取得するために必要な値を返すようにしましょう
ログイン処理を実装しましょう
def login():
form = LoginForm()
if form.validate_on_submit():
login_user(user)
flask.flash('Logged in successfully.')
next = flask.request.args.get('next')
if not is_safe_url(next):
return flask.abort(400)
return flask.redirect(next or flask.url_for('index'))
return flask.render_template('login.html', form=form)
login_userにuserを渡すと、後は必要なことはflask-loginがやってくれます。
主にログイン状態の保持に必要な情報をセッションに保存しているのと、リクエストコンテキストのuserのアップデート、ログインシグナルの送信などをしてくれています
ログアウト処理を実装しましょう
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(somewhere)
これは非常にシンプルで、logout_userを呼んであげるだけです。logout_userの関数内で、ログイン中のuserを取得してきて、ログアウト処理を実行するので、userを渡す必要はありません。
大雑把に言うと、ログインと逆のことをしています。ログイン状態の保持のために、セッションに保持していた値を削除、リクエストコンテキストのuserをanonymous_userにアップデート、ログアウトシグナルの送信です。
ログイン前の挙動
login_requiredデコレータの中で、current_userを取得します。current_userはセッションに保存してある情報(具体的にはuser_id)から、userの取得を試みます。 [a]
ログイン前なので、セッションにはuser_idはないので、userの取得に失敗します。request_loaderが定義されていれば、それらを使用してログインを試みます。今回はrequest_loaderは定義していないので、current_userはAnonymous Userになります。
Anonymous Userクラスに定義されている、is_authenticatedはFalseを返すので、認証されていないと判定され、LoginMangerのunauthorized()が呼ばれて、ログイン画面へリダイレクトされます。
ログイン後の挙動
ログイン前の挙動の[a]と同じです。userの取得に成功した場合は、そのままuserを返します。is_authenticatedで認証済みのユーザーかどうかチェックします。Anonymous Userではないので、Userモデルで定義した通り、Trueが返ります。認証済みと判定されるため、後続の処理が続行されます
セキュリティ対策
-
HttpOnly属性
Flaskはデフォルトでは、cookie based sessionといってクライアント側でセッションの情報を保持しています。そのため、javascriptからこれらの情報にアクセスできないようにするために、SESSION_COOKIE_HTTPONLYはTrueにしておきましょう。(デフォルトはTrueです) [1] -
cookie based sessionのcookieの送信をHttpsのみに限定
SESSION_COOKIE_SECUREをTrueにしておきましょう。(デフォルトはFalseです) [2] -
セッションプロテクション
flask-loginがIPアドレスとuser-agentからハッシュ値を生成して、セッションに保持しているので、その値が前回と変わっていたら、デフォルトのbasicモードの挙動では、ログイン状態は破棄されます。ただし、flask側で、セッションの永続化設定がされている場合はログイン状態は維持されますが、このセッションはフレッシュではないというフラグが立ちます。strongモードでは、ハッシュが変わっている場合は、ログイン状態は破棄されます。
By default, it is activated in "basic" mode. It can be disabled in the app’s configuration by setting the SESSION_PROTECTION setting to None, "basic", or "strong".
When session protection is active, each request, it generates an identifier for the user’s computer (basically, a secure hash of the IP address and user agent). If the session does not have an associated identifier, the one generated will be stored. If it has an identifier, and it matches the one generated, then the request is OK.
If the identifiers do not match in basic mode, or when the session is permanent, then the session will simply be marked as non-fresh, and anything requiring a fresh login will force the user to re-authenticate. (Of course, you must be already using fresh logins where appropriate for this to have an effect.)
If the identifiers do not match in strong mode for a non-permanent session, then the entire session (as well as the remember token if it exists) is deleted. [3]
[1] https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_HTTPONLY
[2] https://flask.palletsprojects.com/en/1.1.x/config/#SESSION_COOKIE_SECURE
[2] https://flask-login.readthedocs.io/en/latest/#session-protection