第二章 Flask与HTTP(上)

​   在第1章,我们已经了解了Flask的基本知识,如果想要进一步开发更复杂的Flask应用,我们就得了解Flask与HTTP协议的交互方式。HTTP(Hypertext Transfer Protocol,超文本传输协议)定义了服务器和客户端之间信息交流的格式和传递方式,它是万维网(World Wide Web)中数据交换的基础。

​   在这一章,我们会了解Flask处理请求和响应的各种方式,并对HTTP协议以及其他非常规HTTP请求进行简单的介绍。虽然本章的内容很重要,但鉴于内容有些晦涩难懂,如果感到困惑也不用担心,本章介绍的内容你会在后面的实践中逐渐理解和熟悉。如果你愿意,也可以临时跳过本章,等到学习完本书第一部分再回来重读。

​   HTTP的详细定义在RFC 7231~7235中可以看到。RFC(Request For Comment,请求评议)是一系列关于互联网标准和信息的文件,可以将其理解为互联网(Internet)的设计文档。完整的RFC列表可以在这里看到:https://tools.ietf.org/rfc/

2.1 请求响应循环

1.jpg

​   有一个类似我们第1章编写的程序运行着。它负责接收用户的请求,并把对应的内容返回给客户端,显示在用户的浏览器上。事实上,每一个Web应用都包含这种处理模式,即“请求-响应循环(Request-Response Cycle)”:客户端发出请求,服务器端处理请求并返回响应。

image.png

​   当用户访问一个URL,浏览器便生成对应的HTTP请求,经由互联网发送到对应的Web服务器。Web服务器接收请求,通过WSGI将HTTP格式的请求数据转换成我们的Flask程序能够使用的Python数据。在程序中,Flask根据请求的URL执行对应的视图函数,获取返回值生成响应。响应依次经过WSGI转换生成HTTP响应,再经由Web服务器传递,最终被发出请求的客户端接收。浏览器渲染响应中包含的HTML和CSS代码,并执行JavaScript代码,最终把解析后的页面呈现在用户浏览器的窗口中。

2.2 HTTP请求

​   URL是一个请求的起源。不论服务器在何地运行,当我们输入指向服务器所在的地址的URL,都会向服务器发送一个HTTP请求。一个标准的URL由很多部分组成,以下面这个URL为例:

​     http://helloflask.com/hello?name=Grey

​   这个url的各个组成部分如表所示:

信息 说明
http:// 协议字符串,指定要使用的协议
helloflask.com 服务器的地址(域名)
/hello?name=Grey 要获取的资源路径(path),类似UNIX的文件目录结构

这个URL后面的?name=Grey部分是查询字符串(query string)。URL中的查询字符串用来向指定的资源传递参数.查询字符串从?开始,以键值对的形式写出,多个键值对之间用&分隔

2.2.1 请求报文

​   当我们在浏览器中访问这个URL时,随之产生的是一个发向http://helloflask.com所在服务器的请求。请求的实质是发送到服务器上的一些数据,这种浏览器与服务器之间交互的数据成为报文(message),请求时浏览器发送的数据称为请求报文(request message),而服务器返回的数据称为响应报文(response message).

​   请求报文有请求的方法.URL.协议版本.首部字段(header)以及内容实体组成.前面的请求产生的请求报文示意表如下所示:

3.jpg

​   如果你想看真实的HTTP报文,可以在浏览器中向任意一个有效的URL发起请求,然后在浏览器的开发者工具(F12)里的Network标签中看到URL对应资源加载的所有请求列表,单击任一个请求条目即可看到报文信息,如下所示:

4.jpg

​   报文由报文首部和报文主体组成,两者由空行分隔,请求报文的主体一般为空.如果URL中包含查询字符串,或者是提交了表单,name报文主体将会是查询字符串和表单数据.

​   HTTP通过方法来区分不同的请求类型,比如,当你直接访问一个页面时,请求的方法是GET;当你在某个页面填写了表单并提交时,请求方法则通常为POST.

