✅【Flask笔记】第二节:HTTP

img

旧的笔记,以前存放的笔记软件停止运营了,重新迁移到博客上,删除和修改一些过时的内容。

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
2
3
4
5
6
7
8
9
from flask import Flask,request

app = Flask(__name__)

@app.route('/')
def hello():
vaule = request.url_root
print(vaule)
return 'Welcome to Flask!'

除了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
2
3
4
5
6
7
8
from flask import Flask,request

app = Flask(__name__)

@app.route('/')
def hello():
name = request.args.get('name','Flask')
return '<h1>Hello,%s!</h1>' %name

get()方法的第二个参数可以设置默认值,比如request.args.get(‘name’,’Human’)

0x01 处理请求

1. 查看路由表

在Flask中,请求的URL匹配对应的视图函数,视图函数的返回值对应的就是URL资源,程序的实例中存储了一个路由表,使用flask routes命令可以查看程序中定义的所有路由,这个列表由app.url_map解析得到。

2. 设置监听方法

每个路由除了包含URL规则外,还可以设置监听HTTP的方法。

1
2
3
@app.route('/hello',methods = ['GET','POST'])
def hello():
return '<h1>Hello,Flask!</h1>'

在装饰器中使用methods参数可以传入一个包含监听HTTP方法的可迭代对象,通过定义方法列表,可以在同一个URL规则中定义多个视图函数,分别处理不同的HTTP方法的请求。

3. URL变量转换器

Flask内置的URL变量转换器:

转换器 说明
string 不包含斜线的字符串(默认值)
int 整形
float 浮点数
path 包含斜线的字符串。static路由的URL规则中filename变量就使用了这个转换器
any 匹配一系列给定值中的一个元素
uuid UUID字符串

转换器通过特定的规则指定,即<转换器:变量名><int:year>把year的值转换为整数,因此可以在视图函数中直接对year进行数学计算:

1
2
3
@app.route('/goback/<int:year>')
def go_back(year):
return '<p>Welcome to %d!</p>' %(2018-year)

在用法上唯一特殊的是any转换器,需要在转换器后添加括号给出可选值,即<any(value1,value2,value3):变量名>

1
2
3
@app.route('colors/<any(blue,white,red):color>')
def three_color(color):
return '<p>Love is patient and kind. Love is not jealous or boastful or proud or rude.</p>'

如果访问http://127.0.0.1:5000/colors/<color>,如果将color替换成any转换器中设置可选值以外的任意字符串,均会得到404错误响应。
如果想要在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或format()函数)来构建URL规则字符串:

1
2
colors = ['blue','white','red']
@app.route('/colors/<any(%s):color>' %str(colors)[1:-1])

4. 请求钩子

在一些场景下,我们需要对请求进行预处理或者是后处理,这种情况下可以使用Flask的提供一些请求钩子(Hook),它们可以用来注册请求处理不同阶段执行的处理函数,这些请求钩子用装饰器实现,通过程序app实例调用。

钩子 说明
before_first_request 注册一个函数,在处理第一个请求前运行
before_request 注册一个函数,在处理每个请求前运行
after_request 注册一个函数,如果没有未处理的异常抛出,会在每个请求结束之后运行
teardown_request 注册一个函数,即使有未处理的异常抛出,会在每个请求结束之后运行。如果发生异常,会传入异常对象作为参数到注册的函数中
after_this_request 在视图函数内注册一个函数,会在这个请求结束后运行
1
2
3
@app.before_request
def do_something():
pass #这里代码会在每个请求处理前执行

after_request和after_this_request钩子必须接收一个响应类对象作为参数,并且返回同一个或者更新后的响应对象。

补充,after_this_request的例子:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask, after_this_request

app = Flask(__name__)

@app.route('/')
def index():
@after_this_request
def add_header(response):
response.headers['X-Extra-Header'] = 'Value'
return response

return 'Hello, World!'

0x02 HTTP响应

1. Flask生成响应

