flask-用户认证(下)
来源:互联网 发布:嘉兴软件开发制作 编辑:程序博客网 时间:2024/06/05 09:28
确认账户
对于某些特定类型的程序,有必要确认注册时用户提供的信息是否正确。常见要求是能通过提供的电 子邮件地址与用户取得联系。
为验证电子邮件地址, 用户注册后,程序会立即发送一封确认邮件。新账户先被标记成未激活状态,用户点击邮件中的链接后,才能激活。账户确认过程中,往往会要求用户点击一个包含确认令牌的特殊 URL 链接。
1. 使用itsdangerous生成确认令牌
确认邮件中最简单的确认链接是 http://www.example.com/auth/confirm/<id> 这种形式的URL,其 中 id 是数据库分配给用户的数字 id。用户点击链接后,处理这个路由的视图函数就将收到的用户id作为参数进行确认,然后将用户状态更新为已激活。
但是这种方式不是很安全,只要用户能判断确认链接的格式,就可以随便指定URL中的数字,从而确认任意账户。解决方法是把 URL 中的 id换成将相同信息安全加密后得到的令牌。
itsdangerous 提供了多种生成令牌的方法。其中,TimedJSONWebSignatureSerializer 类生成具有 过期时间的 JSON Web 签名(JSON Web Signatures, JWS)。这个类的构造函数接收的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
dumps()方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。 为了解码令牌, 序列化对象提供了 loads()方法,其唯一的参数是令牌字符串。这个方法会检验签 名和过期时间, 如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正确或过期了,则抛 出异常。
我们可以将这种生成和检验令牌的功能可添加到 User 模型中。
【 app/models.py:确认用户账户】
from itsdangerous import TimedJSONWebSignatureSerializer as Serializerfrom flask import current_appclass User(UserMixin, db.Model): confirmed = db.Column(db.Boolean, default=False) def generate_confirmation_token(self, expiration=3600): s = Serializer(current_app.config['SECRET_KEY'], expiration) return s.dumps({'confirm': self.id}) def confirm(self, token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False if data.get('confirm') != self.id: return False self.confirmed = True # 修改 db.session.add(self) return True
generate_confirmation_token() 方法生成一个令牌,有效期默认为一小时。
confirm()方法检验令牌,如果检验通过,则把新添加的 confirmed 属性设为 True。
由于模型中新加入了一个列用来保存账户的确认状态,因此要生成并执行一个新的数据库迁移。
2. 发送确认邮件
/register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发 送确认邮件。
首先编辑邮件:
【confirm.txt】
Dear {{ user.username }},Welcome to Flasky!To confirm your account please click on the following link:{{ url_for('auth.confirm', token=token, _external=True) }}Sincerely,The Flasky TeamNote: replies to this email address are not monitored.
【confirm.html】
<p>Dear {{ user.username }},</p><p>Welcome to <b>Flasky</b>!</p><p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p><p>Alternatively, you can paste the following link in your browser's address bar:</p><p>{{ url_for('auth.confirm', token=token, _external=True) }}</p><p>Sincerely,</p><p>The Flasky Team</p><p><small>Note: replies to this email address are not monitored.</small></p>
默认情况下, url_for() 生成相对 URL,例如 url_for('auth.confirm', token='abc') 返回的字符串是 '/auth/confirm/abc'。这不能够在电子邮件中发送的正确URL。
_external=True 参数要求程序生成完整的 URL,其中包含协议(http://或 https:// )、主机名和端口。
【app/auth/views.py:能发送确认邮件的注册路由】
@auth.route('/register', methods=['GET', 'POST'])def register(): form = RegistrationForm() if form.validate_on_submit(): user = User(email=form.email.data, username=form.username.data, password=form.password.data) db.session.add(user) db.session.commit()
token = user.generate_confirmation_token() send_email(user.email, 'Confirm Your Account', 'auth/mail/confirm', user=user, token=token) flash('A confirmation email has been sent to you by emai return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form)
【 app/auth/views.py:确认用户的账户】
@auth.route('/confirm/<token>')@login_requireddef confirm(token): if current_user.confirmed: return redirect(url_for('main.index')) if current_user.confirm(token): flash('You have confirmed your account. Thanks!') else: flash('The confirmation link is invalid or has expired.') return redirect(url_for('main.index'))
Flask-Login 提供的login_required修饰器会保护这个路由,用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数。
这个函数先检查已登录的用户是否已经确认过, 如果确认过,则重定向到首页,因为很显然此时不 用做什么操作。 这样处理可以避免用户不小心多次点击确认令牌带来的额外工作。
由于令牌确认完全在 User 模型中完成,所以视图函数只需调用 confirm() 方法即可,然后再根据 确认结果显示不同的 Flash 消息。确认成功后, User 模型中 confirmed 属性的值会被修改并添加 到会话中,请求处理完后,这两个操作被提交到数据库。
每个程序都可以决定用户确认账户之前可以做哪些操作。 比如,允许未确认的用户登录,但只显示 一个页面,这个页面要求用户在获取权限之前先确认账户。这一步可使用 Flask 提供的before_request 钩子完成。对蓝本来说, before_request 钩子只能应用到属于蓝本的请求上。若 想在蓝本中使用针对程序全局请求的钩子, 必须使用before_app_request 修饰器。
【app/auth/views.py】
# 处理程序中过滤未确认的账户@auth.before_app_requestdef before_request(): if current_user.is_authenticated \ and not current_user.confirmed \ and request.endpoint \ and request.endpoint[:5] != 'auth.': return redirect(url_for('auth.unconfirmed')) @auth.route('/unconfirmed')def unconfirmed(): if current_user.is_anonymous or current_user.confirm return redirect(url_for('main.index')) return render_template('auth/unconfirmed.html')
同时满足以下 3 个条件时, before_app_request 处理程序会拦截请求。
- (1) 用户已登录(current_user.is_authenticated() 必须返回 True)。
- (2) 用户的账户还未确认。
- (3) 请求的端点(使用request.endpoint获取)不在认证蓝本中。访问认证路由要获取权限,路由的作用是让用户确认账户或执行其他账户管理操作。 如果请求满足以上 3 个条件,则会被重定向到 /auth/unconfirmed 路由,显示一个确认账户相关信息的页面。
显示给未确认用户的页面只渲染一个模板,其中有如何确认账户的说明,此外还提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。
【app/templates/auth/unconfirmed.html】
{% extends "base.html" %}{% block title %}Flasky - Confirm your account{% endblock %}{% block page_content %}<div class="page-header"> <h1> Hello, {{ current_user.username }}! </h1> <h3>You have not confirmed your account yet.</h3> <p> Before you can access this site you need to confirm your account. Check your inbox, you should have received an email with a confirmation link. </p> <p> Need another confirmation email? <a href="{{ url_for('auth.resend_confirmation') }}">Click here</a> </p></div>{% endblock %}
【app/auth/views.py:重新发送账户确认邮件】
@auth.route('/confirm')@login_requireddef resend_confirmation(): token = current_user.generate_confirmation_token() send_email(current_user.email, 'Confirm Your Account', 'auth/email/confirm', user=current_user, token=token) flash('A new confirmation email has been sent to you by email.') return redirect(url_for('main.index'))
这个路由为 current_user(即已登录的用户,也是目标用户)重做了一遍注册路由中的操作。这个路由也用 login_required 保护,确保访问时程序知道请求再次发送邮件的是哪个用户。
【注意】
凡是修改了models.py,都要执行数据库迁移和数据库更新两个操作:
python manage.py db migratepython manage.py db upgrade
管理账户
拥有程序账户的用户有时可能需要修改账户信息。
- 修改密码
安全意识强的用户可能希望定期修改密码。这是一个很容易实现的功能,只要用户处于登录状态,就 可以放心显示一个表单,要求用户输入旧密码和替换的新密码。
- 第1步:建立表单
【app/auth/forms.py:增加修改密码表单】
class ChangePasswordForm(Form): old_password = PasswordField('Old password', validators=[DataRequired()]) password = PasswordField('New password', validators=[ DataRequired(), EqualTo('password2', message='Passwords must match')]) password2 = PasswordField('Confirm new password', validators=[DataRequired()]) submit = SubmitField('Update Password')
- 第2步:建立模板
【app/templates/auth/change_password.html:增加修改密码模板】
{% extends "base.html" %}{% import "bootstrap/wtf.html" as wtf %}{% block title %}Flasky - Change Password{% endblock %}{% block page_content %}<div class="page-header"> <h1>Change Your Password</h1></div><div class="col-md-4"> {{ wtf.quick_form(form) }}</div>{% endblock %}
- 第3步:建立路由
【app/auth/views.py:增加修改密码路由】
@auth.route('/change-password', methods=['GET', 'POST'])@login_requireddef change_password(): form = ChangePasswordForm() if form.validate_on_submit(): if current_user.verify_password(form.old_password.data): current_user.password = form.password.data db.session.add(current_user) flash('Your password has been updated.') return redirect(url_for('main.index')) else: flash('Invalid password.') return render_template("auth/change_password.html", form=form)
- 重设密码
为避免用户忘记密码无法登入的情况,程序可以提供重设密码功能。与修改密码不同的是,安全起 见,有必要使用类似于确认账户时用到的令牌。用户请求重设密码后,程序会向用户注册时提供的电 子邮件地址发送一封包含重设令牌的邮件。用户点击邮件中的链接,令牌验证后,会显示一个用于输 入新密码的表单。
- 第1步:修改模型
【app/models.py:模型中增加重设密码相关函数】
def generate_reset_token(self, expiration=3600): s = Serializer(current_app.config['SECRET_KEY'], expiration) return s.dumps({'reset': self.id}) def reset_password(self, token, new_password): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return False if data.get('reset') != self.id: return False self.password = new_password db.session.add(self) return True
- 第2步:建立表单
【app/auth/forms.py:增加重设密码表单】
class PasswordResetRequestForm(Form): email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()]) submit = SubmitField('Reset Password') def validate_email(self, field): if User.query.filter_by(email=field.data).first() is None: raise ValidationError('Unknown email address.')
class PasswordResetForm(Form): email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()]) password = PasswordField('New Password', validators=[ Required(), EqualTo('password2', message='Passwords must match')]) password2 = PasswordField('Confirm password', validators=[Required()]) submit = SubmitField('Reset Password') def validate_email(self, field): if User.query.filter_by(email=field.data).first() is None: raise ValidationError('Unknown email address.')
- 第3步:建立模板
【app/templates/auth/reset_password.html:增加重设密码模板】
{% extends "base.html" %}{% import "bootstrap/wtf.html" as wtf %}{% block title %}Flasky - Password Reset{% endblock %}{% block page_content %}<div class="page-header"> <h1>Reset Your Password</h1></div><div class="col-md-4"> {{ wtf.quick_form(form) }}</div>{% endblock %}
邮件内容
【app/templates/auth/email/reset_password.html】
<p>Dear {{ user.username }},</p><p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p><p>Alternatively, you can paste the following link in your browser's address bar:</p><p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p><p>If you have not requested a password reset simply ignore this message.</p><p>Sincerely,</p><p>The Flasky Team</p><p><small>Note: replies to this email address are not monitored.</small></p>
【app/templates/auth/email/reset_password.txt】
Dear {{ user.username }},To reset your password click on the following link:{{ url_for('auth.password_reset', token=token, _external=True) }}If you have not requested a password reset simply ignore this message.Sincerely,The Flasky TeamNote: replies to this email address are not monitored.
-第4步:建立路由
【app/auth/views.py:增加重设密码路由】
@auth.route('/reset/', methods=['GET', 'POST'])def password_reset_request(): if not current_user.is_anonymous: return redirect(url_for('main.index')) form = PasswordResetRequestForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user: token = user.generate_reset_token() send_email(user.email, 'Reset Your Password', 'auth/email/reset_password', user=user, token=token) flash('An email with instructions to reset your password has been sent to you.') return redirect(url_for('auth.login')) return render_template('auth/reset_password.html', form=form) @auth.route('/reset/<token>', methods=['GET', 'POST'])def password_reset(token): if not current_user.is_anonymous: return redirect(url_for('main.index')) form = PasswordResetForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user.reset_password(token, form.password.data): flash('Your password has been updated.') return redirect(url_for('auth.login')) else: return redirect(url_for('main.index')) return render_template('auth/reset_password.html', form=form)
- flask-用户认证(下)
- flask-用户认证(上)
- 用户认证(二)【使用Flask-Login认证用户】
- Flask(8)-用户认证
- Flask:用户认证
- Flask Web 开发 用户认证
- Flask用户认证架构图
- (未完占坑)笔记:flask用户认证
- flask web开发-用户认证代码分析(三)
- flask-web开发-用户认证代码分析(四)
- Flask Web 开发 用户认证_2
- Flask Web 开发 用户认证_3
- Flask Web 开发 用户认证_4
- Flask Web 开发 用户认证_5
- Flask Web 开发 用户认证_6
- 《flask web开发》第八章 用户认证
- [python3.6 flask web学习]Flask用户认证框架
- Flask Web开发-用户认证部分代码分析(一)
- Xutils简介和基本使用
- 使用栈实现队列
- DevOps是MindSet:工具也好文化也罢,人员才是关键
- 关于Java中的i++和++i
- 数据结构-队列
- flask-用户认证(下)
- 代理商订货管理系统1.2版(分销模式+积分模式)正式上线
- 让我们的socket服务支持多线程
- 测试架构师之路—python基于大数据类型的持续化测试设计
- 深度探索C++对象模型———Data Member的布局
- 17. 信号量,共享内存和消息队列
- 文件上传注意事项
- 1427:砝码称重
- DevOps实践-打造自服务持续交付-上