11、注册新用户
1、添加用户注册表单
app/auth/forms.py 用户注册表单
表单使用WTForms提供的Regexp验证函数,确保username字段只包含字母、数字、下划线和点号,这个验证函数中正则表达式后面的两个参数分别是正则表达式的旗标和验证失败时显示的错误消息
from flask_wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField, validators, ValidationError
from wtforms.validators import Length, Email, Required, Regexp, EqualTo
from ..models import User
class RegistrationFrom(Form): email = StringField('Email', validators=[Required(), Length(1, 64), Email()]) username = StringField('Username', validators=[Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0, 'Usernames, dots or underscores')]) password = PasswordField('Password', validators=[Required(), EqualTo('password2', message='Passwords must match.')]) password2 = PasswordField('Confirm password', validators=[Required()]) submit = SubmitField('Register') def validate_mail(selfself, field): if User.query.filter_by(email=field.data).first(): raise ValidationError('Eamil already registered') def validate_username(self, field): if User.query.filter_by(username=field.data).frist(): raise ValidationError('Username already in use.')
2、显示登录表单
app/templates/auth/register.html
{% extends "base.htmml" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky - Register {% endblock %} {% block page_content %} <div class="page-header"> <h1>Register</h1> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> {% endblock %}
3、链接到注册页面
app/templates/auth/login.html
登录页面要显示一个指向注册页面的链接,让没有账户的用户能轻易找到注册页面
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky - Login{% endblock %} {% block page_content %} <div class="page-header"> <h1>Login</h1> </div> <div class="col-md-4"> {{ wtf.quick_form(form) }} <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p> </div> {% endblock %}
注册新用户
1、用户注册路由
app/auth/views.py
提交注册表单,通过验证后,系统就使用用户填写的信息在数据库中添加一个用户
from flask import render_template, redirect, request, url_for, flash from flask_login import login_user, login_required, logout_user from . import auth from ..models import User,db from .forms import LoginForm,RegistrationFrom #蓝本中的路由和视图函数 @auth.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user is not None and user.verify_password(form.password.data): login_user(user, form.remeber_me.data) return redirect(request.args.get('next') or url_for('main.index')) flash('Invalid username or password.') return render_template('auth/login.html', form=form) #登出用户 @auth.route('/logout') @login_required def logout(): logout_user() flash('You have been logged out.') return redirect(url_for('main.index')) #用户注册路由 @auth.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationFrom() if form.validate_on_submit(): user = User(email=form.email.data, username=form.username.data, password=form.password.data) db.session.add(user) flash('You can now login.') return redirect(url_for('auth.login')) return render_template('auth/register.html', form=form)
确认账户
为确认注册用户时提供的信息是否正确,要求能通过电子邮件与用户取得联系,为验证电子邮件会,通过邮件发出一份确认邮件,账户确认中往往要求用户点击一个包含确认令牌的特殊URL链接
确认邮件最简单的确认链接是http://www.example.com/auth/confirm/<id>这种形式的URL,其中id是数据库分配给用户的数字id。用户点击这个链接后,处理这个路由的视图函数就将接收到的用户id作为参数进行确认,然后将用户状态更新为已确认
这种方式不安全,透露出了后台的信息,解决方法是把URL中的id换成将相同安全加密后的到的令牌
示例:在shell中使用itsdangerous包生成包含用户id的安全令牌
itsdangerous提供很多生成令牌的方式,其中,TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名 (JSON Web Signatures, JWS )。这个类的构造函数接受的参数是有个密钥,在Flask程序中使用SECRET_KEY设置
dumps()方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in参数设置令牌过期时间,单位为秒
loads()方法可以解码令牌,其唯一的参数是令牌支付串,这个方法会效验签名和过期时间,如果通过,返回原始数据,如果不正确,抛出异常
1、将生成和效验令牌的功能添加到User中
app/models.py 确认用户账户
from . import db from werkzeug.security import generate_password_hash, check_password_hash from flask_login import UserMixin from . import login_manager from flask import current_app from itsdangerous import TimedJSONWebSignatureSerializer as Serializer #定义数据库模型 class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) users = db.relationship('User', backref='role') def __repr__(self): return '<Role %r>' %self.name #加载用户的回调函数 @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) password_hash = db.Column(db.String(128)) email = db.Column(db.String(64), unique=True, index=True) #确认用户账户 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 #在User模型中加入密码散列 @property def password(self): raise AttributeError('password is not a readable attribute') @password.setter def password(self, password): self.password_hash = generate_password_hash(password) def verify_password(self, password): return check_password_hash(self.password_hash, password) def __repr__(self): return '<User %r>' %self.username
2、发送确认邮件
当前/register路由把新用户添加到数据库中后,会重定向到/index。在重定向之前,这个路由需要发送确认邮件
app/auth/views.py 能发送确认邮件的注册路由
通过配置,程序已经可以在请求末尾自动化提交数据库变化,这里要添加db.session.commit()调用。问题在于,提交数据库之后才能赋予新用户id值,而确认令牌需要用到id,所以不能延后提交
一个邮件需要两个模板,分别渲染纯文本文件和富文本文件
from ..email import send_email #用户注册路由 @auth.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationFrom() 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_mail(user.email, 'Confirm Your Accout', 'auth/email/confirm', user=user, token=token) flash('A confirmation email has been sent to you by email.') return redirect(url_for('main.index')) return render_template('auth/register.html', form=form)
3、确认邮件的纯文本正文
app/templates/auth/email/confirm.txt
默认情况下,url_for()生成相对URL,例如 url_for('auth.confirm', token='abc') 返回的字符串是 '/auth/confirm/abc'。这显然不是能够在电子邮件中发送的正确 URL。相对 URL 在网页的上下文中可以正常使用,因为通过添加当前页面的主机名和端口号,浏
览器会将其转换成绝对 URL。 但通过电子邮件发送 URL 时,并没有这种上下文。添加到url_for() 函数中的 _external=True 参数要求程序生成完整的 URL,其中包含协议(http://或 https://)、主机名和端口
Dear {{ user.username }},
Welcome to Flasky!
To confirm your accout please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored
4、确认用户的账户
app/auth/views.py
Flask-Login提供的login_required修饰器会保护这个路由,因此,当用户点击确认邮件中的链接后,要先登录,然后才能执行这个视图函数
函数中,先检查当前登录用户是佛已经确认过,确认过,重定向到首页。
from flask_login import current_user #确认用户的账户 @auth.route('/confirm/<token>') @login_required def 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'))
5、使用钩子在before_app_request处理程序中过滤未确认的账户
app/auth/views.py
同时满足以下3个条件时,before_app_request处理程序会拦截请求
(1) 用户已登录(current_user.is_authenticated() 必须返回 True)。
(2) 用户的账户还未确认。
(3) 请求的端点(使用 request.endpoint 获取)不在认证蓝本中。访问认证路由要获取权限,因为这些路由的作用是让用户确认账户或执行其他账户管理操作。
如果满足以上全部条件,则会被重定向到/auth/unconfirmed路由,显示一个确认账户相关信息的页面
#过滤未确认用户 @auth.before_app_request def before_request(): if current_user.is_authenticated() and not current_user.confirmed and request.endpoint[:5] != 'auth.' and request.endpoint != 'static': return redirect(url_for('auth.unconfirmed')) @auth.route('/unconfirmed') def unconfirmed(): if current_user.is_annoymous() or current_user.confirmed: return redirect(url_for('main.index')) return render_template('auth/unconfirmed.html')
6、重新发送账户确认邮件
app/auth/views.py
显示给未确认用户的页面只渲染一个模板,其中有如何确认账户的说明,此外还提供了一个链接, 用于请求发送新的确认邮件,以防之前的邮件丢失
这个路由也有login_required保护,确认访问时程序知道请求再次发送邮件的是哪个用户
#给未确认用户,显示页面,是否需要重发确认邮件 @auth.route('/confirm') @login_required def resend_confirmation(): token = current_user.generate_confirmation_token() send_mail(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'))