Flask Webapp - проверка электронной почты после регистрации - передовой опыт

Я слежу за замечательным учебником Кори Шафера на YouTube по базовому блогу flaskblog. В дополнение к коду Кори я хотел бы добавить логику, в которой пользователи должны проверять свой адрес электронной почты, прежде чем смогут войти в систему. Я решил сделать это с помощью URLSafeTimedSerializer из его опасного, как это предлагается здесь.. Кажется, что весь процесс создания и проверки токена работает. К сожалению, из-за моих очень свежих знаний о питоне в целом я не могу понять, как сохранить это в базе данных sqlite3. В своих моделях я создал логический столбец email_confirmed со значением default=False, который я собираюсь изменить на True после процесса проверки. Мой вопрос: как мне лучше всего идентифицировать пользователя (для которого нужно изменить столбец email_confirmed), когда он нажимает на свой настраиваемый URL? Будет ли хорошей практикой также сохранить токен в столбце db, а затем фильтровать по этому токену для идентификации пользователя?
Вот некоторые из соответствующих кодов:

Класс пользователя в моем modely.py

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_profile.jpg')
    password = db.Column(db.String(60), nullable=False)
    date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
    email_confirm_date = db.Column(db.DateTime)
    projects = db.relationship('Project', backref='author', lazy=True)


    def get_mail_confirm_token(self, expires_sec=1800):
        s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'], expires_sec)
        return s.dumps(self.email, salt='email-confirm')


    @staticmethod
    def verify_mail_confirm_token(token):
        s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
        try: 
            return s.loads(token, salt='email-confirm', max_age=60)
        except SignatureExpired:
            return "PROBLEM" 

Логика регистрации в моих маршрутах (с использованием схемы пользователя):

@users.route('/register', methods=['GET', 'POST'])
def register():
    if current_user.is_authenticated: 
        return redirect(url_for('dash.dashboard'))
    form = RegistrationForm()
    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()
        send_mail_confirmation(user)
        return redirect(url_for('users.welcome'))
    return render_template('register.html', form=form)


@users.route('/welcome')
def welcome():
    return render_template('welcome.html')


@users.route('/confirm_email/<token>')
def confirm_email(token):
    user = User.verify_mail_confirm_token(token)
    current_user.email_confirmed = True
    current_user.email_confirm_date = datetime.utcnow 
    return user

Последние части current_user.email_confirmed = True а также current_user.email_confirm_date =datetime.utcnowвероятно, строки, о которых идет речь. Как указано выше, желаемые записи не делаются, потому что пользователь еще не вошел в систему на этом этапе. Я благодарен за любую помощь в этом! Заранее большое спасибо!

1 ответ

Спасибо @exhuma. Вот как я в конечном итоге заставил это работать - также я публикую ранее отсутствующую часть отправки электронной почты.

Класс пользователя в моем models.py

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_profile.jpg")
    password = db.Column(db.String(60), nullable=False)
    date_registered = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    email_confirmed = db.Column(db.Boolean(), nullable=False, default=False)
    email_confirm_date = db.Column(db.DateTime)
    projects = db.relationship("Project", backref="author", lazy=True)

    def get_mail_confirm_token(self):
        s = URLSafeTimedSerializer(
            current_app.config["SECRET_KEY"], salt="email-comfirm"
        )
        return s.dumps(self.email, salt="email-confirm")

    @staticmethod
    def verify_mail_confirm_token(token):
        try:
            s = URLSafeTimedSerializer(
                current_app.config["SECRET_KEY"], salt="email-confirm"
            )
            email = s.loads(token, salt="email-confirm", max_age=3600)
            return email
        except (SignatureExpired, BadSignature):
            return None

Функция отправки почты в моем utils.py

def send_mail_confirmation(user):
    token = user.get_mail_confirm_token()
    msg = Message(
        "Please Confirm Your Email",
        sender="noreply@demo.com",
        recipients=[user.email],
    )
    msg.html = render_template("mail_welcome_confirm.html", token=token)
    mail.send(msg)

Логика регистрации в моем routes.py (с использованием схемы пользователя):

@users.route("/register", methods=["GET", "POST"])
def register():
    if current_user.is_authenticated:
        return redirect(url_for("dash.dashboard"))
    form = RegistrationForm()
    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()
        send_mail_confirmation(user)
        return redirect(url_for("users.welcome"))
    return render_template("register.html", form=form)


@users.route("/welcome")
def welcome():
    return render_template("welcome.html")


@users.route("/confirm_email/<token>")
def confirm_email(token):
    email = User.verify_mail_confirm_token(token)
    if email:
        user = db.session.query(User).filter(User.email == email).one_or_none()
        user.email_confirmed = True
        user.email_confirm_date = datetime.utcnow()
        db.session.add(user)
        db.session.commit()
        return redirect(url_for("users.login"))
        flash(
            f"Your email has been verified and you can now login to your account",
            "success",
        )
    else:
        return render_template("errors/token_invalid.html")

Только не хватает, с моей точки зрения простой условной логики, чтобы проверить, если email_confirmed = True перед входом в, а также тот же чек внутри confirm_email(маркер), чтобы не сделать этот процесс Повторяется в случае, когда пользователь нажимает на подтверждение ссылку несколько раз. Еще раз спасибо! Надеюсь, это поможет кому-то еще!

Ключ к вашему вопросу заключается в следующем:

Мой вопрос: как мне лучше всего идентифицировать пользователя (для которого нужно изменить столбец email_confirmed), когда он нажимает на свой настраиваемый URL?

Ответ можно увидеть в примере безопасной сериализации URL-адресов с использованием itsdangerous.

Сам токен содержит адрес электронной почты, потому что это то, что вы используете внутри своегоget_mail_confirm_token() функция.

Затем вы можете использовать сериализатор для получения адреса электронной почты из этого токена. Вы можете сделать это внутри своегоverify_mail_confirm_token()функция, но, поскольку это статический метод, вам все равно нужен сеанс. Вы можете передать это как отдельный аргумент, но без проблем. Вы также должны лечитьBadSignature исключение из itsdangerous. Тогда это станет:

@staticmethod
def verify_mail_confirm_token(session, token):
    s = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
    try: 
        email = s.loads(token, salt='email-confirm', max_age=60)
    except (BadSignature, SignatureExpired):
        return "PROBLEM"

    user = session.query(User).filter(User.email == email).one_or_none()
    return user

Будет ли хорошей практикой также сохранить токен в столбце db, а затем фильтровать по этому токену для идентификации пользователя?

Нет. Токен должен быть недолговечным и не должен храниться там.

Наконец, в вашем get_mail_confirm_token реализация, которую вы не используете URLSafeTimedSerializerкласс правильно. Вы передаете второй аргумент, называемыйexpires_sec, но если вы посмотрите документацию, то увидите, что второй аргумент - это соль, которая может привести к непредвиденным проблемам.

Другие вопросы по тегам