LoginSignup
17
29

More than 5 years have passed since last update.

Python 軽量フレームワーク Flask と CSRF Protection を使う

Last updated at Posted at 2018-04-30

課題

  • Web サイト上で問い合わせを受け付ける、問い合わせ Web アプリケーションを作成する。「入力フォーム」「入力確認画面」「送信完了画面」の3画面からなり、少なくとも氏名・メールアドレス・問い合わせ内容を記入するものとします。
  • 開発言語や動作環境を含め、その他の仕様は指定しませんので、自由に作成してください。

フレームワーク選択

Python の軽量フレームワーク Flask を選択。

画面遷移と機能

  • index.html (メニュー)→ register.html (入力)→ confirm.html (確認)→ finish.html (完了)
  • どうセキュアにつくるかが課題なので、データベースへの書き込みなど実務面の機能はなし。

留意した点

  • 車輪を発明しない。フレームワーク内の機能を使う、具体的には
    • CSRF Protect
    • html エスケープ
    • email アドレスなど入力値 validate
    • セッション管理
    • テンプレートとプログラムの分離

CSRF protect

  • 入力 → 確認 → 終了 一連の流れでは、横からブラウザで同画面をアクセスしても許可しない。

セッション管理

  • クライアント(Web ブラウザ)の cookie でもたせるのはキー値のみ。クライアントのキー値をもとに cookie の中身はすべてサーバー側でもつ
  • 更にサーバーでもつ cookie の中身は暗号化される

html エスケープ

  • お問い合わせの textbox のみ改行(\r, \r\n)のみ <br/> に変形し、確認時の視認性を高める。
  • 他はフレームワーク側で文脈により自動で html エスケープがかかる(例えば、textbox 再入力の際の値入れは html エスケープをかけない)。

動作環境

Python + Windows10

> python --version
Python 2.7.14

https://www.python.org/ でダウンロード c:\Python27 にインストール。cygwin の python だといろいろうまく動かないと思われる。

PyCharm Professional

例えば email アドレスの validator があるなんてどこにも書いていないので、試行錯誤するときは PyCharm のサジェスチョン機能が重宝されます。関数のあとは二行あけろとか細かく指定されるので体で Python のお作法を覚えることができます。
メールアドレスで ac.jp を持っている人は一年間タダになります。無料の community edition もありますがどこまでつかえるのかは未確認です。
ダウンロードはこちらから 201801 Professional https://www.jetbrains.com/pycharm/

ディレクトリ構成

+--venv (Python Virtual Env PyCharm より自動生成)
|  |--Include
|  |--Lib
|  |--Scripts
|  +--tcl
+--www
   |--app.py
   |--flask_session (flask_session により自動生成)
   |  |--0aff4f53f81709b8a8d38705b0b1b093 (自動生成)
   |  |--2029240f6d1128be89ddc32729463129 (自動生成)
   |  +--a97766e7f7198c8c4ea4750802c0286a (自動生成)
   +--templates
      |--_formhelpers.html (入力画面ヘルパー)
      |--confirm.html
      |--csrf_error.html (CSRF Protect のエラー画面)
      |--finish.html
      |--index.html
      +--register.html

Python 拡張モジュールインストール

以下は Flask だけですが、他にも必要なので app.py の import ~ from の行を参照して入れてください。モジュールが入っていなければ Pycharm でうねうね下線が入ってます。
PyCharm の Teminal 画面より

> pip install Flask

起動

PyCharm の Teminal 画面より

> python app.py

閲覧

Web ブラウザから
http://localhost:5000/

ソース

app.py
# -*- coding: utf-8 -*-
from wtforms import BooleanField, StringField, PasswordField, validators
from wtforms.widgets import TextArea
from flask import render_template, redirect, request, url_for, Flask, session
from flask_session import Session
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect, CSRFError
import re
from jinja2 import evalcontextfilter, Markup, escape

app = Flask(__name__)
sess = Session()
csrf = CSRFProtect(app)

_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}')

app = Flask(__name__)


@app.template_filter()
@evalcontextfilter
# http://flask.pocoo.org/snippets/28/
def nl2br(eval_ctx, value):
    result = u'\n\n'.join(u'<p>%s</p>' % p.replace('\n', '<br/>\n') for p in _paragraph_re.split(escape(value)))
    if eval_ctx.autoescape:
        result = Markup(result)
    return result


