概要
ユーザーがパスワードを忘れた場合に、入力されたメールアドレス宛にパスワード更新用のURLを送り、パスワードの再設定を行えるような画面をFlaskで作る場合のサンプル。
サンプルコードではDB操作とメール送信については取り扱わずにスタブで済ませています。
バージョン情報
- Flask==1.1.1
- Flask-WTF==0.14.2
- itsdangerous==1.1.0
参考ページ
http://www.patricksoftwareblog.com/password_reset_via_email_link/
サンプルコードのフロー
- フォームにメールアドレスを入力してsubmit
- メールアドレスと時刻から作ったトークンをパラメータに持つURLがメール送信される
- URLからパスワードの再設定ページを開いて、パスワードを設定する
tokenの生成
URLに渡すtokenはitsdangerousのURLSafeTimedSerializerを利用する。
from itsdangerous.url_safe import URLSafeTimedSerializer def create_token(user_id, secret_key, salt): ''' user_idからtokenを生成 ''' serializer = URLSafeTimedSerializer(secret_key) return serializer.dumps(user_id, salt=salt) def load_token(token, secret_key, salt): ''' tokenからuser_idとtimeを取得 ''' serializer = URLSafeTimedSerializer(secret_key) return serializer.loads(token, salt=salt, return_timestamp=True)
下記のように呼び出せる。
secret_key = 'jugemjugemnoponpokorinnoponn' salt = 'shio' token = create_token('hogehoge', secret_key, salt) #=> ('ImhvZ2Vob2dlIg.Xl_Egw.Z_F3qYCxdu9W_awkz6by0Uxg0oo',) user_id, time = load_token(token, secret_key, salt) #=> ('hogehoge', datetime.datetime(2020, 3, 4, 15, 20, 31)
上記例では、serializer.loadsの箇所でreturn_timestampを指定して戻り値にtimestampを含めている。
max_ageを指定することで、有効期限切れのtokenであれば例外が出るようにすることもできる。
max_ageの単位は秒。
def load_token(token, secret_key, salt): ''' tokenからuser_idとtimeを取得 ''' serializer = URLSafeTimedSerializer(secret_key) return serializer.loads(token, salt=salt, max_age=60)
実行結果
SignatureExpired: Signature age 907 > 60 seconds
今回の処理ではmax_ageを指定した方が向いてそうなので、以後のサンプルでは後者を利用する。
また、トークンに適当な文字列を指定すると、BadSignature例外が発生する。
user_id = load_token('間違ったトークン', secret_key, salt) #=> BadSignature: No b'.' found in value
フォルダ構成
下記のような構成でファイルを作成する。
. ├── app.py └── templates ├── mail.html └── new_pwd.html
templates/mail.htmlがメールアドレスの入力画面。
templates/new_pwd.htmlが新規パスワード入力画面。
templates/mail.htmlの記述
メールアドレスを入力するフォーム画面。
flashメッセージやvalidate時のエラーメッセージについても実装している。
<form method="POST"> {% for message in get_flashed_messages() %} <p>{{ message }}</p> {% endfor %} {{ form.csrf_token }} {% for error in form.mail.errors %} <p style="color:red">{{ error }}</p> {% endfor %} {{ form.mail.label }} {{ form.mail(placeholder="メールアドレス", size=20, id="mail") }} <button>送信</button> </form>
こんなシンプルな画面を記述した。
app.pyの記述(mail側)
DB参照やメール送信については割愛して、画面にパスワード変更用のURLを表示する。
import flask import flask_wtf import wtforms from wtforms import validators from itsdangerous.url_safe import URLSafeTimedSerializer app = flask.Flask(__name__) app.secret_key = 'nanika secret key wo ireru' SALT = 'shio' @app.route('/mail', methods=['GET', 'POST']) def index(): ''' メールアドレスを入力してもらって パスワード変更画面のURLを通知する ''' form = AddressForm() # ポストの場合のメール送信とメッセージ表示 if flask.request.method == 'POST' and form.validate_on_submit(): # TODO DBを参照して存在するメールアドレスか確認(割愛) # 先ほど書いたcreate_tokenメソッドでtokenを生成 token = create_token(form.mail.data, app.secret_key, SALT) url = flask.url_for('new_pwd', token=token, _external=True) # TODO メール送信(割愛) # メール送信を割愛してURLをページ表示 flask.flash('メール送ったよ : %s' % url) # ページ表示 return flask.render_template( 'mail.html', form=form) @app.route('/new_pwd', methods=['GET', 'POST']) def new_pwd(): ''' 新規パスワード設定 ''' # TODO 後述 (url_forが動くようにスタブだけ先に記述) class AddressForm(flask_wtf.FlaskForm): mail = wtforms.StringField('mail', [ validators.Email(message='メールアドレスの形式が間違っています'), validators.InputRequired(message='メールアドレスを入力してください')])
flask runで実行。
validationエラー時。
実行成功時。
templates/new_pwd.htmlの実装
パスワード入力フォーム(確認用付き)に入力して送信できるページを用意する。
<form method="POST"> {% for message in get_flashed_messages() %} <p>{{ message }}</p> {% endfor %} {{ form.csrf_token }} <p>{{ mail_address }}</p> <p> {{ form.token() }} </p> <p> {% for error in form.new_pwd1.errors %} <p style="color:red">{{ error }}</p> {% endfor %} </p> <p> {{ form.new_pwd1.label }} {{ form.new_pwd1(placeholder="パスワード", size=20) }} </p> <p> {{ form.new_pwd2.label }} {{ form.new_pwd2(placeholder="パスワード(確認用)", size=20) }} </p> <button>送信</button> </form>
app.pyの記述(new_pwd側)
新規パスワード設定画面の実装。
tokenからメールアドレスを取得するload_tokenメソッドは下記の実装とする。有効時間は10分。
def load_token(token, secret_key, salt, max_age=600): ''' tokenからメールアドレスを取得 ''' serializer = URLSafeTimedSerializer(secret_key) return serializer.loads(token, salt=salt, max_age=max_age)
app.pyへの記述。
@app.route('/new_pwd', methods=['GET', 'POST']) def new_pwd(): ''' 新規パスワード設定 ''' if flask.request.method == 'GET': # アドレスの取得 try: token = flask.request.args.get('token') mail_address = load_token(token, app.secret_key, SALT) except Exception as e: return flask.abort(400) # ページ表示 form = NewPwdForm(token=token) return flask.render_template( 'new_pwd.html', form=form, mail_address=mail_address) else: form = NewPwdForm() # アドレスの取得 try: mail_address = load_token(form.token.data, app.secret_key, SALT) except Exception as e: return flask.abort(400) if form.validate_on_submit(): # TODO ここにパスワードのudpate処理 flask.flash('パスワードを更新しました') return flask.render_template( 'new_pwd.html', form=form, mail_address=mail_address) class NewPwdForm(flask_wtf.FlaskForm): token = wtforms.HiddenField('token', [ validators.InputRequired()] ) new_pwd1 = wtforms.PasswordField('パスワード', [ validators.InputRequired(), validators.EqualTo('new_pwd2')] ) new_pwd2 = wtforms.PasswordField('パスワード(確認用)', [ validators.InputRequired()] )
これでメールアドレス入力ページで取得したURLを叩くと、下記のような画面でパスワード入力ができる。
不正なtokenが入力された場合は400 Bad Request。
改定履歴
Author: Masato Watanabe, Date: 2020-03-08, 記事投稿