Python + Flask を使ったWebアプリ作成⑧(パスワードリセット編)

下記記事の続きです。






今回はパスワードリセット機能を追加します。


*参考



*環境

  • 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のセキュリティにひっかかってログインブロックされてエラーになってしまいました。
ローカルでの対処方法がわからなかったので時間のあるときに調べたいと思います。
パスワード再設定のメールを送る機能はよくあるケースなので、実装方法について覚えておこうと思います。

Previous
Next Post »

人気の投稿