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
, но если вы посмотрите документацию, то увидите, что второй аргумент - это соль, которая может привести к непредвиденным проблемам.