今回はパスワードリセット機能を追加します。
*参考
- Python Flask Tutorial: Full-Featured Web App Part 10 - Email and Password Reset
- flask-mail / 公式ドキュメント
*環境
- MacOS
- Python 3.6.3
- Flask 1.0.2
*リセット機能
itsdangerous
を使うことでトークンでの認証を行うことができます。TimedJSONWebSignatureSerializer()
にSECRET_KEY
を渡すとトークンが生成されます。verify_reset_token()
は指定したトークンを持つデータがDBにいるか確認するために使います。<models.py>
from datetime import datetime
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer <-- 追加
from flaskblog import db, login_manager, app <-- appを追加
from flask_login import UserMixin
...
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(20), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
image_file = db.Column(db.String(20), nullable=False, default='default.jpg')
password = db.Column(db.String(60), nullable=False)
posts = db.relationship('Post', backref='author', lazy=True)
# ---- ↓追加ここから ----
def get_reset_token(self, expires_sec=1800):
s = Serializer(app.config['SECRET_KEY'], expires_sec)
return s.dumps({'user_id': self.id}).decode('utf-8')
@staticmethod
def verify_reset_token(token):
s = Serializer(app.config['SECRET_KEY'])
try:
user_id = s.loads(token)['user_id']
except:
return None
return User.query.get(user_id)
# ---- ↑追加ここまで ----
def __repr__(self):
return "User('{}', '{}', '{}')".format(self.username, self.email, self.image_file)
forms.py
に下記クラスを追加します。<forms.py>
...
class RequestResetForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')
def validate_email(self, email):
email = User.query.filter_by(email=email.data).first()
if email is None:
raise ValidationError('There is no account with that email. You must register first.')
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Reset Password')
パスワードとトークンをリセットするAPIを追加します。
<routes.py>
from flaskblog.forms import RegistrationForm, LoginForm, UpdateAccountForm, \
PostForm, RequestResetForm, ResetPasswordForm <-- 追加
...
@app.route('/reset_password', methods=['GET', 'POST'])
def reset_request():
if current_user.is_authenticated:
return redirect(url_for('home'))
form = RequestResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
send_reset_email(user)
flash('An email has been sent with instructions to reset your password', 'info')
return redirect(url_for('login'))
return render_template('reset_request.html', title='Reset Password', form=form)
@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_token(token):
if current_user.is_authenticated:
return redirect(url_for('home'))
user = User.verify_reset_token(token)
if user is None:
flash('That is an invalid or expired token', 'warning')
return redirect(url_for('reset_request'))
form = ResetPasswordForm()
if form.validate_on_submit():
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
user.password = hashed_password
db.session.commit()
flash('Your password has been updated! You are now able to log in', 'success')
return redirect(url_for('login'))
return render_template('reset_token.html', title='Reset Password', form=form)
リセットするメールアドレスを入力する画面を新規作成します。
<templates/reset_request.html>
{% extends "layout.html" %}
{% block content %}
<div class="content-section">
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset class="form-group">
<legend class="border-bottom md-4">Reset Password</legend>
<div class="form-group">
{{ form.email.label(class="form-control-label") }}
{% if form.email.errors %}
{{ form.email(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.email(class="form-control form-control-lg") }}
{% endif %}
</div>
</fieldset>
<div class="form-group">
{{ form.submit(class="btn btn-outline-info") }}
</div>
</form>
</div>
{% endblock content %}
新しいパスワードを入力する画面を新規作成します。
<templates/reset_token.html>
{% extends "layout.html" %}
{% block content %}
<div class="content-section">
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset class="form-group">
<legend class="border-bottom md-4">Reset Password</legend>
<div class="form-group">
{{ form.password.label(class="form-control-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class="form-group">
{{ form.confirm_password.label(class="form-control-label") }}
{% if form.confirm_password.errors %}
{{ form.confirm_password(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.confirm_password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.confirm_password(class="form-control form-control-lg") }}
{% endif %}
</div>
</fieldset>
<div class="form-group">
{{ form.submit(class="btn btn-outline-info") }}
</div>
</form>
</div>
{% endblock content %}
*メール送信機能を追加
Flaskの拡張機能flask-mail
を使ってパスワードリセット時にメールを送信する機能を追加します。ターミナルで下記コマンドを実行してインストールします。
$ pip install flask-mail
メール送信設定を追加します。
<
__init__.py
>import os <-- 追加
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
from flask_mail import Mail <-- 追加
app = Flask(__name__)
app.config['SECRET_KEY'] = 'cfb33786023cc152019e747a051f73c6'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'
# ----- ↓追加ここから -----
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('EMAIL_USER')
app.config['MAIL_PASSWORD'] = os.environ.get('EMAIL_PASS')
mail = Mail(app)
# ----- ↑追加ここまで -----
from flaskblog import routes
メールを送信する処理を追加します。
<routes.py>
from flaskblog import app, db, bcrypt, mail <-- mailを追加
from flask_mail import Message <-- 追加
...
def send_reset_email(user):
token = user.get_reset_token()
msg = Message('Password Reset Request',
sender='noreply@demo.com',
recipients=[user.email])
msg.body = '''To reset your password, visit the following link:{url}
If you did not make this request then simply ignore this email and no change will be made.
'''.format(url=url_for('reset_token', token=token, _external=True))
mail.send(msg)
ログイン画面にパスワードを忘れたときのためのリンクを追加します。
<templates/login.html>
{% extends "layout.html" %}
{% block content %}
<div class="content-section">
<form method="POST" action="">
{{ form.hidden_tag() }}
<fieldset class="form-group">
<legend class="border-bottom md-4">Join Today</legend>
<div class="form-group">
{{ form.email.label(class="form-control-label") }}
{% if form.email.errors %}
{{ form.email(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.email.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.email(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class="form-group">
{{ form.password.label(class="form-control-label") }}
{% if form.password.errors %}
{{ form.password(class="form-control form-control-lg is-invalid") }}
<div class="invalid-feedback">
{% for error in form.password.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% else %}
{{ form.password(class="form-control form-control-lg") }}
{% endif %}
</div>
<div class="from-check">
{{ form.remember(class="form-check-input") }}
{{ form.remember.label(class="form-check-label") }}
</div>
</fieldset>
<div class="form-group">
{{ form.submit(class="btn btn-outline-info") }}
<!-- ↓修正ここから -->
<small class="text-muted ml-2">
<a href="{{ url_for('reset_request') }}">Forgot Password?</a>
</small>
<!-- ↑修正ここまで -->
</div>
</form>
</div>
<div class="border-top pt-3">
<small class="text-muted">
Need Have An Account? <a class="ml-2" href="{{ url_for('register')}}">Sign Up Now</a>
</small>
</div>
{% endblock content %}
*画面
ログイン画面
ログインボタン横の「Forget Password?」のリンクをクリック後
メールアドレスを入力
メールを送信
*所感
メールクライアントとメール送信先を自分のGmailにしたのですが、Gmailのセキュリティにひっかかってログインブロックされてエラーになってしまいました。ローカルでの対処方法がわからなかったので時間のあるときに調べたいと思います。
パスワード再設定のメールを送る機能はよくあるケースなので、実装方法について覚えておこうと思います。
Sign up here with your email
ConversionConversion EmoticonEmoticon