☆☆ 新着記事 ☆☆

2019年3月5日火曜日

Flask (1) WTForms でHTMLの入力フォームのセキュリティー対策

 
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. ファイル・ディレクトり構成
 

---/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.pyflask runで起動する。
(from app import app)のみ
 
 config.py
Configの設定をConfigclassの変数として定義している。
このように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’
 
 __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 などと名前がつけられていたりする、URLFunctionひもづける機能。
 
(内容)
   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')])
       remember_me = BooleanField('Remember Me')
       submit = SubmitField('Sign In')
 
* importしたStringField, PasswordField, BooleanField, SubmitFieldは、wtformsで定義されているclass (TextField, TextAreaField, RadioField(ラジオボタン), SelectField(selectフィールド, IntegerField)  などもある。 TextAreaFieldは、以下のように行数も指摘できて便利。) {{ form.message(cols="40", rows="4")}}

 
*緑色の変数名は任意に決めてよい。

*水色の'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で、
  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() }}と、configSECRET_KEYが設定されていればCSRF attackswtformが防いでくれる。(どういう仕組みかは、知らない。)
 
*htmlで入力フォーム・フィールドの書き方

 {{ form.username.label) }}
  {{ form.username}}
のように記述。 <input>タグ内に nameを指定するなどの、postで受け取る際に、どのフィールドかを特定する為の記述は不要。


 一般化すると、
 
{{ form.<field_name>.label }} :ラベル名を付けたい時
{{ form.<field_name>.(size=32) }}<input>に記述するCSSのクラス、ID
などを()内に記述できる。この例は、フィールドのサイズの指定。クラスの指定は、

{{ 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上でエラー・メッセージが表示される場所を書くだけで良い。

validatorのあるフィールドには、大抵、つけられる。 Validatorは複数つけられるので、errorの中のメッセージはリスト形式で格納されているので、for文で書き出し。



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')

これを付けることでsimpleString型式にできる。 ないと、バイト形式となっている。
 
 
##ハッシュを平文に戻して、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は、Flaskdependencyなので、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.')


この段階では、こんな感じです。
 


 


formに入力して、データを送ると、エラーメッセージがでます。








以上です。
 

0 件のコメント:

コメントを投稿