响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。Flask在处理响应时候,会先判断是否可以找到与请求URL相匹配的路由,如果没有则返回404响应。如果找到了则调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容,正确返回时状态码默认为200。Flask会调用make_response()方法将视图函数返回值转换为响应对象。

简单的说,视图函数可以返回最多由三个元素组成的元组:响应主体,状态码,首部字段。

普通的响应只包含主体:

1
2
3
4
@app.route('/hello')
def hello():
...
return '<h1>Hello,Flask!</h1>'

默认状态码为200,下面指定了不同的状态码:

1
2
3
4
@app.route('/hello')
def hello():
...
return '<h1>Hello,Flask!</h1>',201

有时候你会想附加或修改某个首部字段,比如生成状态码为3XX的重定向,需要将首部中的Location字段设置为重定向的目标URL:

1
2
3
4
@app.route('/hello')
def hello():
...
return '',302,{'location':'http://www.example.com'}

2. 重定向

除了像前面那样手动生成302响应,我们还可以使用Flask提供的redirect()函数来生成重定向响应。

1
2
3
4
from flask import Flask,redirect
@app.route('/hello')
def hello():
return redirect('http://www.example.com')

使用redirect()函数时,默认的状态码为302,如果想要修改状态码,可以在redirect()函数中作为第二个参数或使用code关键字传入。

如果想要在程序内重定向到其他视图,那么只需要在redirect()函数中使用url_for()函数生成目标URL即可:

1
2
3
4
5
6
7
8
from flask import Flask,redirect,url_for

@app.route('/hi')
def hi():
return redirect(url_for('hello'))

@app.route('/hello')
def hello():

3. 错误响应

大多数情况下,Flask会自动处理常见的错误响应,如果你想手动处理,可以使用Flask提供的abort()函数,在abort()函数中传入状态码即可返回对应的错误响应:

1
2
3
4
from flask import Flask,abort
@app.route('/404')
def not_found():
abort(404)

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
2
3
4
5
6
7
from flask import make_response

@app.route('/foo')
def foo():
response = make_response('Hello,World!')
response.mimetype = 'text/plain'
return 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
2
3
4
5
6
7
8
9
10
11
from flask import make_response,Flask,json

@app.route('/foo')
def foo():
data = {
'name':'Grey Li',
'gender':'male'
}
response = make_response(json.dumps(data))
response.mimetype = 'application/json'
return response

不过一般不使用json模块,Flask提供了一种更方便的jsonify()函数,可以简化上述代码:

1
2
3
4
5
from flask import jsonify

@app.route('/foo')
def foo():
return jsonify(name='Grey Li',gender='male')

jsonify()函数接收多种形式的参数。你既可以传入普通参数,也可以传入关键字参数,也可以传入字典,列表,或元组:

1
2
3
4
5
from flask import jsonify

@app.route('/foo')
def foo():
return jsonify({'name':'Grey Li','gender':'male'})

jsonify()函数默认生成200响应,可以通过附加状态码自定义响应类型:

1
2
3
4
5
from flask import jsonify

@app.route('/foo')
def foo():
return jsonify(message='Error!'),500

Cookie是Web服务器为了存储某些数据而保存在浏览器上的小型文本数据,浏览器会在一定时间内保存它,并在下一次向同一个服务器发送请求时候附带这些数据。

在Flask中如果想要在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()方法。

1
2
3
4
5
6
7
from flask import Flask, make_response
...
@app.route('/set/<name>')
def set_cookie(name):
response = make_response(redirect(url_for('hello')))
response.set_cookie('name', name)
return 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
2
3
4
5
6
7
8
from flask import Flask, request
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human') # 从Cookie中获取name值
return '<h1>Hello, %s</h1>' % name

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
2
3
import os
# ...
app.secret_key = os.getenv('SECRET_KEY', 'secret string')

在生产环境中,为了安全考虑,你必须使用随机生成的密钥。

6.2 模拟用户认证

1
2
3
4
5
from flask import redirect, session, url_for
@app.route('/login')
def login():
session['logged_in'] = True # 写入session
return redirect(url_for('hello'))