方法 说明 方法 说明
GET 获取资源 DELETE 删除资源
POST 传输数据 HEAD 获得报文首部
PUT 传输文件 OPTIONS 询问支持的方法

​   报文首部包含了请求的各种信息,比如客户端类型、是否设置缓存、语言偏好等。

HTTP中可用的首部字段列表可以在https://www.iana.org/assignments/message-headers/message-headers.xhtml 看到。请求方法的详细列表和说明可以在RFC 7231(https://tools.ietf.org/html/rfc7231 )中看到。

如果运行了示例程序,那么当你在浏览器中访问http://127.0.0.1:5000/hello 时,开发服务器会在命令行中输出一条记录日志,其中包含请求的主要信息:

127.0.0.1 - - [02/Aug/2017 09:51:37] "GET /hello HTTP/1.1" 200 –

2.2.2 Request对象

​   假设请求的url是:http://helloflask.com/hello?name=Grey

​   使用request的属性获取获取请求url属性如下:

属性 属性
path u'/hello' base_url u'http://helloflask.com/hello'
full_path u'/hello?name=Grey' url u'http://helloflask.com/hello?name=Grey'
host u'helloflask.com url_root u'http://helloflask.com/'
host_url u'http://helloflask.com/'

​   request对象常用的属性和方法:

5.jpg

​   Werkzeug的MutliDict类是字典的子类,它主要实现了同一个键对应多个值的情况。比如一个文件上传字段可能会接收多个文件。这时就可以通过getlist()方法来获取文件对象列表。而ImmutableMultiDict类继承了MutliDict类,但其值不可更改。更多内容可访问Werkzeug相关数据结构章节http://werkzeug.pocoo.org/docs/latest/datastructures/

​   代码实例2-1:

  获取请求URL中的查询字符串

from flask import Flask, request

app = Flask(__name__)

@app.route('/hello')
def hello():
    name = request.args.get('name', 'Flask')    # 获取查询参数name的值
    return '<h1>Hello, %s!<h1>' % name            

​   访问:http://localhost:5000/hello?name=Grey

​   输出:Hello, Grey!

​   上面的示例代码包含安全漏洞,在现实中我们要避免直接将用户传入的数据直接作为响应返回,在本章的末尾我们将介绍这个安全漏洞的具体细节和防范措施。

​   需要注意的是,和普通的字典类型不同,当我们从request对象的类型为MutliDict或ImmutableMultiDict的属性(比如files、form、args)中直接使用键作为索引获取数据时(比如request.args['name']),如果没有对应的键,那么会返回HTTP 400错误响应(Bad Request,表示请求无效),而不是抛出KeyError异常,如图所示。为了避免这个错误,我们应该使用get()方法获取数据,如果没有对应的值则返回None;get()方法的第二个参数可以设置默认值,比如requset.args.get('name','Human')。

6.jpg

如果开启了调试模式,那么抛出BadRequestKeyError异常并显示对应的错误栈信息,而不是常规的404错误

2.2.3 在flask中处理请求

​   URL是指网络上资源的地址。在Flask中,我们需要让请求的URL匹配对应的视图函数,视图函数返回值就是URL对应的资源。

1.路由匹配

​   为了便于将请求分发到对应的函数,程序实例存储了一个路由表(app.url_map),其中定义了URL规则和视图函数的映射关系,当请求发来后,Flask会根据请求报文中的URL(path部分)来尝试与这个表中的所有URL规则进行匹配,调用匹配成功的视图函数.如果没有匹配的URL规则,说明程序中没有处理这个URL的视图函数,flask]会自动返回404错误响应(Not Found表示资源未找到),你可以尝试在浏览器中访问http://localhost:5000/nothing ,因为我们的程序中没有视图函数负责处理这个URL,所以你会得到404响应。

​   当请求的URL与某个视图函数的URL规则匹配成功时,对应的视图函数就会调用。使用flask routes命令可以查看程序中定义的所有路由,这个列表由app.url_map解析得到:

​   $ flask routes
​   Endpoint Methods Rule


​   hello_world GET /hello
​   static GET /static/<path:filename>

