一、编写忘记密码
1.1 成品的网站功能展示
为什么要向上面的人发邮件啊?
确定用户的身份:只有邮箱的主人才能看到这封重置密码的邮件。总不能让一个用户随便填写一个邮箱号,或填成别人的邮箱号,你也进不去这个邮箱啊。
1.2 确定用户身份的三种方式
以前,确定用户的身份是通过cookie;但是,现在是没有cookie的。怎么办?
推荐第二种和第三种,一个是直接将用户id信息发向客户端,另一种是间接发向。
二、 first_or_404
2.1 返回邮箱账号填写页面
在本项目中:
下面会报错,因为视图函数没有返回值。
在视图函数app|web|auth.py中:·
...
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
return render_template('auth/forget_password_request.html') # 对应的是get方法在浏览器中再次点击忘记密码按钮,进入到邮箱账号填写页面:
2.2 用户账号不存在的两种处理思路
因为邮箱账号是用户输入的,该邮箱账号可能是非法的,所以要先进行验证操作。
用户账号不存在的处理思路:
思路1:参考上面的登录视图函数的用户验证
if user:
pass
else:
flash('邮箱账户不存在')思路2:借助flask提供的快捷方法:first_or_404()
在视图函数app|web|auth.py中:·
...
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
if request.method == 'POST':
form = EmailForm(request.form) # 实例化一个EmailForm
if form.validate(): # 如果email 符合验证规则
account_email = form.email.data # 拿到用户的邮箱账号
user = User.query.filter_by(email=account_email).first_or_404()
return render_template('auth/forget_password_request.html')输入非法的邮箱账号,返回的页面结果没有任何错误提示:这是因为实例化的form,没有传入到render_template中
改正
在视图函数app|web|auth.py中:·
...
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
form = EmailForm(request.form) # 一定要将实例化的对象放到外面!不跟上面一样![警示]
if request.method == 'POST':
if form.validate():
account_email = form.email.data
user = User.query.filter_by(email=account_email).first_or_404()
return render_template('auth/forget_password_request.html', form=form) # 实例化的`form`,没有传入到`render_template`中输入非法的邮箱账号,返回的页面结果有了报错提示:
2.3 first_or_404()的作用
1.作用演示
输入一个符合邮箱标准、但是数据库中不存在的邮箱账号:
以上就是firs_or_404()的作用。即:抛出异常
2.两个好处
(1)遇到空值,抛出异常、后续代码不执行
如果是下面这种:此次查询没找到结果,user将被赋予一个空值,后面的流程也会继续执行。
user = User.query.filter_by(email=account_email).first()
pass
return render_template('auth/forget_password_request.html')如果是下面这种:一旦查询没找到结果,user被赋予一个空值,那么将会抛出异常、后续的代码将不执行了。
user = User.query.filter_by(email=account_email).first_or_404()
pass
return render_template('auth/forget_password_request.html')(2)判空操作中,避免每次都手动抛异常
因为判空操作经常被使用,所以上面的封装写法是非常有用的
user = User.query.filter_by(email=account_email).first()
if not user:
raise Exception
pass
return render_template('auth/forget_password_request.html')2.4 first_or_404()的原理机制
1.图示关系
2.源码关系
ctrl+b找不到?脚本或动态语言,有时候会有这个问题。去query对象中找。
看下abort()的源码:
三、 callable 可调用对象的意义
3.1 可调用对象:跟函数一样调用的对象
类的实例化对象,怎么也可以像函数一样的用小括号来调用呢?
细看对象的类Aborter()的源码:
只要一个类的内部,定义了上面那个特殊的__call__()方法,那么就可以把个类的实例化对象,当作函数一样来调用。
def abort(
status: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any
) -> "te.NoReturn":
return _aborter(status, *args, **kwargs) # 这里实质上就是调用类Aborter()下面的__call__()方法
_aborter: Aborter = Aborter()像_aborter这种,可以被当作函数来调用的对象,叫做可调用对象。
3.2 可调用对象的两个作用
有什么用,用黑点来调用不就可以了?
一般用于抽象编程。
普通的对象和函数混杂:
class A(): # 形式:类
def go(self):
return object()
class B(): # 形式:类
def run(self):
return object
def func(): # 形式:函数
return object
def main(param): # 我们不知道传入的参数的形式是类,还是函数?导致下面的接口不统一
a.go() # 类的调用方式
b.run()
func() # 函数的调用方式
main(A())
main(B())
main(func)可调用对象与函数混杂:
class A():
def __call__(self): # __call__()方法,让该类变成了可调用对象
return object()
class B(): # __call__()方法,让该类变成了可调用对象
def __call__(self):
return object
def func(): # 形式:函数
return object
def main(callable): # 既然知道是可调用对象,那就好好办了,一律按函数形式调用,接口统一
callable()
main(A())
main(B())
main(func)下面箭头所指的就是可调用对象
四、 HTTP Exception
关注下__call__()方法里边的相关代码:
class Aborter:
def __init__(
self,
mapping: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None,
extra: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None,
) -> None:
if mapping is None:
mapping = default_exceptions # 下面的mapping对象是由default_exceptions装载
self.mapping = dict(mapping)
if extra is not None:
self.mapping.update(extra)
def __call__(
self, code: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any
) -> "te.NoReturn":
from .sansio.response import Response
if isinstance(code, Response):
raise HTTPException(response=code) # 抛出异常
if code not in self.mapping:
raise LookupError(f"no exception for {code!r}") # 抛出错误
raise self.mapping[code](*args, **kwargs) # 抛出mapping对象里的异常扫描当前模块下所有的类,如果某一个类是继承自HTTPException的,那么就会把它存入default_exceptions空字典中。
default_exceptions: t.Dict[int, t.Type[HTTPException]] = {}
def _find_exceptions() -> None: # 若想要扫描一个文件或一个模块下面的所有的对象,可借鉴此思想
for obj in globals().values():
try:
is_http_exception = issubclass(obj, HTTPException)
except TypeError:
is_http_exception = False
if not is_http_exception or obj.code is None:
continue
old_obj = default_exceptions.get(obj.code, None)
if old_obj is not None and issubclass(obj, old_obj):
continue
default_exceptions[obj.code] = obj # 将所有的异常对象,存进空字典 default_exceptions中
_find_exceptions()上面讲的异常,与最终要探究的页面出现404这个现象有什么关系呢?
first_or_404抛出的不是基类HTTP Exception ,而是基类HTTP Exception 的一个子类。
异常的response对象在哪里构建呢 ?
五、 装饰器@ app_errorhandler:AOP的应用
5.1 实现自定义的404页面
如何实现自定义的404页面?
在app|templates|404.html中:
<h1>别瞎访问,没这个页面</h1>在视图函数app|web|auth.py中:
...
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
form = EmailForm(request.form)
if request.method == 'POST':
if form.validate():
account_email = form.email.data
try:
user = User.query.filter_by(email=account_email).first_or_404() # 去数据库的User表中查下,该邮箱对应哪个用户
except Exception as e:
return render_template('404.html')
return render_template('auth/forget_password_request.html', form=form) # 实例化的`form`,没有传入到`render_template`中输入一个符合邮箱标准、但是数据库中不存在的邮箱账号:
成功自定义了404页面:
5.2 体现AOP思想的装饰器@ app_errorhandler
1.在每个可能出现404的地方,都要try except 吗?太麻烦
不用。利用体现AOP思想的装饰器@ app_errorhandler。
在初始化文件app|web|__init__.py中:
from flask import Blueprint, render_template
web = Blueprint('web', __name__)
@web.app_errorhandler(404) # 监控所有状态码是404的http异常,从而实现返回自定义的404页面
def not_found(e): # 只监听404
return render_template('404.html'), 404
from app.web import book # 有多少视图函数分布的模块,都要在这里都要导入执行下,从而执行其代码、完成视图函数的注册
from app.web import auth
from app.web import drift
from app.web import gift
from app.web import main
from app.web import wish
# from app.web import user
from app.web import test上述的装饰器app_errorhandler属于蓝图对象,如果不使用蓝图对象,核心对象app中也有这个装饰器。
2.当然,return不只是可以返回上述渲染模板,还可以用其他的很多形式,比如:
from flask import Blueprint, render_template
web = Blueprint('web', __name__)
@web.app_errorhandler(404) # 监控所有状态码是404的http异常,从而实现返回自定义的404页面
def not_found(e): # 只监听404
return 'not found', 404
from app.web import book # 有多少视图函数分布的模块,都要在这里都要导入执行下,从而执行其代码、完成视图函数的注册
from app.web import auth
from app.web import drift
from app.web import gift
from app.web import main
from app.web import wish
# from app.web import user
from app.web import test浏览器页面:
上述体现的是AOP思想:面向切面编程。
即,不要零散的在每一个可能出现404的地方,去try except 去具体的执行,而是把处理的代码集中到一个地方。
本项目中,就是借助flask写好的装饰器@ app_errorhandler。
以后有此类需求,也可以自己编写这种装饰器。
六、 发送电子邮件
目前,如果可以根据用户提交过来的邮箱账号,确实查询到该用户。那么,在系统中我们就可以向该邮箱账号发送一封重置密码的电子邮件。
电子邮件在该项目中,是一种重要的通知手段。所以,很多的视图函数中,都需要发送电子邮件。
基于此,最好将发送电子邮件的业务,单独写到一个模块中。
6.1 安装邮件发送插件flask-mail,完成测试代码
Python的标准库中,其实是已经提供了相关电子邮件的接口的。但是接口偏底层、自己控制的参数多,不方便。
flask提供了一个发送电子邮件的插件flask-mail:
注意要在代码程序关闭运行的情况下安装。
既然是插件,那么就要在app的初始化时,注册一下。
在app|__init__.py中:
from flask_mail import Mail # 导入Mail类
mail = Mail # Mail类的实例化
def create_app():
'''
用于创建flask的核心对象app
'''
app = Flask(__name__) # 新的静态文件的路径,相对与app根目录的
app.config.from_object('app.secure')
app.config.from_object('app.setting')
register_blueprint(app)
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'web.login'
login_manager.login_message = '请先登录或注册'
mail.init_app(app) # 注册mail插件
db.create_all(app=app)
return app在自定义文件app|libs|mail.py中:
from app import mail # 导入在app的初始化文件中实例化的对象mail
from flask_mail import Message # 导入该插件的 Message 类
def send_mail(): # 标题 即下面配置的MAIL_USERNAME 正文 收件人
msg = Message('测试邮件', sender='1121611414@qq.com', body='Test', recipients=['1121611414@qq.com'])
mail.send(msg) # 因为是测试邮件,可以自己给自己发邮件在配置文件中,定义一系列的配置项:
在 app|secure.py中:
DEBUG = True
SQLALCHEMY_DATABASE_URI = 'mysql+cymysql://root:1234abcd@localhost:3306/fisher'
SECRET_KEY = 'qwertasdfghjklzxcvbnm123456789zx'
# email 配置 # 有选择性的深挖,一下配置不建议深挖
MAIL_SERVER = 'smtp.qq.com' # 采用的是腾讯的qq电子邮件服务器
MAIL_PORT = 465
MAIL_USE_SSL = True
MAIL_USE_TSL = False
MAIL_USERNAME = '1121611414@qq.com' # 自己的qq邮箱
MAIL_PASSWORD = 'siyekmnzbpnygbeg' # 自己邮箱的授权码,见下获得自己邮箱的授权码:
做一个简单的邮件发送测试:
在视图函数app|web|auth.py中:
...
@web.route('/reset/password', methods=['GET', 'POST']) # 支持两种http请求方式:get:显示一个页面,让用户来填写账号 post:处理和接受用户提交的信息
def forget_password_request():
form = EmailForm(request.form) # 将实例化的对象放到外面
if request.method == 'POST':
if form.validate(): # 参数校验,如果email符合验证规则
account_email = form.email.data
user = User.query.filter_by(email=account_email).first_or_404() # 去数据库的User表中查下,该邮箱对应哪个用户
from app.libs.email import send_mail # 导入上面定义好的函数
send_mail() # 调用下这个函数
return render_template('auth/forget_password_request.html', form=form) # 对应的是get请求qq邮箱中成功收到测试邮件:
6.2 结合具体业务,完成电子邮件发送
在自定义文件app|libs|mail.py中:
from app import mail # 导入在app的初始化文件中实例化的对象mail
from flask_mail import Message # 导入该插件的 Message 类
def send_mail(to, subject, template, **kwargs): # 新增四个惨呼,增加通用性、灵活性 # template:指定模板的名称 # **kwargs:传入到模板里面的一组参数
msg = Message('[鱼书]'+' '+subject,
sender=current_app.config['MAIL_USERNAME'],
recipients=[to])
msg.html = render_template(template, **kwargs)
mail.send(msg)在模板email/reset_password.html中:
在视图函数app|web|auth.py中:
...
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
form = EmailForm(request.form)
if request.method == 'POST':
if form.validate():
account_email = form.email.data
user = User.query.filter_by(email=account_email).first_or_404()
from app.libs.email import send_mail # 导入上面新定义的函数
send_mail(form.email.data, '重置你的密码', # 调用该函数,并传入参数
'email/reset_password.html', user=user, token='123123')
return render_template('auth/forget_password_request.html', form=form) qq邮箱中成功收到重置密码的邮件:
点击上述地址,结果如下:
这是因为上述URL对应的正是视图函数forget_password(),该函数未编写、还没有返回值。
注意:
如果是上线的产品,用以上的个人方式发送电子邮件是不合适的。应该使用企业电子邮件,得注册一个域名,按照腾讯的设置规则进行设置即可。
七、 使用 PY JWT生成令牌
7.1 编写视图函数forget_password()的业务逻辑
在视图函数app|web|auth.py中:
...
@web.route('/reset/password/<token>', methods=['GET', 'POST'])
def forget_password(token):
return render_template('auth/forget_password.html')再次点击上述URL地址,结果如下:
接下来做一些准备工作,已接受用户的新密码:即验证用户表单提交的两个密码。
在app|forms|auth.py中:
class ResetPasswordForm(Form): # 新定义一个类,用于校验提交的参数:新密码、确认密码
password1 = PasswordField(validators=[
DataRequired(),
Length(6, 32, message='密码长度至少需要在6到20个字符串之间'),
EqualTo('password2', message='再次输入的密码不相同')])
password2 = PasswordField(validators=[DataRequired(), Length(6, 32)])在视图函数app|web|auth.py中:
@web.route('/reset/password/<token>', methods=['GET', 'POST'])
def forget_password(token): # 注意,这里不要求必须登录。所以用户id就不能从current_user中拿到
form = ResetPasswordForm(request.form) # 调用上面的验证层类
if request.method == 'POST' and form.validate(): # 如果是POST请求,且通过了form的校验
pass # 更新密码
return render_template('auth/forget_password.html')在app|models|user.py中:
class User(Base, UserMixin):
...
@staticmethod
def reset_password(token, new_password): # 新定义一个函数,用于更新数据库中用户的密码
'''通过读取token中的用户id号,来知道该新密码是哪个用户的'''
pass原课程中:
在app|models|user.py中:
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
class User(Base, UserMixin):
# __tablename__ ='user1'
id = Column(Integer, primary_key=True)
nickname = Column(String(24), nullable=False)
phone_number = Column(String(18), unique=True)
_password = Column('password', String(128), nullable=False)
email = Column(String(50), unique=True, nullable=False)
confirmed = Column(Boolean, default=False)
beans = Column(Float, default=0)
send_counter = Column(Integer, default=0)
receive_counter = Column(Integer, default=0)
wx_open_id = Column(String(50))
wx_name = Column(String(32))
def generate_token(self, expiration=600): # 记录用户的ID号 # 必须是经过加密和编码的,不能是明文形式
s = Serializer(current_app.config['SECRET_KEY'], expiration) # 实例化一个序列化器
temp = s.dumps({'id': self.id}) # 用户信息的写入
return temp
-->
❌ImportError: cannot import name 'TimedJSONWebSignatureSerializer' from 'itsdangerous'更新:
安装pyjwt:
pipenv install pyjwt在app|models|user.py中:
import time
import jwt
def generate_token(self):
'''生成用于邮箱验证的token:记录用户的ID号 + 必须是经过加密和编码的,不能是明文形式'''
headers = {
'alg': 'HS256',
'typ': 'JWT'
}
key = current_app.config['SECRET_KEY'] # 密钥
exp = int(time.time() + 600) # 过期时间:当前时间的600秒后
payload = {
'name': 'dd',
'exp': exp
}
token = jwt.encode(headers=headers, key=key, payload=payload, algorithm='HS256')
return token在视图函数app|web|auth.py中:
@web.route('/reset/password', methods=['GET', 'POST']) # 支持两种http请求方式:get:显示一个页面,让用户来填写账号 post:处理和接受用户提交的信息
def forget_password_request():
form = EmailForm(request.form) # 将实例化的对象放到外面
if request.method == 'POST':
if form.validate(): # 参数校验,如果email符合验证规则
account_email = form.email.data
user = User.query.filter_by(email=account_email).first_or_404() # 去数据库的User表中查下,该邮箱对应哪个用户
from app.libs.email import send_mail
send_mail(form.email.data, '重置你的密码',
'email/reset_password.html', user=user,
token=user.generate_token()) # 调用下这个函数
return render_template('auth/forget_password_request.html', form=form) # 对应的是get请求在浏览器中:
上面的url地址看似是无意义得到,但是包含了用户的id号等信息。
打开以上链接,结果如下:
7.2 重置密码
目前,新密码、确认密码已经传进来了。
在app|models|user.py中:
...
class User(Base, UserMixin):
def generate_token(self):
...
@staticmethod
def reset_password(token, new_password): # 用于更新数据库中用户的密码
'''通过读取token中的上节写入的用户id号,知道所传进来的新密码是哪个用户的'''
key = current_app.config['SECRET_KEY'] # 密钥
try:
data = jwt.decode(token, key, algorithms="HS256")
except:
return False # 如果token过期或非法
uid = data.get('id')
with db.auto_commit():
user = User.query.get(uid)
user.password = new_password
return True在视图函数app|web|auth.py中:
...
@web.route('/reset/password/<token>', methods=['GET', 'POST'])
def forget_password(token): # 注意,这里不要求必须登录
form = ResetPasswordForm(request.form)
if request.method == 'POST' and form.validate(): # 如果是POST请求,且通过了form的校验
success = User.reset_password(token, form.password1.data) # 更新密码
if success:
flash('你的密码已经更新,请使用新密码登录')
return redirect(url_for('web.login'))
else:
flash('密码重置失败')
return render_template('auth/forget_password.html', form=form)7.3 单元测试
为了测试第二个视图函数,就要访问第一个视图函数。测试流程冗长,且可能是走很多遍。
当前项目还好,只有两个视图函数。
但是,在一些大型项目中,一个业务逻辑可能会涉及到十几个视图函数的调用流程。如果只想测试该流程中最后的几个视图函数,却不得不把前面所有的额流程都走一遍,这非常烦人。
- 能够确保所写的一个视图函数的正确性,甚至内部更小粒度的
send_mail()的正确性。 - 能够解决测试流程过于冗长的问题
编写单元测试的测试用例,伪造一些数据,以测试第二步视图函数。
| 单元测试 | 国内 | 国外 |
|---|---|---|
| 不怎么做 | 必须的环节 |
7.4 异步编程发送电子邮件
几个细节的问题:
(1)重置密码页面,输入邮箱账号提交后,邮件收到前,等待时间长、加载速度比较慢。
(2)收到邮件后,页面没有任何的提示,要去邮箱查看邮件。
(3)邮件发送成功后,不应该还是停留在原密码重置的页面,而是应该跳转到登录页面。
改正:
在视图函数app|web|auth.py中:
@web.route('/reset/password', methods=['GET', 'POST'])
def forget_password_request():
form = EmailForm(request.form)
if request.method == 'POST':
if form.validate():
account_email = form.email.data
user = User.query.filter_by(email=account_email).first_or_404()
send_mail(form.email.data, '重置你的密码','email/reset_password.html', user=user,
token=user.generate_token())
flash('一封邮件已发送到邮箱'+account_email+',请及时查收') # 邮件收到的提示
return render_template('auth/forget_password_request.html', form=form) 在上述邮件中点击url地址,进入密码重置页面,在填写完新密码、确认密码之后,成功跳转如下:
上述解决了后两个问题。
刚才加载缓慢,是因为send_mail()函数缓慢,而它缓慢是因为需要连接邮件服务器。连接过程、发送时间,不是由写代码的人决定的。
而页面的停顿,是由于必须等到send_mail()函数执行结束后才能最终return结束掉该视图函数的访问。也就是说,如果send_mail()函数时间非常长,那么页面就会一直处于停顿状态。
发送邮件需要很高的实时性吗?
不需要。可以把send_mail()函数放到另外的线程中,异步的执行整个邮件发送的操作。
| 对比 | current_app | app |
|---|---|---|
| 代理对象,是不真实的 | 实例化的核心对象,是真实的 | |
| 受到线程id的影响,会变 因为每次访问 current_app时,都会去栈中读取栈顶元素 | 无论何地引用,永远都是app=Flask() |
如何在send_mail()函数中,拿到真实的flask的核心对象,而不是current_app代理对象?
通过调用_get_current_object()来拿到。
在自定义文件app|libs|email.py中:
from threading import Thread
from flask import current_app, render_template
from app import mail # 导入在app的初始化文件中实例化的对象mail
from flask_mail import Message
def send_async_email(app, msg): # 定义一个新的线程
'''异步发送电子邮件'''
with app.app_context(): # 应该手动的将app_context上下文推入栈中
try:
mail.send(msg)
except Exception as e:
pass # 这里的异常处理,既可以记日志,也可以直接抛出来
def send_mail(to, subject, template, **kwargs):
msg = Message('[鱼书]'+' '+subject,
sender=current_app.config['MAIL_USERNAME'],
recipients=[to])
msg.html = render_template(template, **kwargs)
app = current_app._get_current_object() # 拿到真实的flask的核心对象
thr = Thread(target=send_async_email, args=[app, msg]) # 调用上面的新的线程,中的函数
thr.start()进入重置密码页面,输入邮箱账号提交后,加载速度变快、很快就收到了邮件。
由上可知:
异步编程不简单。
如果对性能要求不高,建议同步编程;
如果确实对性能要求高,首先考虑各种优化,实在考虑到极限了,再考虑异步编程。
所以,不要盲目追求并发、异步,绝大多数的人都用到。













































Comments | NOTHING