表单
表单是实现一个web程序非常重要的部分,依我之见,表单是整个应用系统的数据输入部分,不仅在整个程序的正常运行中起着非常大的作用,还深刻影响到整个应用系统的稳定性和性能,也是系统安全比较关心的一个部分。在此书中使用的是基于WTForms拓展的Flask-WTF,这个拓展不仅能在后端清晰地声明好表单的形式,还可以便捷的实现表单的验证,结合jinja渲染系统,还可以实现其在前端的表现。我在深入阅读中发现,定义好的扩展可以在后端程序和渲染时分别设置其html标签的属性,这样一来,一方面可以结合Bootstrap之类的ui框架,通过class属性设置其外观,另一方面在后端定义可以方便的实现复用属性部分的编写,前端设置可以实现不同页面的个性化需求。
具体用法,首先需要在表单模块中从flask-form导入FlaskForm作为后续表单类的父类,然后再从wtforms中导入需要使用到的表单类型和验证器,然后定义表单即可。其中验证器一般是html自身可以实现的简单表单验证,例如长度限制,数字范围、网址邮箱电话等,也有非常实用的html似乎无法直接实现的如正则表达式,限定值等等使用功能。需要注意的是,虽然在导入时验证器是以类的形式导入的,但在使用时需要加上括号,标识其为可调用对象。一个例子:
from flask_wtf import FlaskForm
from wtforms import SubmitField, StringField, TextAreaField
from wtforms.validators import DataRequired, Length
class MessageForm(FlaskForm):
title = StringField(
'title',
validators=[DataRequired(), Length(1, 50)],
render_kw={'placeholder': 'Message title / 标题'}
)
body = TextAreaField('message', validators=[DataRequired(), Length(1, 140)])
submit = SubmitField()
对于前端显示的html代码,可以通过前面说的两种方式传入属性。第一种方式就是如上面的代码显示的,在后端定义时,在对应的条目建立时,传入一个render_kw 的键值对形式的参数。另一种方法就是在jinja模板中,如下书写:
<div class="col-lg-6 mb-4">
{{ form.title(class_='form-control') }}
</div>
需要注意的是,表单类并不作为程序运行的上下文供模板调用,需要实例化后传入模板。在模板中使用时,如果要设置html标签的class属性,需要在后面加一个下划线,与python的class关键字有所区别。那么自定义验证器输出后是否可以在前端实现?(我猜自定义的可能不行,涉及到JavaScript的部分还是需要自己写)html自身不能实现的部分前端验证是什么原理?前端使用JavaScript写的一些规则是否仍然有效?
然而,WTForms做的仅仅是服务端的验证,稍微复杂一点的验证器需还是要是通过自己在前端手写JavaScript代码实现,所以以上的的问题很多都是根本不存在的。这个拓展在jinja模板中输出了html代码后,在上传视图函数接收前就不管了,前端依然是正常的处理,JavaScript也依然可以起作用。
后端又是如何处理呢?WTForms需要将用户上传的信息先储存在一个实例化的表单对象中,然后通过调用这个对象的validate()方法验证信息。通过验证后,进行处理(计算、调用数据库等),最后给客户端重定向一个GET请求,并给予用户适当的提示。在这个过程中,FlaskForm基类会自动从上下文中获取请求中的表单信息,无需手动执行。需要注意的是,向数据库内写入数据时,需要调用字段的data属性,而不是将整个表单的字段类写入。
关于CSRF
在这个扩展中,提到了很多关于防御CSRF攻击的事情。CSRF是Cross-site request forgery,跨站伪造请求攻击。它相比于XSS攻击来说,需要更严格的条件,因此在防御上也更有难度。当用户A与访问被攻击网站B,还存在一个恶意网站C,一般来说,进行CSRF攻击需要用户已经登陆了网站B,并且没有关闭网站B的页面(session有效)。在这个条件下,用户A访问网站C,触发C以用户A的身份请求对网站B进行操作,从而实现攻击,而请求网站B这个操作是A不知情的。这个攻击过程,需要注意,网站C并没有窃取任何用户A的信息,只是通过用户A的浏览器,在A不知情的情况下向B发送了攻击请求。
防御手段之一就是检查请求头的reference,如果不是来自于站内或者是指定的URL,就拒绝请求。然而这是不靠谱的,请求头很容易被篡改。在WTForms中,是使用csrf-token来防御的,在表单中添加一段随机的、加密的、有时效的token字符串字段,这也是一个非常常用的方法。在学习时看见有评论在提问,既然攻击者能伪造用户身份,你一条字符串怎么就不能伪造?首先,字符串不仅仅是随机的,更是被加密的,需要有服务端密钥才能正确伪造;其次,攻击者是通过浏览器发送请求,伪造用户身份,身份数据保存在浏览器cookies与session中,攻击对象是客户端,即使用户现在打开了一个有csrf-token的页面,他没办法从另一个页面中获取这个数据。还有一种方式是验证码,这种方式不仅可以防御CSRF攻击,还可以减少网络爬虫对服务端的骚扰。
蓝本与模块化
蓝本
当我第一次在flask中看到蓝本这个名词的的时候,我内心里马上浮现出一种“宏伟蓝图”的形象,以为这是一个了不得部分,他是不是可以提供各种有力的扩展,帮我搞定各种难题?随着我深入的学习,我发现我想错了。蓝本只是将原来app.py中的视图函数单独拿出来,根据功能或者域名,将视图函数们模块化与主程序分离。只不过视图函数是整个web程序中比较重要的一部分,牵涉到环境、路由和资源加载等一系列问题,值得单独拿出来研究。
蓝本的的引入,就不得不深入理解一下前面提到的关于url、端点和视图函数之间的关系。这三层之间,结合前面的路由表,可以看出,其实是一个两层映射的关系。url规则和端点的映射,端点和视图函数的映射。多个url可以对应到一个端点上,例如/ /home /index 三条url都可以绑定到首页的端点上。那为什么不直接对应到视图函数的函数名名上呢,需要中间进行一层端点的映射?蓝图的出现可以解决这个问题,通过端点的引入,可以为每个蓝图内部创建本地的命名空间。比如一个CMS系统,用户访问的网址首页是www.websit.com/index ,后台管理的网址首页是admin.websit.com/index ,在url规则看来,他们都是/index ,但是他们的前缀有所不同(通过注册蓝图时的subdomain参数实现),为了能对应到不同蓝本的同名视图函数上,需要使用带前缀的端点,类似于类的调用:admin.index pub.index 。
模块化
之前在数据库部分讨论过,flask的app实例需要创建后才能继续加载其他的拓展,这给不同拓展的相关代码的模块化处理带来了不便。作者当时的解决方法是先将app实例化,拓展对象也实例化完成后,通过函数传参的方式传递到拓展代码中,实现模块化。然而这种方法不仅代码冗余,逻辑上也不容易理解,虽然实现了模块化,但单独的拓展模块代码的代码可读性非常低(拓展和app在代码中只是参数名,没有上下文供理解)
事实上有更优的解决方案,flask及其拓展开发者们早就考虑到了这个问题,大部分flask相关拓展都可以在没有app实例的情况下先行定义,然后再在创建程序实例时使用object.init_app(app) 方法将拓展初始化。这种做法不仅实现了代码层面的模块化,更是架构上的模块化,flask实例不用写死在程序中,可以在主程序的__init__.py 文件中组织工厂函数(面向对象编程中创建对象实例的函数)
回到上面关于蓝本的讨论,蓝本使得冗长的视图函数代码可以模块化组织。开发者可以通过业务的实际需求将所有的函数进一步模块化,一套程序多个蓝本,将不同的程序功能分开,便于开发、维护。除了数据库模型和蓝本外,表单模型,模板渲染模型(本项目为Bootstrap)也可以模块化处理。
|