对于许多程序以及社交类的媒介,都需要对访问人有一个专门的认证状态(即登录账户),这会让程序记录和跟踪用户,以便提供更好地适配服务,提高用户体验!同样flask web开发时也是很重要的,这是许多网站所要提供的一类基础功能!
我们前面说过flask相对Django来说就是自由选配度高,二次开发方便,但相对来说,内聚程度也相对较低,例如,数据库和登录认证功能,flask需要开发者自己集成和组织该类模块,Django会很好地提供这类完整的服务,但也不会让你改动和变化太多!
所以,我们开发flask登录认证模块时,没有完整的包来解决,需要我们利用好几个库来集成来发出来。例如下面几个:
werkzeug
flask-login
itsdangerous
flask-migrate
flask-mail
flask-bootstrap
flask-wtf
由于在程序控制方面的不同,许多人的库还是有不同的,例如控制shell命令方面的click和flask-script.
如果在(服务器)数据库中直接保存用户输入的密码,那么有可能被别有用心的黑客,进行劫持服务器数据库,剽窃了用户密码,由于许多用户密码可能是其他不同网站的登录密码,那么账号许多也是由邮箱账户注册并命名的,这就会存在很大的风险,所以我们对密码进行一定的掩盖处理!
若想保证数据库中用户密码的安全,关键在于不能存储密码本身,而要存储密码的散列值。计算密码散列值的函数接收密码作为输入,使用一种或多种加密算法转换密码,最得到一个和原始密码没有关系的字符序列。核对密码时,密码散列值可代替原始密码,因为计算散列值的函数是可复现的:只要输入一样,结果就一样。
但是,不同用户的相同密码的散列值还是不同的,复现时还是要识别不同用户的,下面会有例子说明的!
计算密码散列值是个复杂的任务,很难正确处理。所以我们只需掌握设定方法,不必深究,了解下即可!
Werkzeug 中的 security 模块能够很方便地实现密码散列值的计算。这一功能的实现只需要两个函数,分别用在注册用户和验证用户阶段。
(1) generate_password_hash(password, method=pbkdf2:sha1, salt_length=8):这个函数将原始密码作为输入,以字符串形式输出密码的散列值,输出的值可保存在用户数据库中。method 和 salt_length 的默认值就能满足大多数需求。
(2) check_password_hash(hash, password):这个函数的参数是从数据库中取回的密码散列值和用户输入的密码。返回值为 True 表明密码正确。
下面在app/models.py中,修改数据库模型,以便能放入这两个功能:
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
#
password_hash = db.Column(db.String(128))
@property
def password(self):
raise AttributeError('密码不是可读属性!')
@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)
计算密码散列值的函数通过名为 password 的只写属性实现。设定这个属性的值时,赋值方法会调用 Werkzeug 提供的 generate_password_hash() 函数,并把得到的结果赋值给password_hash 字段。如果试图读取 password 属性的值,则会返回错误,因为生成散列值后就无法还原成原来的密码了。
verify_password 方 法 接 受 一 个 参 数( 即 密 码 ), 将 其 传 给 Werkzeug 提 供 的 check_password_hash() 函数,和存储在 User 模型中的密码散列值进行比对。如果这个方法返回True,就表明密码是正确的。
完成后,我们可以在terminal命令中验证密码散列值是否转换成功:
(venv) D:\E盘\python\Flask Web应用\社交博客程序实验版>python manage.py shell
>>> u=User()
>>> u.password='girl'
>>> u.password_hash
'pbkdf2:sha256:260000$rwZyFHZqzOi9iVAH$d5a172bcbbecdbb1df0f4ef5fe37831366d6a86a365
6ddc2d9ed708a1cc3b3e4'
>>> u.verify_password('girl')
True
>>> u.verify_password('man')
False
>>> u2=User()
>>> u2.password='girl'
>>> u2.password_hash
'pbkdf2:sha256:260000$yNrcoOZmjkDvAm14$2abb5e3821bb6d267834bdc7c6f63587ea880805bfde6d4c34f148b161601d90'
注意:即使用户 u 和 u2 使用了相同的密码,它们的密码散列值也完全不一样。
为了这个功能今后可持续使用,我们可以把上述测试写成单元测试,以便于重复执行。编写 3 个新测试,测试最近对 User 模型所做的修改。
在新建的test文件下新建test_user_model.py:
import unittest
from app import create_app, db
from app.models import User
class UserModelTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_password_setter(self):
u = User(password='cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password='cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
与用户认证系统相关的路由可在 auth 蓝本中定义。对于不同的程序功能,我们要使用不同的蓝本,这是保持代码整齐有序的好方法。
auth 蓝本保存在同名 Python 包中。蓝本的包构造文件创建蓝本对象,再从 views.py 模块中引入路由。
在app/auth/__init __.py中编写:
from flask import Blueprint
auth=Blueprint('auth',__name__)
from . import views
在app/auth/views.py添加如下路由:
from flask import render_template
from . import auth
@auth.route('/login')
def login():
return render_template('login.html')
注意,为 render_template() 指定的模板文件保存在 auth 文件夹中。这个文件夹必须在app/templates 中创建,因为 Flask 认为模板的路径是相对于程序模板文件夹而言的。如下所示:
auth 蓝本要在 create_app() 工厂函数中附加到程序上,在app/__init __.py中编写:
def create_app(config_name):
# ...
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
return app
注册蓝本时使用的 url_prefix 是可选参数。如果使用了这个参数,注册后蓝本中定义的所有路由都会加上指定的前缀,即这个例子中的 /auth。例如,/login 路由会注册成 /auth/login,在开发 Web 服务器中,完整的 URL 就变成了http://localhost:5000/auth/login。
用户登录程序后,认证状态要被记录下来,这样浏览不同的页面时才能记住这个状态。Flask-Login 是个非常有用的小型扩展,专门用来管理用户认证系统中的认证状态,且不依赖特定的认证机制。
可以用pip指令下载:
pip install flask-login
我们要在登录功能实现一些数据方面的交互,需要向数据库模型添加一些修改!
4 个方法可以在模型类中作为方法直接实现,不过还有一种更简单的替代方案。Flask-Login 提供了一个 UserMixin 类,其中包含这些方法的默认实现,且能满足大多数需求。
在app/models.py中:修改 User 模型,支持用户登录
from flask_login import UserMixin
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
上面添加了 email 字段。在这个程序中,用户使用电子邮件地址登录,因为相对于用户名而言,用户更不容易忘记自己的电子邮件地址。
app/__init __.py中:初始化 Flask-Login
from flask_login import LoginManager
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app(config_name):
# ...
login_manager.init_app(app)
# ...
LoginManager 对象的 session_protection 属性可以设为 None、‘basic’ 或 ‘strong’,以提供不同的安全等级防止用户会话遭篡改。设为 ‘strong’ 时,Flask-Login 会记录客户端 IP地址和浏览器的用户代理信息,如果发现异动就登出用户。login_view 属性设置登录页面的端点。
app/models.py:加载用户的回调函数
from . import login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
加载用户的回调函数接收以 Unicode 字符串形式表示的用户标识符。如果能找到用户,这个函数必须返回用户对象;否则应该返回 None。
为了保护路由只让认证用户访问,Flask-Login 提供了一个 login_required 修饰器。暂时不添加到程序脚本中,演示使用方法如下:
from flask_login import login_required
@app.route('/secret')
@login_required
def secret():
return '只有登录用户才能访问此页面!'
如果未认证的用户访问这个路由,Flask-Login 会拦截请求,把用户发往登录页面。
可以向用户的登录表单中包含一个用于输入电子邮件地址的文本字段、一个密码字段、一个“记住我”复选框和提交按钮。
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Length, Email
class LoginForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Length(1, 64),Email()])
password = PasswordField('密码', validators=[DataRequired()])
remember_me = BooleanField('勿忘我')
submit = SubmitField('登录')
电子邮件字段用到了 WTForms 提供的 Length() 和 Email() 验证函数。PasswordField 类表示属性为 type=“password” 的 元素。BooleanField 类表示复选框。
app/templates/base.html:
{% extends "bases.html" %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>你好, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}游客{% endif %}!h1>
div>
{% endblock %}
判断条件中的变量 current_user 由 Flask-Login 定义,且在视图函数和模板中自动可用。这个变量的值是当前登录的用户,如果用户尚未登录,则是一个匿名用户代理对象。如果是匿名用户(游客),is_authenticated() 方法返回 False。所以这个方法可用来判断当前用户是否已经登录。
app/auth/views.py:登录路由
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user
from . import auth
from ..models import User
from .forms import LoginForm
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('无效的邮件或密码.')
return render_template('auth/login.html', form=form)
当请求类型是 GET 时,视图函数直接渲染模板,即显示表单。当表单在 POST 请求中提交时,Flask-WTF 中的 validate_on_submit() 函数会验证表单数据,然后尝试登入用户。
为了登入用户,视图函数首先使用表单中填写的 email 从数据库中加载用户。如果电子邮件地址对应的用户存在,再调用用户对象的 verify_password() 方法,其参数是表单中填写的密码。如果密码正确,则调用 Flask-Login 中的 login_user() 函数,在用户会话中把
用户标记为已登录。login_user() 函数的参数是要登录的用户,以及可选的“记住我”布尔值,“记住我”也在表单中填写。如果值为 False,那么关闭浏览器后用户会话就过期了,所以下次用户访问时要重新登录。如果值为 True,那么会在用户浏览器中写入一个长期有效的 cookie,使用这个 cookie 可以复现用户会话。
提交登录密令的 POST 请求最后也做了重定向,不过目标 URL 有两种可能。用户访问未授权的 URL 时会显示登录表单,Flask-Login
会把原地址保存在查询字符串的 next 参数中,这个参数可从 request.args 字典中读取。如果查询字符串中没有 next 参数,则重定向到首页。如果用户输入的电子邮件或密码不正确,程序会设定一个 Flash 消息,再次渲染表单,让用户重试登录。
app/templates/auth/login.html:
{% extends "bases.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>用户登录h1>
div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to registera>.p>
div>
{% endblock %}
app/auth/views.py:退出路由
from flask_login import logout_user, login_required
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))
为了登出用户,这个视图函数调用 Flask-Login 中的 logout_user() 函数,删除并重设用户会话。随后会显示一个 Flash 消息,确认这次操作,再重定向到首页,这样登出就完成了。
还未创建用户注册功能,所以新用户可在 shell 中注册:
(venv) D:\E盘\python\Flask Web应用\社交博客程序实验版>python manage.py shell
>>> u = User(email='[email protected]',username='steve', password='123')
>>> db.session.add(u)
>>> db.session.commit()
程序需要用游客注册成用户后,才能识别并让用户登录。程序的登录页面中要显示一个链接,把用户带到注册页面,让用户输入电子邮件地址、用户名和密码。
在app/auth/forms.py中:用户注册表单
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class RegistrationForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Length(1, 64),Email()])
username = StringField('用户名', validators=[
DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'用户名必须只有字母、数字、点或 '
'下划线')])
password = PasswordField('密码', validators=[DataRequired(), EqualTo('password2', message='两次输入的密码必须保持一致!')])
password2 = PasswordField('确认密码', validators=[DataRequired()])
submit = SubmitField('注册')
def validate_email(self, field):
if User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('邮箱已经被注册了!')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('用户名已经被使用了!')
这个表单使用 WTForms 提供的 Regexp 验证函数,确保 username 字段只包含字母、数字、下划线和点号。这个验证函数中正则表达式后面的两个参数分别是正则表达式的旗标和验证失败时显示的错误消息(可以自己根据需求改变一下)。
安全起见,密码要输入两次。此时要验证两个密码字段中的值是否一致,这种验证可使用WTForms 提供的另一验证函数实现,即EqualTo。这个验证函数要附属到两个密码字段中的一个上,另一个字段则作为参数传入。
这个表单还有两个自定义的验证函数,以方法的形式实现。如果表单类中定义了以validate_ 开头且后面跟着字段名的方法,这个方法就和常规的验证函数一起调用。本例分别为 email 和 username 字段定义了验证函数,确保填写的值在数据库中没出现过。自定义的验证函数要想表示验证失败,可以抛出 ValidationError 异常,其参数就是错误消息。显示这个表单的模板是 /templates/auth/register.html。和登录模板一样,这个模板也使用wtf.quick_form() 渲染表单。
注册页面如下图:
登录页面要显示一个指向注册页面的链接,让没有账户的用户能轻易找到注册页面。
app/templates/auth/login.html:添加链接到注册页面的内容
<p>新用户
<a href="{{ url_for('auth.register') }}">单击此处完成注册a>.
p>
提交注册表单,通过验证后,系统就使用用户填写的信息在数据库中添加一个新用户。
在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.lower(),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/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
有必要确认注册时用户提供的信息是否正确。常见要求是能通过提供的电子邮件地址与用户取得联系。
为验证电子邮件地址,用户注册后,程序会立即发送一封确认邮件。新账户先被标记成待确认状态,用户按照邮件中的说明操作后,才能证明自己可以被联系上。账户确认过程中,往往会要求用户点击一个包含确认令牌的特殊 URL 链接。
确认邮件中最简单的确认链接是 http://www.example.com/auth/confirm/ 这种形式的URL,其中 id 是数据库分配给用户的数字 id。用户点击链接后,处理这个路由的视图函数就将收到的用户 id 作为参数进行确认,然后将用户状态更新为已确认。
但这种实现方式显然不是很安全,只要用户能判断确认链接的格式,就可以随便指定 URL中的数字,从而确认任意账户。解决方法是把 URL 中的 id 换成将相同信息安全加密后得到的令牌。
shell 会话演示使用 itsdangerous 包生成包含用户 id 的安全令牌:
(venv) D:\E盘\python\Flask Web应用\社交博客程序实验版>python manage.py shell
>>> from manage import app
>>> from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
>>> s = Serializer(app.config['SECRET_KEY'], expires_in = 3600)
>>> token = s.dumps({ 'confirm': 23 })
>>> token
b'eyJhbGciOiJIUzUxMiIsImlhdCI6MTYzMjc0ODA1NiwiZXhwIjoxNjMyNzUxNjU2fQ.eyJjb25maXJtIjoyM30.t9icK-Dmv2NlTUWTq8kRpVhpltnMr2fMfG_0GbwiM90r7_P_OuDr2i4jAULWYL9z2HSi7gBAintxegUBJEu
_4A'
>>> data = s.loads(token)
>>> data
{'confirm': 23}
>>>
TimedJSONWebSignatureSerializer 类生成具有过期时间的 JSON Web 签名(JSON Web Signatures,JWS)。这个类的构造函数接收
的参数是一个密钥,在 Flask 程序中可使用 SECRET_KEY 设置。
dumps() 方法为指定的数据生成一个加密签名,然后再对数据和签名进行序列化,生成令牌字符串。expires_in 参数设置令牌的过期时间,单位为秒。
为了解码令牌,序列化对象提供了 loads() 方法,其唯一的参数是令牌字符串。这个方法会检验签名和过期时间,如果通过,返回原始数据。如果提供给 loads() 方法的令牌不正确或过期了,则抛出异常。
在app/models.py中:添加确认用户账户模型
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from . import db
class 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}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
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。
除了检验令牌,confirm() 方法还检查令牌中的 id 是否和存储在 current_user 中的已登录用户匹配。即使恶意用户知道如何生成签名令牌,也无法确认别人的账户。
注意:每次修改数据库模型后,都要更新数据库!
python manage.py db migrate
python manage.py db upgrade
当前的 /register 路由把新用户添加到数据库中后,会重定向到 /index。在重定向之前,这个路由需要发送确认邮件。
app/auth/views.py:增加发送确认邮件部分的注册路由
from ..email import send_email
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data.lower(),username=form.username.data,password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, '确认你的账户',
'auth/email/confirm', user=user, token=token)
flash('已通过电子邮件向您发送确认电子邮件!')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
注意,即便通过配置,程序已经可以在请求末尾自动提交数据库变化,这里也要添加db.session.commit() 调用。因为提交数据库之后才能赋予新用户 id 值,而确认令牌需要用到 id,所以不能延后提交。
认证蓝本使用的电子邮件模板保存在 templates/auth/email 文件夹中,以便和 HTML 模板区分开来。
一个电子邮件需要两个模板,分别用于渲染纯文本正文和富文本正文。
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 Team
Note: replies to this email address are not monitored.
默认情况下,url_for() 生成相对 URL,例如 url_for(‘auth.confirm’, token=‘abc’) 返回的字符串是 ‘/auth/confirm/abc’。这显然不是能够在电子邮件中发送的正确 URL。相对 URL 在网页的上下文中可以正常使用,因为通过添加当前页面的主机名和端口号,浏览器会将其转换成绝对 URL。但通过电子邮件发送 URL 时,并没有这种上下文。添加到url_for() 函数中的 _external=True 参数要求程序生成完整的 URL,其中包含协议(http://或 https://)、主机名和端口。
app/auth/views.py:确认用户的账户
@auth.route('/confirm/' )
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('您已确认您的帐户!谢谢!')
else:
flash('确认链接无效或已过期!')
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:在 before_app_request 处理程序中过滤未确认的账户
@auth.before_app_request
def before_request():
if current_user.is_authenticated \
and not current_user.confirmed \
and request.endpoint \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
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 路由,显示一个确认账户相关信息的页面。
注意:如果 before_request 或 before_app_request 的回调返回响应或重定向,Flask会直接将其发送至客户端,而不会调用请求的视图函数。因此,这些回调可在必要时拦截请求。
显示给未确认用户的页面只渲染一个模板,其中有如何确认账户的说明,此外还提供了一个链接,用于请求发送新的确认邮件,以防之前的邮件丢失。
app/auth/views.py:重新发送账户确认邮件
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, '确认你的账户',
'auth/email/confirm', user=current_user, token=token)
flash('已通过电子邮件向您发送确认电子邮件!')
return redirect(url_for('main.index'))
这个路由为 current_user(即已登录的用户,也是目标用户)重做了一遍注册路由中的操作。这个路由也用 login_required 保护,确保访问时程序知道请求再次发送邮件的是哪个用户。
我们对账户管理还可以增加以下的功能。
(1)修改密码
(2)重设密码
(3)修改电子邮件地址
flask开发-数据库迁移问题解决:ERROR [flask_migrate] Error: Can‘t locate revision identified by ‘a1c25fe0fc0e‘
flask web开发-网页模板问题错误解决:raise TemplateNotFound(template) jinja2.exceptions.TemplateNotFound:
flask web开发问题解决:Exception: Install ‘email_validator‘ for email validation support.
<p>敬爱的{{ user.username }}用户,p>
<p>欢迎访问 <b>Flasky网站b>!p>
<p>请确认你的账户 <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">点击此处确认 herea>.p>
<p>或者,您可以在浏览器的地址栏中粘贴以下链接:p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}p>
<p>开发者,Steve Draw敬上p>
<p><small>注意:不监视对此电子邮件地址的回复!small>p>
{% extends "bases.html" %}
{% block title %}Flasky - 确认账户{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
你好, {{ current_user.username }}!
h1>
<h3>你仍然还没确认你的账户h3>
<p>
在访问此网站之前,您需要确认您的帐户。
检查您的收件箱,您应该已经收到一封带有确认链接的电子邮件。
p>
<p>
需要重新发送一封邮箱确认吗?
<a href="{{ url_for('auth.resend_confirmation') }}">点击此处a>
p>
div>
{% endblock %}
{% extends "bases.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - 注册页面{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>用户注册h1>
div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
div>
{% endblock %}
{% extends "bases.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>用户登录h1>
div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>新用户
<a href="{{ url_for('auth.register') }}">单击此处完成注册a>.
p>
div>
{% endblock %}
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required, current_user
from . import auth
from .. import db
from ..models import User
from ..email import send_email
from .forms import LoginForm, RegistrationForm
@auth.before_app_request
def before_request():
if current_user.is_authenticated \
and not current_user.confirmed \
and request.endpoint \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('无效的邮件或密码.')
return render_template('auth/login.html', form=form)
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('你已经退出登录!')
return redirect(url_for('main.index'))
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data.lower(),username=form.username.data,password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, '确认你的账户',
'auth/email/confirm', user=user, token=token)
flash('已通过电子邮件向您发送确认电子邮件!')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth.route('/confirm/' )
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('您已确认您的帐户!谢谢!')
else:
flash('确认链接无效或已过期!')
return redirect(url_for('main.index'))
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, '确认你的账户',
'auth/email/confirm', user=current_user, token=token)
flash('已通过电子邮件向您发送确认电子邮件!')
return redirect(url_for('main.index'))
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class LoginForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Length(1, 64),Email()])
password = PasswordField('密码', validators=[DataRequired()])
remember_me = BooleanField('勿忘我')
submit = SubmitField('登录')
class RegistrationForm(FlaskForm):
email = StringField('邮箱', validators=[DataRequired(), Length(1, 64),Email()])
username = StringField('用户名', validators=[
DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'用户名必须只有字母、数字、点或 '
'下划线')])
password = PasswordField('密码', validators=[DataRequired(), EqualTo('password2', message='两次输入的密码必须保持一致!')])
password2 = PasswordField('确认密码', validators=[DataRequired()])
submit = SubmitField('注册')
def validate_email(self, field):
if User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('邮箱已经被注册了!')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('用户名已经被使用了!')
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from flask import current_app
from flask_login import UserMixin
from . import db, login_manager
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', lazy='dynamic')
def __repr__(self):
return '' % self.name
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=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))
confirmed = db.Column(db.Boolean, default=False)
@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 generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
def __repr__(self):
return '' % self.username
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
最后,文中如有不足,敬请批评指正!