​   在输出的文本中,我们看到每个路由对应断点(Endpoint)、HTTP方法(Methods)和URL规则(Rule),其中static是flask添加的特殊路由,用来访问静态文件

2.设置监听的HTTP方法

​   通过flask routes命令打印出的路由列表可以看到,每一个路由除了包含URL规则外,还设置了监听的HTTP方法。GET是最常用的HTTP方法,所以视图函数默认监听的方法类型就是GET,HEAD、OPTIONS方法的请求由Flask处理,而像DELETE、PUT等方法一般不会在程序中实现,在后面我们构建Web API时才会用到这些方法。

​   我们可以在app.route()装饰器中使用methods参数传入一个包含监听的HTTP方法的可迭代对象。比如,下面的视图函数同时监听GET请求和POST请求:

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

​   当某个请求的方法不合符要求时,请求将无法被正常处理。比如,提交表单通常使用POST方法,而提交的目标URL对应的视图函数只允许使用GET方法,这是Flask会自动返回一个405错误响应(Method Not Allowed, 表示请求方法不允许),如图所示:


image.png

​   通过定义方法列表,我们可以为同一个URL规则定义多个视图函数,分别处理不同HTTP方法的请求。

3.URL处理

​   从前面的路由列表可以看到,除了/hello,这个程序还包含许多URL规则,比如和go_back端点对应的/goback/<int:year>。现在请尝试访问http://localhost:5000/goback/34 ,在URL中加入一个数字作为时光倒流的年数,你会发现加载后的页面中有通过传入的年数计算出的年份:“Welcome to 1984!”。仔细观察一下,你会发现URL规则中的变量部分有一些特别,<int:year>表示为year变量添加了一个int转换器,Flask在解析这个URL变量时会将其转换为整型。URL中的变量部分默认类型为字符串,但Flask提供了一些转换器可以在URL规则里使用,如下表所示:

  Flask内置的URL变量转换器

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

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

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

​   默认的行为不仅仅是类型转换,还包括URL匹配。在这个例子中,如果不使用转换器,默认year变量会被转换成字符串,为了能够在Python中计算天数,我们需要使用int()函数将year变量转换成整型。但是如果用户输入的是英文字母,就会出现转换错误,抛出ValueError异常,我们还需要手动验证;使用了转换器后,如果URL中传入的变量不是数字,那么会直接返回404错误响应。比如,你可以尝试访问http://localhost:5000/goback/tang

​   在用法上唯一特别的是any转换器,你需要在转换器后添加括号来给出可选值,即"<any(value1,valuel2,...):变量名>",比如:

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

​   当你在浏览器中访问http://localhost:5000/colors/ 时,如果将<color>部分替换为any转换器中设置的可选值以外的任意字符,均会获得404错误响应。

​   如果你想在any转换器中传入一个预先定义的列表,可以通过格式化字符串的方式(使用%或是format()函数)来构建URL规则字符串,比如:

colors = ['blue', 'white', 'red']

@app.route('/colors/<any(%s):color>' % str(colors)[1:-1])
...

2.2.4 请求钩子

​   有时我们需要对请求进行预处理(preprocessing)和后处理(postprocessing),这时可以使用Flask提供的一些请求钩子(Hook),他们可以用来注册在请求处理的不同阶段执行的处理函数(或称为回调函数,即Callback)。这些请求钩子使用装饰器实现,通过程序实例app调用,用法很简单:以before_request钩子(请求之前)为例,当你对一个函数附加了app.before_request装饰器后,就会将这个函数注册为before_request处理函数,每次执行请求前都会触发所有before_request处理函数。Flask默认实现的五种请求钩子如下表所示:

请求钩子

钩子 说明
before_first_request 注册一个函数,在处理第一个请求运行
before_request 注册一个函数,在处理每个请求前运行
after_request 注册一个函数,如果没有未处理的异常抛出,会在每个请求结束后运行
teardown_request 注册一个函数,即使有未处理的异常抛出,会在每个请求结束后运行。如果发生异常,会传入异常对象作为参数到注册的函数中
after_this_request 在视图函数内注册一个函数,会在这个请求结束后运行