class RegistrationForm(FlaskForm):  # FlaskForm にしないと CSRF Protect が有効にならない
    username = StringField(u"ユーザー名", [validators.DataRequired(), validators.Length(min=4, max=25)])
    email = StringField(u"メールアドレス", [validators.DataRequired(), validators.Email()])
    question = StringField(u"お問い合わせ内容", validators=[validators.DataRequired()],
                           widget=TextArea())
    password = PasswordField(u"パスワード", [
        validators.DataRequired(),
        validators.EqualTo('confirm', message=u"パスワードが一致しません")
    ])
    confirm = PasswordField(u"パスワード確認")
    accept_tos = BooleanField(u"私は規約に同意します", [validators.DataRequired()])


class ConfirmationForm(FlaskForm):
    accept_confirm = BooleanField(u"この内容で登録する(チェックなしはやり直し)", [validators.DataRequired()])


@app.errorhandler(CSRFError)
def handle_csrf_error(e):
    return render_template('csrf_error.html', reason=e.description), 400


@app.route('/', methods=['GET'])
def index():
    session.clear()
    return render_template('index.html', register_url=url_for('register'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    register_form = RegistrationForm(request.form,
                                     username=session.get('username', ''), email=session.get('email', ''),
                                     question=session.get('question', ''))
    # このトークンの正当性はform.validate_on_submit()で検証します
    if request.method == 'POST' and register_form.validate():
        session['username'] = request.form.get('username')
        session['email'] = request.form.get('email')
        session['password'] = request.form.get('password')
        session['question'] = request.form.get('question')
        confirm_form = ConfirmationForm(request.form)
        return render_template('confirm.html', username=session['username'], email=session['email'],
                               question=session['question'],
                               form=confirm_form, action_for=url_for('finish'), submit_value=u"よろしいです")
    return render_template('register.html', form=register_form, submit_value=u"入力内容を確認")


@app.route('/finish', methods=['POST'])
def finish():
    confirm_form = ConfirmationForm(request.form)
    if request.method == 'POST' and confirm_form.validate():
        username = session.get('username')
        session.clear()
        return render_template('finish.html', username=username,
                               message=u"ご登録ありがとうございました。")
    return redirect(url_for('register'))


if __name__ == "__main__":
    app.debug = True
    app.config.update(dict(
        SECRET_KEY="powerful secret key",
        WTF_CSRF_SECRET_KEY="a csrf secret key",
        SESSION_TYPE='filesystem'
    ))
    sess.init_app(app)
    csrf.init_app(app)
    app.run()
_formhelpers.html
{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}
confirm.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>confirm</title>
</head>
<body>
{% from "_formhelpers.html" import render_field %}
<form action="{{ action_for }}" method=post>
  {{ form.csrf_token }}
  <dl>
    ユーザー名: {{ username }}<br/>
    パスワード: {{ email }}<br/>
    お問い合わせ内容: {{ question | nl2br }}<br/>
    {{ render_field(form.accept_confirm) }}
  </dl>
  <p><input type=submit value={{submit_value}}>
</form>
</body>
csrf_error.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>csrf_error</title>
</head>
<body>
csrf error page
</body>
</html>
finish.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>finish</title>
</head>
<body>
{{ username }}様 {{message}}
</body>
index.html
<!DOCTYPE html>
<html lang="ja>
<head>
    <meta charset="UTF-8">
    <title>メニュー</title>
</head>
<body>
Привет!
<a href="{{register_url}}">登録</a>
</body>
</html>
register.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>register</title>
</head>
<body>
{% from "_formhelpers.html" import render_field %}
<form method=post>
  {{ form.csrf_token }}
  <dl>
    {{ render_field(form.username, size=40) }}
    {{ render_field(form.email, size=40) }}
    {{ render_field(form.password, size=40) }}
    {{ render_field(form.confirm, size=40) }}
    {{ render_field(form.question, rows=10, cols=40) }}
    {{ render_field(form.accept_tos) }}
  </dl>
  <p><input type=submit value={{submit_value}}>
</form>
17
29
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
17
29