FlaskのエクステンションであるWTFormsで、htmlの入力フォームから送られてくるデータのセキュリティー・チェックをします。 この実行環境は、Pyhon3 + Flask なので、シンプルです。
WTFormでは、htmlの各入力フィールドから送信されてくる値のValidationが可能ですし、SECRET_KEYを設定することで、CSRF攻撃を防ぐことが可能です。
尚、ここの例は、アプリ・ファイルを分割しているぶん、Flaskに慣れていないと理解するのが大変かも知れません。 Flask-wtfを理解するだけなら、不必要に難しくなっているかも。
そこで、アプリ・ファイルが1つになっている、シンプルなサンプル・コードを作ってみました。
FLASK-WTF でセキュリティー対策をして、html経由でデータベースを操作する。
こちらのコードを理解する為に、この投稿を参照する方が分かり易いかもしれません。
◇flask-wtf をインストールする。
pip install flask-wtf
1. ファイル・ディレクトり構成
*config.pyをappフォルダに配置すると、以下のエラーとなる。
File "C:\Users\username\Desktop\microblog\app\__init__.py", line 3, in <module>
from config import Config
ModuleNotFoundError: No module named 'config'
2. ファイルの記述内容と説明
☆ redirect(url_for(' '))の中には、def で定義されたfunction名を記載する。
htmlからの入力をセキュリティー・チェックする為に、WTForms を利用する。
<input type = text name = username value = "{{ request.form.username }}">
<input type = submit value = Login>
WTFormでは、htmlの各入力フィールドから送信されてくる値のValidationが可能ですし、SECRET_KEYを設定することで、CSRF攻撃を防ぐことが可能です。
尚、ここの例は、アプリ・ファイルを分割しているぶん、Flaskに慣れていないと理解するのが大変かも知れません。 Flask-wtfを理解するだけなら、不必要に難しくなっているかも。
そこで、アプリ・ファイルが1つになっている、シンプルなサンプル・コードを作ってみました。
FLASK-WTF でセキュリティー対策をして、html経由でデータベースを操作する。
こちらのコードを理解する為に、この投稿を参照する方が分かり易いかもしれません。
◇flask-wtf をインストールする。
pip install flask-wtf
---/microblog
|---①microblog.py
|---② config.py
/app
|---③ __init__.py
|---④ routes.py
|---⑤ forms.py #今回、主に記述するファイル
/templates
|---⑥ login.html # forms.pyと連動するhtmlファイル
*config.pyをappフォルダに配置すると、以下のエラーとなる。
File "C:\Users\username\Desktop\microblog\app\__init__.py", line 3, in <module>
from config import Config
ModuleNotFoundError: No module named 'config'
2. ファイルの記述内容と説明
① microblog.py
このmicroblog.pyをflask runで起動する。
(from app import app)のみ
② config.py
Configの設定をConfigのclassの変数として定義している。
このようにConfigを書くファイルを分けた方が、アプリケーション・ファイルにルーティングから、処理までの全てを書き込むより、可読性・拡張性が向上するため、分けることが推奨されている場合もある。
このようにConfigを書くファイルを分けた方が、アプリケーション・ファイルにルーティングから、処理までの全てを書き込むより、可読性・拡張性が向上するため、分けることが推奨されている場合もある。
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
*os.environ.get()は、環境変数としてセットされていれば取得する。
*SECRET_KEYは、
>>> import secrets
>>> secrets.token_hex(16)
'5dc5812f1a51647c48b5c6abd4bd5eee'
で得られるランダムなテキストを使って、以下のようにべた書きでも良い
SECRET_KEY = ’5dc5812f1a51647c48b5c6abd4bd5eee’
*os.environ.get()は、環境変数としてセットされていれば取得する。
*SECRET_KEYは、
>>> import secrets
>>> secrets.token_hex(16)
'5dc5812f1a51647c48b5c6abd4bd5eee'
で得られるランダムなテキストを使って、以下のようにべた書きでも良い
SECRET_KEY = ’5dc5812f1a51647c48b5c6abd4bd5eee’
③ __init__.py
appパッケージが呼ばれると、このファイルが読み込まれる。
(内容)
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
④ routes.py
Flask Tutorialでは、view.py などと名前がつけられていたりする、URLとFunctionを ひもづける機能。
(内容)
from flask import render_template, flash, redirect, url_for
from app import app
from app.forms import LoginForm ♯ forms を別ファイルにしたため、LoginFormクラスをインポートする。
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Justine
posts = [
{
'author': {'username': 'Bieber'
'body': 'Good Morning!
},
{
'author': {'username': 'Timberlake
'body': 'Good Afternoon!'
}
]
return render_template('index.html', title='Home', user=user, posts=posts)
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm() # login Form の処理結果をformに格納
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect(url_for('index')) ♯ ログイン成功時にindex.htmlにリダイレクト
return render_template('login.html', title='Sign In', form=form)
* form.validate_on_submit()
Webから送られてくるデータが、有効なpost methodであるか, validatorで指定された要件を満たしているを検証。 GetであればFalseを返し、if文の処理をスキップし最終 行redirect(url_for('index'))が実行 される。
☆ redirect(url_for(' '))の中には、def で定義されたfunction名を記載する。
⑤ forms.py
htmlからの入力をセキュリティー・チェックする為に、WTForms
WT Formsでは、htmlの入力フィールドを、クラスで定義するのが特徴。
(内容)
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
# htmlでの入力フォームの定義
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password ',
validators=[DataRequired(), EqualTo('password')])
confirm_password = PasswordField('Confirm Password ',
validators=[DataRequired(), EqualTo('password')])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
* importしたStringField, PasswordField, BooleanField, SubmitFieldは、wtformsで定義されているcl ass (TextField, TextAreaField, RadioField(ラジオボタン), SelectField(selectフィールド, IntegerField) などもある。 TextAreaFieldは、以下のように行数も指摘できて便利。) {{ form.message(cols="40", rows="4")}}
*緑色の変数名は任意に決めてよい。
*水色の'Username'、'Password'・・・は、htmlの入力フィールドで使用されるラベル名
*水色の'Username'、'Password'・・・は、htmlの入力フィールドで使用されるラベル名
*validatorsで設定できる引数(各フィールド・タイプに対応する、validatorsは、wtformsからインポートしておく。
from wtforms.validators import DataRequired, Length, Email, EqualTo
・Length() : 入力可能なmin, maxのテキスト長
validators =[DataRequired(), Length(min=2, max=20)]
・Email() : @が入っているかのチェック。()内の引数は不要。
validators =[DataRequired,Email()]
・EqualTo() : 指定したフィールドと同じ値かチェック
validators =[DataRequired(), EqualTo('password')]
・SubmitField() : submit ボタン用フィールド
*Remenber_me: Cookieを利用して、セッションを保持する為のもの
booleanfiledなので、結果はTrue/False
*HTMLからの値の受取り
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
if form.email.data == 'admin@blog.com' and form.password.data == 'password':
flash('You have been logged in!', 'success')
return redirect(url_for('home'))
return render_template('login.html', title='Login', form=form)
* formを受け取る各ファンクションでは、form = ファンクションを変数に入れ、form = formで、
from wtforms.validators import DataRequired, Length, Email, EqualTo
・Length() : 入力可能なmin, maxのテキスト長
validators =[DataRequired(), Length(min=2, max=20)]
・Email() : @が入っているかのチェック。()内の引数は不要。
validators =[DataRequired,Email()]
・EqualTo() : 指定したフィールドと同じ値かチェック
validators =[DataRequired(), EqualTo('password')]
・SubmitField() : submit ボタン用フィールド
*Remenber_me: Cookieを利用して、セッションを保持する為のもの
booleanfiledなので、結果はTrue/False
*HTMLからの値の受取り
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
if form.email.data == 'admin@blog.com' and form.password.data == 'password':
flash('You have been logged in!', 'success')
return redirect(url_for('home'))
return render_template('login.html', title='Login', form=form)
* formを受け取る各ファンクションでは、form = ファンクションを変数に入れ、form = formで、
htmlに渡すことで、htmlで指定されたフォームでの記述が可能になる。
*htmlのフィールドから送られてくる値を取得する場合は、
form.email.data
*form.validate_on_submit()
htmlから送られたきて値が、formで指定した条件に合致するかを検証。 その他、不等式などを混在させた様々な条件を検証する方法は、ここに詳しい。
FlaskFormで簡単にバリデーションする方法
https://qiita.com/kotamatsuoka/items/c93129f6ade5974dc122
⑥ login.html
上述のLoginForm classで定義された変数については、細かく指定しなくても、 勝手にhtmlをレンダーしてくれる。
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
*{{ form.hidden_tag() }}と、configにSECRET_KEYが設定されていれば 、CSRF attacksをwtformが防いでくれる。(どういう仕組みかは、 知らない。)
*htmlで入力フォーム・フィールドの書き方
{{ form.username.label) }}
{{ form.username}}
のように記述。 <input>タグ内に nameを指定するなどの、postで受け取る際に、どのフィールドかを特定する為の記述は不要。
一般化すると、
{{ form.username.label) }}
{{ form.username}}
のように記述。 <input>タグ内に nameを指定するなどの、postで受け取る際に、どのフィールドかを特定する為の記述は不要。
一般化すると、
{{ form.<field_name>.label }} :ラベル名を付けたい時
{{ form.<field_name>.(size=32) }}:<input>に記述するCSSのクラス、ID
などを() 内に記述できる。この例は、フィールドのサイズの指定。クラスの指定は、
placeholderを記述することも
{{ form.username (placeholder='ここにユーザ名を書いてね!')}}
(参考) WTFを使わない場合のhtmlのフィールドの記述方法
などを()
{{ form.username(class="form-control form-control-lg
is-invalid") }}のように記述。
placeholderを記述することも
{{ form.username (placeholder='ここにユーザ名を書いてね!')}}
(参考) WTFを使わない場合のhtmlのフィールドの記述方法
(html)
<form action =" " method = post><input type = text name = username value = "{{ request.form.username }}">
<input type = submit value = Login>
*{{ error }}
Error messageの内容自体は自動で生成されているので、html 上でエラー・メッセージが表示される場所を書くだけで良い。
v alidatorのあるフィールドには、大抵、つけられる。 Validatorは複数つけられるので、errorの中のメッセージはリスト形式で格納されているので、for文で書き出し。
⑦ password hashing
DBに格納するpasswordをハッシュしておく。 これで、誰かがDBにアクセスできる状態になったとしても、パスワードが漏えいする心配がなくなる。
ハッシュ化にはいくつかやり方がある。
(1) flask bcryptを利用する。
from flask_bcrypt import Bcrypt
from werkzeug.security import generate_password_hash, check_password_hash
# wtformsからValidationErrorをインポート
フォームを指定したクラス内で、エラー発生時の処理に関するファンクションを定義。
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('That username is taken.')
def validate_username(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('That email is taken.')
この段階では、こんな感じです。
v
⑦ password hashing
DBに格納するpasswordをハッシュしておく。 これで、誰かがDBにアクセスできる状態になったとしても、パスワードが漏えいする心配がなくなる。
ハッシュ化にはいくつかやり方がある。
(1) flask bcryptを利用する。
>>>pip install flask-bcrypt
(flaskとbcryptの間はハイフン)from flask_bcrypt import Bcrypt
app = Flask(__name__)
app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
##ハッシュ化の使用例
@app.route("/register", methods=['GET', 'POST'])
def register():
if form.validate_on_submit():
hashed_password =
bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user =
User(username=form.username.data, email=form.email.data,
password=hashed_password)
db.session.add(user)
db.session.commit()
*.decode('utf-8')
これを付けることでsimpleなString型式にできる。 ないと、バイト形式となっている。
##ハッシュを平文に戻して、htmlから送信されてきた値と照合
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user =
User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password,
form.password.data):
login_user(user, remember=form.remember.data)
next_page = request.args.get('next')
return
redirect(next_page) if next_page else redirect(url_for('home'))
(2) Werkzeugを利用する。
Werkzeugは、Flaskのdependencyなので、Flask導入時についてくる。
使い方は、bcryptと同じ。
from werkzeug.security import generate_password_hash, check_password_hash
hash = generate_password_hash('foobar')
check_password_hash(hash, 'foobar')
⑧ ユーザ登録時の重複エラーチェックとメッセージ。
forms.py
### ユーザの重複チェック
# wtformsからValidationErrorをインポート
from wtforms.validators import DataRequired, Length, Email, EqualTo,
ValidationError
class RegistrationForm(FlaskForm):
・・・・
submit = SubmitField('Sign Up')
def validate_username(self, username):
user = User.query.filter_by(username=username.data).first()
if user:
raise ValidationError('That username is taken.')
def validate_username(self, email):
user = User.query.filter_by(email=email.data).first()
if user:
raise ValidationError('That email is taken.')
この段階では、こんな感じです。
0 件のコメント:
コメントを投稿