課題
- 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/
ソース
# -*- 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()
{% 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 %}
<!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>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>csrf_error</title>
</head>
<body>
csrf error page
</body>
</html>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>finish</title>
</head>
<body>
{{ username }}様 {{message}}
</body>
<!DOCTYPE html>
<html lang="ja>
<head>
<meta charset="UTF-8">
<title>メニュー</title>
</head>
<body>
Привет!
<a href="{{register_url}}">登録</a>
</body>
</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>