向session中添加一个logged_in,将它的值设为True,表示用户已认证。

当用户登录后,我们就可以根据用户的认证状态分别显示不同的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import request, session
@app.route('/')
@app.route('/hello')
def hello():
name = request.args.get('name')
if name is None:
name = request.cookies.get('name', 'Human')
response = '<h1>Hello, %s!</h1>' % name
# 根据用户认证状态返回不同的内容
if 'logged_in' in session:
response += '[Authenticated]'
else:
response += '[Not Authenticated]'
return response

session中的数据可以像字典一样通过键来读取,或是使用get()方法。

程序中某些资源仅提供给登录用户,比如后台,这时候可以通过session是否存在logged_in键来进行判断:

1
2
3
4
5
6
from flask import session, abort
@app.route('/admin')
def admin():
if 'logged_in' not in session:
abort(403)
return 'Welcome to admin page.'

用户退出登录,可以用session的pop方法实现:

1
2
3
4
5
6
from flask import session
@app.route('/logout')
def logout():
if 'logged_in' in session:
session.pop('logged_in')
return redirect(url_for('hello'))

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
2
3
4
@app.teardown_appcontext
def teardown_db(exception):
...
db.close()

使用@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
2
3
4
5
6
7
8
9
10
11
12
13
from flask import request
@app.route('/foo')
def foo():
return '<h1>Foo page</h1><a href="%s">Do something and redirect</a>' % url_for('do_something', next=request.full_path)

@app.route('/bar')
def bar():
return '<h1>Bar page</h1><a href="%s">Do something and redirect</a>' % url_for('do_something', next=request.full_path)

@app.route('/do-something')
def do_something():
# do something
return redirect(request.args.get('next'),url_for('hello'))

为了覆盖全面,我们可以把这两种方法一起使用,创建一个通用的redirect_back()函数:

1
2
3
4
5
6
7
8
9
10
def redirect_back(default='hello', **kwargs):
for target in request.args.get('next'), request.referrer:
if target:
return redirect(target)
return redirect(url_for(default, **kwargs))

@app.route('/do_something_and_redirect')
def do_something():
# do something
return redirect_back()

1.3 URL安全验证

next参数,需要保证是我们网站内的链接。

验证链接的函数:

1
2
3
4
5
6
7
from urlparse import urlparse, urljoin  # Python3需要从urllib.parse导入
from flask import request
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc

使用is_safe_url进行验证:

1
2
3
4
5
6
7
def redirect_back(default='hello', **kwargs):
for target in request.args.get('next'), request.referrer:
if not target:
continue
if is_safe_url(target):
return redirect(target)
return redirect(url_for(default, **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
2
3
4
@app.route('/comments/<int:post_id>')
def get_comments(post_id):
...
return render_template('comments.html')
JSON数据
1
2
3
4
@app.route('/profile/<int:user_id>')
def get_profile(user_id):
...
return jsonify(username=username, bio=bio)

在JQuery的ajax()方法的success回调中,响应主体的JSON字符串会被解析为JSON对象,可以直接获取并操作。

空值

删除文章的视图,可以直接返回空值,将状态码指定为204。

1
2
3
4
@app.route('/post/delete/<int:post_id>', methods=['DELETE'])
def delete_post(post_id):
...
return '', 204
异步加载长文章示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from jinja2.utils import generate_lorem_ipsum
@app.route('/post')
def show_post():
post_body = generate_lorem_ipsum(n=2) # 生成两段随机文本
return '''
<h1>A very long post</h1>
<div class="body">%s</div>
<button id="load">Load More</button>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script type="text/javascript">
$(function() {
$('#load').click(function() {
$.ajax({
url: '/more', // 目标URL
type: 'get', // 请求方法
success: function(data){ // 返回2XX响应后触发的回调函数
$('.body').append(data); // 将返回的响应插入到页面中
}
})
})
})
</script>''' % post_body

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)。