✅【Flask笔记】第二节:HTTP
旧的笔记,以前存放的笔记软件停止运营了,重新迁移到博客上,删除和修改一些过时的内容。
0x00 Request对象
Request对象封装了从客户端发来的请求报文,我们能从它获取请求报文中所有的数据。
假设请求的URL是http://helloflask.com/hello?name=Grey
,当Flask接收到请求后, 请求对象会提供多个属性来获取URL的各个部分,常用的属性表如下:
属性 | 值 | 属性 | 值 |
---|---|---|---|
path | ‘/hello’ | base_url | ‘http://helloflask.com/hello' |
full_path | ‘/hello?name=Grey’ | url | ‘http://helloflask.com/hello?name=Grey' |
host | ‘helloflask.com’ | url_root | ‘http://helloflask.com/' |
host_url | ‘http://helloflask.com/' |
1 | from flask import Flask,request |
除了URL,请求报文中的其他信息都可以通过request对象提供的属性和方法获取,最常见的部分如下:
属性/方法 | 说明 |
---|---|
args | Werkzeug的ImmutableMultiDict对象,存储解析后的查询字符串,可以通过字典方式获取键值。如果你想获取未解析的原生查询字符串,可以使用query_string属性 |
blueprint | 当前蓝本的名称 |
cookies | 一个包含所有随请求提交的cookies的字典 |
data | 包含字符串形式的请求数据 |
endpoint | 与当前请求相匹配的端点值 |
files | Werkzeug的MultiDict对象,包含所有上传文件,可以使用字典的形式获取文件。使用的键为文件input标签中的name属性值,对应的值为Werkzeug的FileStorage对象,可以调用save()方法并传入保存路径来保存文件 |
form | Werkzeug的ImmutableMultiDict对象,与files类似,包含解析后的表单数据,表单字段值通过input标签的name属性值作为键获取 |
values | Werkzeug的CombinedMultiDict对象,结合了args和form属性的值 |
get_data(cache=True,as_text=False,parse_from_data=False) | 获取请求中的数据,默认读取为字节字符串(bytestring),将as_text设为True则返回值将是解码后的unicode字符串 |
get_json(self,force=False,silent=False,cache=True) | 作为JSON解析并返回数据,如果MIME类型不是JSON,返回None(除非force设为True);解析出错则抛出Werkzeug提供的BadRequest异常(如果未开调试模式,则返回400错误响应),如果silent设为True则返回None;cache设置是否缓存解析后的JSON数据 |
headers | 一个Werkzeug的EnvironHeaders对象,包含首部字段,可以以字典的形式操作 |
is_json | 通过MIME类型判断是否为JSON数据,返回布尔值 |
json | 包含解析后的JSON数据,内部调用get_json(),可通过字典的方式获取键值 |
method | 请求的HTTP方法 |
referrer | 请求发起源的URL,即referer |
scheme | 请求的URL模式(HTTP或HTTPS) |
user_agent | 用户代理(User Agent,UA),包含了用户的客户端类型,操作系统类型等信息 |
当你访问:http://localhost:5000/hello?name=Grey
时,页面加载后会显示“Hello,Grey!”
1 | from flask import Flask,request |
get()方法的第二个参数可以设置默认值,比如request.args.get(‘name’,’Human’)
0x01 处理请求
1. 查看路由表
在Flask中,请求的URL匹配对应的视图函数,视图函数的返回值对应的就是URL资源,程序的实例中存储了一个路由表,使用flask routes
命令可以查看程序中定义的所有路由,这个列表由app.url_map解析得到。
2. 设置监听方法
每个路由除了包含URL规则外,还可以设置监听HTTP的方法。
1 |
|
在装饰器中使用methods参数可以传入一个包含监听HTTP方法的可迭代对象,通过定义方法列表,可以在同一个URL规则中定义多个视图函数,分别处理不同的HTTP方法的请求。
3. URL变量转换器
Flask内置的URL变量转换器:
转换器 | 说明 |
---|---|
string | 不包含斜线的字符串(默认值) |
int | 整形 |
float | 浮点数 |
path | 包含斜线的字符串。static路由的URL规则中filename变量就使用了这个转换器 |
any | 匹配一系列给定值中的一个元素 |
uuid | UUID字符串 |
转换器通过特定的规则指定,即<转换器:变量名>
。<int:year>
把year的值转换为整数,因此可以在视图函数中直接对year进行数学计算:
1 |
|
在用法上唯一特殊的是any转换器,需要在转换器后添加括号给出可选值,即<any(value1,value2,value3):变量名>
1 |
|
如果访问http://127.0.0.1:5000/colors/<color>
,如果将color替换成any转换器中设置可选值以外的任意字符串,均会得到404错误响应。
如果想要在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或format()函数)来构建URL规则字符串:
1 | colors = ['blue','white','red'] |
4. 请求钩子
在一些场景下,我们需要对请求进行预处理或者是后处理,这种情况下可以使用Flask的提供一些请求钩子(Hook),它们可以用来注册请求处理不同阶段执行的处理函数,这些请求钩子用装饰器实现,通过程序app实例调用。
钩子 | 说明 |
---|---|
before_first_request | 注册一个函数,在处理第一个请求前运行 |
before_request | 注册一个函数,在处理每个请求前运行 |
after_request | 注册一个函数,如果没有未处理的异常抛出,会在每个请求结束之后运行 |
teardown_request | 注册一个函数,即使有未处理的异常抛出,会在每个请求结束之后运行。如果发生异常,会传入异常对象作为参数到注册的函数中 |
after_this_request | 在视图函数内注册一个函数,会在这个请求结束后运行 |
1 |
|
after_request和after_this_request钩子必须接收一个响应类对象作为参数,并且返回同一个或者更新后的响应对象。
补充,after_this_request的例子:
1 | from flask import Flask, after_this_request |
0x02 HTTP响应
1. Flask生成响应
响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。Flask在处理响应时候,会先判断是否可以找到与请求URL相匹配的路由,如果没有则返回404响应。如果找到了则调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容,正确返回时状态码默认为200。Flask会调用make_response()方法将视图函数返回值转换为响应对象。
简单的说,视图函数可以返回最多由三个元素组成的元组:响应主体,状态码,首部字段。
普通的响应只包含主体:
1 |
|
默认状态码为200,下面指定了不同的状态码:
1 |
|
有时候你会想附加或修改某个首部字段,比如生成状态码为3XX的重定向,需要将首部中的Location字段设置为重定向的目标URL:
1 |
|
2. 重定向
除了像前面那样手动生成302响应,我们还可以使用Flask提供的redirect()函数来生成重定向响应。
1 | from flask import Flask,redirect |
使用redirect()函数时,默认的状态码为302,如果想要修改状态码,可以在redirect()函数中作为第二个参数或使用code关键字传入。
如果想要在程序内重定向到其他视图,那么只需要在redirect()函数中使用url_for()函数生成目标URL即可:
1 | from flask import Flask,redirect,url_for |
3. 错误响应
大多数情况下,Flask会自动处理常见的错误响应,如果你想手动处理,可以使用Flask提供的abort()函数,在abort()函数中传入状态码即可返回对应的错误响应:
1 | from flask import Flask,abort |
abort()函数前不需要return语句,一旦abort()函数被调用,abort()函数之后的代码将不会被执行。
4. 响应格式
在Flask中默认的响应格式是HTML,使用其他响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义,以默认的HTML类型为例:
Content-Type:text/html;charset=utf-8
如果你想使用其他MIME类型,可以通过Flask提供的make_response()方法生成响应,传入响应的主体作为参数,然后使用响应对象的mimetype属性设置MIME类型,比如:
1 | from flask import make_response |
你也可以直接设置首部字段,比如response.headers['Content-Type'] = 'text/xml;charset=utf-8'
,但操作mimetype属性更加方便,而且不用设置字符集选项。
4.1 纯文本
将MIME类型设置为:“text/plain”
4.2 HTML
默认返回的类型,MIME类型为:“text/html”
4.3 XML
将MIME类型设置为:“application/xml”
4.4 JSON
MIME类型:“application/json”
JSON的结构基于“键值对的集合”和“有序的值列表”,这两种数据结构类似于Python中的字典和列表,Flask中通过引用Python标准库中的json模块,为程序提供JSON支持。可以直接从FLASK中导入json对象,然后调用dumps()方法,将字典,列表或元组序列化为JSON字符串,即可返回JSON响应。
1 | from flask import make_response,Flask,json |
不过一般不使用json模块,Flask提供了一种更方便的jsonify()函数,可以简化上述代码:
1 | from flask import jsonify |
jsonify()函数接收多种形式的参数。你既可以传入普通参数,也可以传入关键字参数,也可以传入字典,列表,或元组:
1 | from flask import jsonify |
jsonify()函数默认生成200响应,可以通过附加状态码自定义响应类型:
1 | from flask import jsonify |
5. Cookie
Cookie是Web服务器为了存储某些数据而保存在浏览器上的小型文本数据,浏览器会在一定时间内保存它,并在下一次向同一个服务器发送请求时候附带这些数据。
在Flask中如果想要在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()方法。
1 | from flask import Flask, make_response |
下面是Response类常用的属性和方法:
方法/属性 | 说明 |
---|---|
headers | 一个Werkzeug的Header对象,表示响应首部,可以像字典一样操作 |
status | 状态码,文本类型 |
status_code | 状态码,整型 |
mimetype | MIME类型 |
set_cookie | 设置一个cookie |
set_cookie()方法支持多个参数来设置Cookie的选项
方法/属性 | 说明 |
---|---|
key | cookie的键(名称) |
value | cookie的值 |
max_age | cookie的保存时间数,单位为秒,默认在管理浏览器时过期 |
expires | 具体的过期时间,一个datetime对象或UNIX时间戳 |
path | 限制cookie只在给定的路径可用,默认整个域名 |
domain | 设置cookie可用的域名 |
secure | 如果为True,则只有通过HTTPS才可以使用 |
httponly | 如果为True,禁止客户端JavaScript获取cookie |
当浏览器保存了服务器设置的Cookie之后,浏览器再次发送到该服务器的请求就会自动携带设置的Cookie信息。
1 | from flask import Flask, request |
6. Session
直接把认证信息用明文方式存储在浏览器是一件非常危险事,Flask提供了session对象来将Cookie数据进行加密存储。
6.1 设置程序密钥
程序的密钥可以通过Flask.secret_key属性或配置环境变量SECRET_KEY来设置。
1 | app.secret_key='secret string' |
更安全做法是写入环境变量或保存.env文件中:
1 | SECRET_KEY=secret string |
然后通过os的getenv()方法获取:
1 | import os |
在生产环境中,为了安全考虑,你必须使用随机生成的密钥。
6.2 模拟用户认证
1 | from flask import redirect, session, url_for |
向session中添加一个logged_in,将它的值设为True,表示用户已认证。
当用户登录后,我们就可以根据用户的认证状态分别显示不同的内容:
1 | from flask import request, session |
session中的数据可以像字典一样通过键来读取,或是使用get()方法。
程序中某些资源仅提供给登录用户,比如后台,这时候可以通过session是否存在logged_in键来进行判断:
1 | from flask import session, abort |
用户退出登录,可以用session的pop方法实现:
1 | from flask import session |
0x03 Flask上下文
Flask中有两种上下文,程序上下文(appliciton context)和请求上下文(request context)。
1. 上下文全局变量
变量名 | 上下文类型 | 说明 |
---|---|---|
current | 程序上下文 | 指向处理请求的当前程序实例 |
g | 程序上下文 | 代替Python的全局变量用法,确保仅在当前请求中可用。用于存储全局数据,每次请求都会重设 |
request | 请求上下文 | 封装客户端发出请求报文数据 |
session | 请求上下文 | 用于记住请求之间的数据,通过签名的Cookie实现 |
2. 激活上下文
下面这些情况,Flask会自动帮我们激活程序上下文:
- 使用
falsk run
命令启动程序 - 使用
app.run()
方法启动程序 - 执行使用
@app.cli.command()
装饰器注册flask命令时 - 使用
flask shell
命令启动Python Shell时
当请求进入时,Flask会自动激活请求上下文,另外当请求上下文被激活时,程序上下文也会被自动激活,当请求处理完成后,请求上下文和程序上下文也会自动销毁。
3. 上下文钩子
Flask为上下文提供了一个钩子,使用它注册的回调函数会在程序上下文被销毁时调用,也通常会在请求上下文销毁时调用。例如你需要在每个请求处理结束后销毁数据库连接:
1 |
|
使用@app.teardown_appcontext装饰器注册的回调函数需要接收异常对象作为参数,当请求被正常处理时这个参数值将是None,这个函数的返回值将被忽略。
0x04 HTTP进阶
1. 重定向上一个页面
要重定向回到上一个页面,最关键是获取上一个页面的URL。一般有两种方式可以获取。
1.1 HTTP referer
1 | return redirect(request.referrer) |
很多情况referrer字段是空值,这时候我们需要添加一个备选项:
1 | return redirect(request.referrer or url_for('hello')) |
1.2 查询参数
除了referrer获取,另一种更常见的方式是在URL中手动加入包含当前页面URL的查询参数,这个查询参数一般命名为next。
1 | from flask import request |
为了覆盖全面,我们可以把这两种方法一起使用,创建一个通用的redirect_back()函数:
1 | def redirect_back(default='hello', **kwargs): |
1.3 URL安全验证
next参数,需要保证是我们网站内的链接。
验证链接的函数:
1 | from urlparse import urlparse, urljoin # Python3需要从urllib.parse导入 |
使用is_safe_url进行验证:
1 | def redirect_back(default='hello', **kwargs): |
2. 使用Ajax发送异步请求
JQuery函数ajax()支持的主要参数:
参数 | 参数值类型及其默认值 | 说明 |
---|---|---|
url | 字符串:默认为当前页地址 | 请求的地址 |
type | 字符串:默认为“GET” | 请求的方式,即HTTP方法,比如GET,POST,DELETE等 |
data | 字符串:无默认值 | 发送到服务器的数据,会被JQuery自动转换为查询字符串 |
dataType | 字符串:默认由JQuery自动判断 | 期待服务器返回的数据类型,可用值如下:”xml” “html” “script” “json” “jsonp” “text” |
contentType | 字符串:默认为“applicatiob/x-www-form-urlencoded;charset=UTF-8” | 发送请求时使用的内容类型,即Content-Type的内容 |
complete | 函数;无默认值 | 请求完成后调用的回调函数 |
success | 函数;无默认值 | 请求成功后调用的回调函数 |
error | 函数;无默认值 | 请求失败后调用的回调函数 |
2.1 返回局部数据
纯文本或局部HTML模版
纯文本可以用JS直接替换页面中的文本值,而局部HTML则可以直接插入页面中。
1 |
|
JSON数据
1 |
|
在JQuery的ajax()方法的success回调中,响应主体的JSON字符串会被解析为JSON对象,可以直接获取并操作。
空值
删除文章的视图,可以直接返回空值,将状态码指定为204。
1 |
|
异步加载长文章示例
1 | from jinja2.utils import generate_lorem_ipsum |
3. HTTP服务器端推送
实现服务器端推送的一系列技术被合称为HTTP Server Push(HTTP服务器端推送)
主要的技术有:
名称 | 说明 |
---|---|
传统轮询 | 在特定的直接间隔内,客户端使用AJAX技术不断向服务器发起HTTP请求,然后获取新的数据更新页面 |
长轮询 | 和传统轮询类似,但是如果服务器没有返回数据,那就保持连接一直开启,直到有数据时才返回。取回数据后再发送另一个请求 |
Server-Sent Event(SSE) | SSE通过HTML5中的EventSource API实现,SSE会在客户端和服务端建立一个单向通道,客户端监听来自服务器端的数据,而服务器端可以在任意时间发送数据,两者建立类似订阅/发布的通信模式 |
4. Web安全防范
4.1 注入攻击
- 使用ORM可以一定程度上避免SQL注入问题
- 验证输入类型。比如某个视图函数接收整型id来查询,那么就在URL规则中限制URL变量为整型。
- 参数化查询。在构造SQL语句时避免使用拼接字符串或字符串格式化(使用百分号或format()方法)的方式来构建SQL语句。而要使用各类接口库提供的参数化查询方法。
- 转义特殊字符,比如引号、分号和横线等。使用参数化查询时,各种接口库会为我们做转义工作。
4.2 XSS
- HTML转义,防范XSS攻击最主要的方法是对用户输入的内容进行HTML转义,转义后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析。
- 验证用户输入,XSS攻击可以在任何用户可定制内容的地方进行,例如图片引用、自定义链接。仅仅转义HTML中的特殊字符并不能完全规避XSS攻击,因为在某些HTML属性中,使用普通的字符也可以插入JavaScript代码。除了转义用户输入外,我们还需要对用户的输入数据进行类型验证。
4.3 CSRF
- 正确使用HTTP方法防范CSRF的基础就是正确使用HTTP方法。在前面介绍过HTTP中的常用方法。在普通的Web程序中,一般只会使用到GET和POST方法。而且,目前在HTML中仅支持GET和POST方法(借助AJAX则可以使用其他方法)。在使用HTTP方法时,通常应该遵循下:
- GET方法属于安全方法,不会改变资源状态,仅用于获取资源,因此又被称为幂等方法 (idempotentmethod)。页面中所有可以通过链接发起的请求都属于GET请求。
- POST方法用于创建、修改和删除资源。在HTML中使用form标签创建表单并设置提交方法为POST,在提交时会创建POST请求。
- CSRF令牌校验,除了在表单中加入验证码外,一般的做法是通过在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为CSRF令牌(token)。