​   这些钩子使用起来和app.route()装饰器基本相同,每个钩子可以注册多个处理函数,函数名并不是必须和钩子名称相同,下面是一个基本实例:

@app.before_request
def do_something():
    pass # 这里的代码会在每个请求处理前执行

​   假如我们创建了三个视图函数A、B、C,其中视图C使用了after_this_reques钩子,那么当请求A进入后,整个请求处理周期的请求处理函数调用流程如下所示。

​   请求钩子常用场景:

·before_first_request:在玩具程序中,运行程序前我们需要进行一些程序的初始化操作,比如创建数据库表,添加管理员用户。这些工作可以放到使用before_first_request装饰器注册的函数中。

·before_request:比如网站上要记录用户最后在线的时间,可以通过用户最后发送的请求时间来实现。为了避免在每个视图函数都添加更新在线时间的代码,我们可以仅在使用before_request钩子注册的函数中调用这段代码。

·after_request:我们经常在视图函数中进行数据库操作,比如更新、插入等,之后需要将更改提交到数据库中。提交更改的代码就可以放到after_request钩子注册的函数中。

另一种常见的应用是建立数据库连接,通常会有多个视图函数需要建立和关闭数据库连接,这些操作基本相同。一个理想的解决方法是在请求之前(before_request)建立连接,在请求之后(teardown_request)关闭连接。通过在使用相应的请求钩子注册的函数中添加代码就可以实现。这很像单元测试中的setUp()方法和tearDown()方法。

请求钩子流程图
请求钩子流程图

注意

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

2.3 HTTP响应

​   在Flask程序中,客户端发出的请求触发响应的视图函数,获取返回值会作为响应的主体最后生成完整的响应,即响应报文

2.3.1 响应报文

​   响应报文主要由协议版本、状态码(status code)、原因短语(reason phrase)、响应首部和响应主体组成。以发向localhost:5000/hello的请求为例,服务器生成的响应报文示意如图所示

响应报文

响应报文

​   响应报文的首部包含一些关于响应和服务器的信息,这些内容由Flask生成,而我们在视图函数中返回的内容即为响应报文中的主体内容。浏览器接收到响应后,会把返回的响应主体解析并显示在浏览器窗口上。

​   HTTP状态码用来表示请求处理的结果,下表是常见的几种状态码和相应的原因短语。

常见的HTTP状态码

常见的HTTP状态码

当关闭调试模式,即FLASK_ENV使用默认值production,如果程序出错,Flask会自动返回500错误响应,而调试模式下则会显示调试信息和错误堆栈

