/ Python

Flaskで認証

前回作成したシステムに,認証機能を追加する.

app.pyの編集

まずは,app.pyを以下のように変更する.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from flask import Flask, render_template
from flask import request, jsonify
import json
from flask import session, redirect, url_for
import os

app = Flask(__name__)
# cookieを暗号化する秘密鍵
app.config['SECRET_KEY'] = os.urandom(24)


# 各route関数の前に実行される処理
@app.before_request
def before_request():
    # 静的ファイルへのアクセスについては、チェック対象としない
    if request.path.startswith('/static/'):
        return
    # セッションにusernameが保存されている.つまりログイン済み
    if session.get('username') is not None:
        return
    # リクエストパスがログインページに関する場合
    if request.path == '/login':
        return
    # ログインされておらず,ログインページに関するリクエストでない場合
    return redirect('/login')


# ログイン処理を行う
@app.route('/login', methods=['GET', 'POST'])
def login():
    # ログイン処理
    if request.method == 'POST' and _is_account_valid():
        # セッションにユーザ名を保存してからトップページにリダイレクト
        session['username'] = request.form['username']
        return redirect(url_for('index'))
    # ログインページに戻る
    return render_template('login.html')


# 個人認証を行い,正規のアカウントか確認する
def _is_account_valid():
    username = request.form.get('username')
    # この例では,ユーザ名にadminが指定されていれば正規のアカウントであるとみなしている
    # ここで具体的な個人認証処理を行う.認証に成功であればTrueを返すようにする
    if username == 'admin':
        return True
    return False


# ログアウト処理を行う
@app.route('/logout')
def logout():
    # セッションからusernameを取り出す
    session.pop('username', None)
    return redirect(url_for('login'))


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

@app.route('/postText', methods=['POST'])
def lower_conversion():
    text = request.json['text']
    if "ping" in text:
        return_data = {"result":"pong"}
        return jsonify(ResultSet=json.dumps(return_data))
    lower_text = text.lower()
    return_data = {"result":lower_text}
    return jsonify(ResultSet=json.dumps(return_data))


if __name__ == '__main__':
    app.run(host="127.0.0.1", port=8080)

flaskからsessionredirecturl_forをインポートし,乱数を生成するためにosをインポートした.
app.config['SECRET_KEY'] = os.urandom(24)でセッション情報を保存するcookieを暗号化する秘密鍵を生成し保存している.
ここで乱数生成のバイト長として24バイトを指定しているが,ここは十分な長さの鍵長を指定するべきである(つまりここに小さい値を指定するべきではない).
before_request()では,送られてくるすべてのリクエストを処理する前にユーザがログインしている状態であるかを確認している.ログインしていなければログインページにリダイレクトされ,ユーザにログイン処理を行わせる.
ログイン処理に関係する部分はlogin()_is_account_valid()で,特に認証処理は_is_account_valid()が受け持つ.
この例ではユーザ名にadminが入力されていた場合に認証に成功するという風にしているが,本来のシステムではこの部分にかっちりとした認証処理を組み込む.
ユーザのログアウト処理は,logout()で行う.

login.htmlの作成

次に,templatesディレクトリにlogin.htmlを作成し,以下のようにする.

<!DOCTYPE html>
<html lang=ja>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>HelloFlask</title>

    <!-- Bootstrap CDN -->
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">
  </head>
  <body>
    {% if error %}
      {{ error }}
    {% endif %}
    <nav class="navbar navbar-inverse">
      <div class="container-fluid">
        <div class="navbar-header">
          <a class="navbar-brand">HelloFlask</a>
        </div> <!-- navbar-header -->
        <ul class="nav navbar-nav navbar-right">
          <li><a href="#signinModal" data-toggle="modal">Sign in</a></li>
        </ul>
      </div> <!-- container-fluid -->
    </nav>
    <div class="container-fluid">
      <div class="row">
        <div class="col-sm-12 col-md-12">
          <h1 class="text-center">HelloFlask</h1>
        </div> <!-- mainform -->
      </div> <!-- row -->
    </div> <!-- container-fluid -->

    <!-- Login Modal -->
    <div id="signinModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
            <h1 class="modal-title">Sign in</h1>
          </div>
          <div class="modal-body">
            <form class="form col-md-12 center-block" method="POST">
              <div class="form-group">
                <input type="text" class="form-control input-lg" id="username" name="username" placeholder="Username"/>
              </div>
              <div class="form-group">
                <input type="password" class="form-control input-lg" id="password" name="password" placeholder="Password"/>
              </div>
              <div class="form-group">
                <button class="btn btn-primary btn-lg btn-block">Sign in</button>
              </div>
            </form>
          </div>
          <div class="modal-footer" style="border-top: 0px;"><!-- border-top: 0px で余計な横線を消す -->
              <button type="button" class="btn btn-danger" data-dismiss="modal" aria-hidden="true">Cancel</button>
          </div>
        </div>
      </div>
    </div>

    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  </body>
</html>

画面上部にナビゲーションバーがあり,この右端にあるSign inリンクを押すことでユーザ名のパスワードを入力させるモーダルが表示される.
このモーダルでユーザ名とパスワードを入力し,下部にあるSign inボタンを押すことでapp.pylogin()に処理が移り,ログイン処理を行うような流れになっている.

index.htmlの編集

最後に,index.htmlを以下のように少し編集する.

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8"></meta>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"></meta>
    <meta content="width=device-width, initial-scale=1" name="viewport"></meta>
    <title>HelloFlask</title>
    <!-- Bootstrap CDN -->
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css">

    <link rel="stylesheet" type="text/css" href="../static/css/sample.css">
  </head>
  <body>
    <nav class="navbar navbar-default">
      <div class="container-fluid">
        <div class="navbar-header">
          <a class="navbar-brand">HelloFlask</a>
        </div>
        <ul class="nav navbar-nav navbar-right">
          <li><p class="navbar-text">Signed in as: <a href="#" class="navbar-link" id="username">{{ session.username }}</a></p></li>
          <li><a href="/logout" id="logout">Logout</a></li>
        </ul>
      </div>
    </nav>
    <div class="container-fluid">
      <div class="row">
        <div class="col-sm-12">
          <h1 class="text-center" id="hello">Hello Flask</h1>
          <input type="text" class="form-control" id="text" placeholder="Please input text">
          <button class="btn btn-default" id="button">Change text</button>
        </div>
      </div>
    </div>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script type="text/javascript" src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="../static/js/sample.js"></script>
  </body>
</html>

追加したのはnavタグで括られた部分.
ログアウト処理を行えるようにナビゲーションバーを設置してその右端にログアウトリンクを置いた.
このリンクを押すことで,app.pylogout()に処理が移り,ログアウト処理を行うような流れになっている.


app.pyを起動して試してもらえれば分かるかとは思うが,これで基本的な認証処理が実装されたはずだ.
それぞれのユーザ,セッションに依存するようなデータに関してはユーザ名を格納したような感じでsessionの中に放り込んでいき,使う際にsession.get('鍵名')session['鍵名']として取り出して利用することになると思う.
また,実際に運用する際には_is_account_valid()に認証処理を組み込むことになろうかと思うが,セキュアな実装を心がけてほしい.

なお,このサンプルアプリはGitHubに公開している.