2

我一直在关注Corey Schafer 在基本烧瓶博客上的精彩 youtube 教程。除了 Corey 的代码,我想添加一个逻辑,用户必须在登录之前验证他们的电子邮件地址。我已经想用来自 itsdangerous 的 URLSafeTimedSerializer 来做到这一点,就像prettyprinted 在这里所建议的那样. 整个令牌创建和验证过程似乎有效。不幸的是,由于我对 python 的了解非常新鲜,我自己无法弄清楚如何将其保存到 sqlite3 数据库中。在我的模型中,我创建了一个带有 default=False 的布尔列 email_confirmed,我打算在验证过程之后将其更改为 True。我的问题是:当他点击他的自定义网址时,我如何最好地识别用户(为谁更改 email_confirmed 列)?将令牌保存在 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 = Truecurrent_user.email_confirm_date =datetime.utcnow可能是有问题的行。如上所述,由于用户在此阶段尚未登录,因此未进行所需的条目。我很感激这方面的任何帮助!提前非常感谢!

4

2 回答 2

4

感谢@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(token) 函数中进行相同的检查,以防止用户点击确认时重复此过程链接几次。再次感谢!希望这对其他人有帮助!

于 2020-09-07T21:03:46.913 回答
2

你的问题的关键是:

我的问题是:当他点击他的自定义网址时,我如何最好地识别用户(为谁更改 email_confirmed 列)?

答案可以在使用 itsdangerous 的 URL 安全序列化示例中看到。

令牌本身包含电子邮件地址,因为这是您在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,但是如果您查看文档,您会发现第二个参数是盐,这可能会导致意想不到的问题。

于 2020-09-05T13:03:42.167 回答