问题描述
我一直在基本flaskblog上关注documentation。除了Corey的代码外,我还想添加一个逻辑,即用户必须先验证其电子邮件地址,然后才能登录。我想从危险的角度使用URLSafeTimedSerializer来做到这一点,就像Corey Schafer's awesome youtube tutorial所建议的那样。
整个令牌创建和验证过程似乎正常。不幸的是,由于我一般都非常了解python,因此我无法自行找到一种干净的方法将其保存到sqlite3 db中。在我的模型中,我创建了一个布尔列email_confirmed(默认值为False),我打算在验证过程后将其更改为True。我的问题是:当用户单击其自定义网址时,如何最好地识别该用户(向谁更改email_confirmed列)?最好将令牌也保存在db列中,然后按该令牌进行过滤以标识用户吗?
以下是一些相关代码:
我的模式中的用户类别。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),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,default=datetime.utcNow)
email_confirmed = db.Column(db.Boolean(),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
可能是相关的行。如上所述,由于用户尚未在此阶段登录,所以未进行所需的输入。
我对此表示感谢!
提前非常感谢!
解决方法
您问题的关键是:
我的问题是:当他单击其自定义网址时,如何最好地识别用户(该用户为谁更改email_confirmed列)?
可以看到答案in the example on URL safe serialisation using 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
,但是如果您使用look at the docs,则会看到第二个参数是盐,这可能会导致意想不到的问题。
感谢@exhuma。这是我最终使它工作的方式-另外,我还发布了电子邮件发送中以前缺少的部分。
我的模型中的用户类别。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),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,default=datetime.utcnow)
email_confirmed = db.Column(db.Boolean(),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)函数内部进行同样的检查以确保不发生此情况如果用户多次单击确认链接,则该过程可以重复。再次感谢!希望这对其他人有帮助!