响应状态码的详细列表和说明可以在RFC7321(https://tools.ietf.org/html/rfc7231 )中看到

2.3.2在Flask中生成响应

​   响应在Flask中使用Response对象表示,响应报文中的大部分内容由服务器处理,大多数情况下,我们只负责返回主体内容。

​   根据我们在上一节介绍的内容,Flask会先判断是否可以找到与请求URL相匹配的路由,如果没有则返回404响应。如果找到,则调用对应的视图函数,视图函数的返回值构成了响应报文的主体内容,正确返回时状态码默认为200。Flask会调用make_response()方法将视图函数返回值转换为响应对象。

​   完整地说,视图函数可以返回最多由三个元素组成的元组:响应主体、状态码、首部字段。其中首部字段可以为字典,或是两元素元组组成的列表。

​   比如,普通的响应可以只包含主体内容:

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

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

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

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

​ ```

@app.route('/hello')
def hello():
    ...
    return '', 302, {'Location', 'http://www.example.com'}

​   现在访问http://localhost:5000/hello ,会重定向到http://www.example.com 。在多数情况下,除了响应主体,其他部分我们通常只需要使用默认值即可。

1.重定向

​   如果你访问http://localhost:5000/hi ,你会发现页面加载后地址栏中的URL变为了http://localhost:5000/hello 。这种行为被称为重定向(Redirect),你可以理解为网页跳转。在上一节的示例中,状态码为302的重定向响应的主体为空,首部中需要将Location字段设为重定向的目标URL,浏览器接收到重定向响应后会向Location字段中的目标URL发起新的GET请求,整个流程如图所示。

重定向流程示意图

重定向流程示意图

​   在Web程序中,我们经常需要进行重定向。比如,当某个用户在没有经过认证的情况下访问需要登录后才能访问的资源,程序通常会重定向到登录页面。

​   对于重定向这一类特殊响应,Flask提供了一些辅助函数。除了像前面那样手动生成302响应,我们可以使用Flask提供的redirect()函数来生成重定向响应,重定向的目标URL作为第一个参数。前面的例子可以简化为:

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

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

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

重定向到其他的视图

from flask import Flask, redirect, url_for 
...
@app.route('/hi')
def hi():
    ...
    return redierct(url_for('hello'))  # 重定向到/hello

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

2.错误响应

​   如果你访问http://localhost:5000/brew/coffee ,会获得一个418错误响应(I'm a teapot),如图下图所示。

418错误响应

418错误响应

​   418错误响应由IETF(Internet Engineering Task Force,互联网工程任务组)在1998年愚人节发布的HTCPCP(Hyper Text Coffee Pot Control Protocol,超文本咖啡壶控制协议)中定义(玩笑),当一个控制茶壶的HTCPCP收到BREW或POST指令要求其煮咖啡时应当回传此错误。

​   大多数情况下,Flask会自动处理常见的错误响应。HTTP错误对应的异常类在Werkzeug的werkzeug.exceptions模块中定义,抛出这些异常即可返回对应的错误响应。如果你想手动返回错误响应,更方便的方法是使用Flask提供的abort()函数。

​   在abort()函数中传入状态码即可返回对应的错误响应,下面代码中的视图函数返回404错误响应。

返回404错误响应

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

abort()函数前不需要使用return语句,但一旦abort()函数被调用,abort()函数之后的代码将不会被执行。

​   虽然我们有必要返回正确的状态码,但这并不是必须的。比如,当某个用户没有权限访问某个资源时,返回404错误要比403错误更加友好

2.3.3 响应格式

​   在HTTP响应中,数据可以通过多种格式传输。大多数情况下,我们会使用HTML格式,这也是Flask中的默认设置。在特定的情况下,我们也会使用其他格式。不同的响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义,以默认的HTML类型为例:

Content-Type: text/html; charset=utf-8

​   MIME类型(又称为media type或content type)是一种用来标识文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的内容类型,并执行不同的操作。一般的格式为“类型名/子类型名”,其中的子类型名一般为文件扩展名。比如,HTML的MIME类型为“text/html”,png图片的MIME类型为“image/png”。完整的标准MIME类型列表可以在这里看到:https://www.iana.org/assignments/media-types/media-types.xhtml

​   如果你想使用其他MIME类型,可以通过Flask提供的make_response()方法生成响应对象,传入响应的主体作为参数,然后使用响应对象的mimetype属性设置MIME类型,比如:

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属性更加方便,而且不用设置字符集(charset)选项。

2.3.4 Cookie

​   HTTP是无状态(stateless)协议。也就是说,在一次请求响应结束后,服务器不会留下任何关于对方状态的信息。但是对于某些Web程序来说,客户端的某些信息又必须被记住,比如用户的登录状态,这样才可以根据用户的状态来返回不同的响应。为了解决这类问题,就有了Cookie技术。Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。

​   Cookie指Web服务器为了存储某些数据(比如用户信息)而保存在浏览器上的小型文本数据。浏览器会在一定时间内保存它,并在下一次向同一个服务器发送请求时附带这些数据。Cookie通常被用来进行用户会话管理(比如登录状态),保存用户的个性化信息(比如语言偏好,视频上次播放的位置,网站主题选项等)以及记录和收集用户浏览数据以用来分析用户行为等。

​   在Flask中,如果想要在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()方法。要使用这个方法,我们需要先使用make_response()方法手动生成一个响应对象,传入响应主体作为参数。这个响应对象默认实例化内置的Response类。下表是内置的Response类常用的属性和方法。

Response类常用的属性和方法

方法/属性 说明
headers 一个Werkzeug的Headers对象,表示响应首部,可以像字典一样操作
status 状态码.文本类型
status_code 状态码,整形
mimetype MIME类型(仅包括内容类型部分)
set_cookie() 用来设置一个cookie

​   set_cookie()方法支持多个参数来设置Cookie的选项,如下表所示

set_cookie()方法的参数

属性 说明
key cookie的键(名称)
value cookie的值
max_age cookie被保存的时间数,单位为秒;默认在用户会话结束(即关闭浏览器)时过期
expires 具体的过期时间,一个datetime对象或UNIX时间戳
path 限制cookie只在给定的路径可用,默认为整个域名
domain 设置cookie可用的域名
secure 如果设置为True,只有通过HTTPS才可以使用
httponly 如果设置为True,禁止客户端JavaScript获取cookie

​   set_cookie视图用来设置cookie,他会将URL中的name变量的值设置到name的cookie里,代码如下所示:

设置cookie

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

from flask imoprt Flask,make_response
@app.route('/cookie')
def set_cookie():
    resp = make_response('this is to set cookie')
    resp.set_cookie('username', 'itcast')
    return resp

​   这个make_response()函数中,我们传入的是使用redirect()函数生成的重定向响应。set_cookie视图会在生成的响应报文首部中创建一个Set-Cookie字段,即“Set-Cookie:name=Grey;Path=/”。

​   现在我们查看浏览器中的Cookie,就会看到多了一块名为name的cookie,其值为我们设置的“Grey”,如下图所示。因为过期时间使用默认值,所以会在浏览会话结束时(关闭浏览器)过期。

在浏览器中查看cookie

image.png

​   当浏览器保存了服务端设置的cookie后,浏览器再次发送到该服务器的请求会自动携带设置的Cookie信息,Cookie的内容存储在请求首部的Cookie字段中,整个交互过程由上至下如下图所示:

Cookie设置示意图

9.jpg

​   在Flask中,Cookie可以通过请求对象的cookie属性读取,在修改后的hello视图中,如果没有从查询参数中获取到name的值,就会从Cookie中寻找:

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

​   这时服务器就可以根据Cookie的内容来获得客户端的状态信息,并根据状态返回不同的响应。如果你访问http://localhost:5000/set/Grey ,那么就会将名为name的cookie设为Grey,重定向到/hello后,你会发现返回的内容变成了“Hello,Grey!”。如果你再次通过访问http://localhost:5000/set/ 修改name cookie的值,那么重定向后的页面返回的内容也会随之改变。

2.3.5 session:安全的Cookie

​   当我们使用浏览器登录某个社交网站时,会在登录表单中填写用户名和密码,单击登录按钮后,这会向服务器发送一个包含认证数据的请求。服务器接收请求后会查找对应的账户,然后验证密码是否匹配,如果匹配,就在返回的响应中设置一个cookie,比如,“login_user:greyli”。

​   响应被浏览器接收后,cookie会被保存在浏览器中。当用户再次向这个服务器发送请求时,根据请求附带的Cookie字段中的内容,服务器上的程序就可以判断用户的认证状态,并识别出用户。

​   但是这会带来一个问题,在浏览器中手动添加和修改Cookie是很容易的事,仅仅通过浏览器插件就可以实现。所以,如果直接把认证信息以明文的方式存储在Cookie里,那么恶意用户就可以通过伪造cookie的内容来获得对网站的权限,冒用别人的账户。为了避免这个问题,我们需要对敏感的Cookie内容进行加密。方便的是,Flask提供了session对象用来将Cookie数据加密储存。

​   在编程中,session指用户会话(user session),又称为对话(dialogue),即服务器和客户端/浏览器之间或桌面程序和用户之间建立的交互活动。在Flask中,session对象用来加密Cookie。默认情况下,它会把数据存储在浏览器上一个名为session的cookie里。

1.设置程序密钥

​   session通过密钥对数据进行签名以加密数据,因此,我们得先设置一个密钥.这里的密钥就是一个局哟偶一定复杂度和随机性的字符串,比如"ADSFFVUKJYHTGRD".

​   程序的密钥可以通过Flask.secret_key属性或配置变量SECRET_KEY设置,比如:

app.secret_key = 'secret string'

​   更安全的做法是把密钥写进系统环境变量(在命令行中使用export或set命令),或者保存在.env文件中:

​ SECRET_KEY = secret string

​   然后在程序脚本中使用os模块提供的getenv()方法获取:

import os
# ...
app.secret_key = os.getenv('SECRET_KEY', 'secret string')

​   我们可以在getenv()方法中添加第二个参数,作为没有获取到对应环境变量时使用的默认值。

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

2.模拟用户认证

​   下面我们会使用session模拟用户的认证功能。

登入用户

from flask import redirect, session, url_for

@app.route('/login')
def login():
    session['logged_in'] = True  # 写入session
    return redirect(url_for('hello'))

​   这个登录视图只是简化的示例,在实际的登录中,我们需要在页面上提供登录表单,供用户填写账户和密码,然后在登录视图里验证账户和密码的有效性。session对象可以像字典一样操作,我们向session中添加一个logged-in cookie,将它的值设为True,表示用户已认证。

​   当我们使用session对象添加cookie时,数据会使用程序的密钥对其进行签名,加密后的数据存储在一块名为session的cookie里,如下图所示。

​   你可以在下图方框内的Content部分看到对应的加密处理后生成的session值。使用session对象存储的Cookie,用户可以看到其加密后的值,但无法修改它。因为session中的内容使用密钥进行签名,一旦数据被修改,签名的值也会变化。这样在读取时,就会验证失败,对应的session值也会随之失效。所以,除非用户知道密钥,否则无法对session cookie的值进行修改。

image.png

​   当支持用户登录后,我们就可以根据用户的认证状态分别显示不同的内容。在login视图的最后,我们将程序重定向到hello视图,下面是修改后的hello视图

@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键,如果有则表示用户已经登录。通过判断用户的认证状态,我们在返回的响应中添加一行表示认证状态的信息:如果用户已经登录,显示[Authenticated];否则显示[Not authenticated]。

  如果你访问http://localhost:5000/login ,就会登入当前用户,重定向到http://localhost:5000/hello 后你会发现加载后的页面显示一行“[Authenticated]”,表示当前用户已经通过认证,如下图所示。

已认证主页

已认证主页

​   程序中的某些资源仅提供给登入的用户,比如管理后台,这时我们就可以通过判断session是否存在logged_in键来判断用户是否认证,下面的代码是模拟管理后台的admin视图

模拟管理后台

from flask import session, abort

@app.route('/admin')
def admin():
    if 'logged_in' not in session:
        abort(403)
    return 'Welcome to admin page.'

​   通过判断logged_in是否在session中,我们可以实现:如果用户已经认证,会返回一行提示文字,否则会返回403错误响应.

​   登出用户的logout视图也非常简单,登出账户对应的实际操作其实就是把代表用户认证的logged_in cookie删除,这通过session对象的pop方法实现,代码如下所示。

登出用户

from flask import session

@app.route('/logout')
def logout():
    if 'logged_in' in session:
        session.pop('logged_in')
    return redirect(url_for('hello'))

​   现在访问http://localhost:5000/logout 则会登出用户,重定向后的/hello页面的认证状态信息会变为[Not authenticated],如下图所示。

未认证的主页

未认证的主页

​   默认情况下,session cookie会在用户关闭浏览器时删除。通过将session.permanent属性设为True可以将session的有效期延长为Flask.permanent_session_lifetime属性值对应的datetime.timedelta对象,也可通过配置变量PERMANENT_SESSION_LIFETIME设置,默认为31天。

​   尽管session对象会对Cookie进行签名并加密,但这种方式仅能够确保session的内容不会被篡改,加密后的数据借助工具仍然可以轻易读取(即使不知道密钥)。因此,绝对不能在session中存储敏感信息,比如用户密码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容