HTTP响应
- 视图函数返回的内容即为响应报文中的主题内容。
- 当关闭调试模式时,即FLAK_ENV使用默认值production,如果程序出错,Flask会自动返回500错误响应,而在调试模式下则会显示调试信息和错误堆栈。
- 如果想手动返回错误响应,更方便的方法是使用Flask提供的abort函数。
响应格式
- 不同的响应数据格式需要设置不同的MIME类型,MIME类型在首部的Content-Type字段中定义
- MIMIE类型是一种用来识别文件类型的机制,它与文件扩展名相对应,可以让客户端区分不同的文件类型,并执行不同的操作。
- MIME格式:“类型名/子类型名”;
- 使用Flask提供的make_response(响应主体)方法生成响应对象,设置响应对象的mimetype属性以设置MIME类型;
- response.headers["Content-Type"]='text/xml'; charset=utf-8也可以设置MIME类型。但是用mimetype不需要设置字符集选项。
常见的数据格式
- 纯文本 text/plain
- HTML text/html,最常用的数据格式,也是Flask返回响应的默认数据类型。
- XML application/xml,HTML中的标签用于显示内容,XML中的标签用于定义数据。xml一般作为AJAX请求的响应格式,或者是WEB api的响应格式。
- JSON application/json
JSON响应
- JSON的结构基于“键值对的集合”和“有序值列表”,这2中数据结果类似Python中的字典和列表。
- Flask通过引入标准库中的json模块为程序提供了json支持,因此可以导入json对象,然后调用dumps方法将字典,列表或元组序列化为json字符串,再使用mimetype修改MIME类型返回JSON响应。
- Flask提供的jsonify()函数包装了json的dumps和load()方法,会对我们传入的参数(传入普通参数或关键字参数)
- 进行序列化,转换成JSON字符串,然后生成一个响应对象,并设置正确的MIME类型。
- Flask在获取请求中的json数据:request.json属性/request.get_json()方法。
来一块Cookie
- HTTP是无状态协议。也就是说在一次请求响应结束后,服务器不会留下任何关于对方状态的信息。
- Cookie技术通过在请求和响应报文中添加Cookie数据来保存客户端的状态信息。
- 在Flask中如果想在响应中添加一个cookie,最方便的方法是使用Response类提供的set_cookie()方法。
- set_cookie视图会在生成的响应报文首部中创建一个Set-Cookie字段,即“Set-Cookie: key1=value1;key2=value2;...”
- 当浏览器保存了服务器端设置的cookie后,浏览器再次发送该服务器的请求会自动携带设置的cookie信息,cookie信息存储在请求首部设置的Cookie字段中。
- 从cookie中获取值:request.cookies.get("key", "default_value")
Response类的常用属性和方法
- headers 一个Werkzeug的Headers对象,表示响应首部,可以像字典一样操作;
- status 状态码,文本类型
- status_code 状态码,整型
- mimetype MIME类型
- set_cookie() 用来设置一个cookie
session:安全的Cookie
- 在浏览器中手动添加和修改Cookie是很容易的事,因此需要对敏感的Cookie内容进行加密。
- Flask提供的session对象用来将Cookie数据加密存储。
- 默认情况下,它会把数据存储在浏览器中一个名为session的Cookie里。
- 设置程序秘钥:session通过秘钥(具有一定复杂度和随机性的字符串)对数据进行签名以加密数据。
- 秘钥可以通过app.secret_key="secret key"设置,更安全的办法是写入环境变量或保存在.env文件中,然后通过app.secret_key=os.getenv("SECRET_KEY", "secret key") 设置。
- 在生产环境中处于安全考虑,必须使用随机生成的秘钥。
使用session模拟用户登录
- session对象可以像字典一样操作,例如,我们向session中添加一个logged-in-cookie,将它的值设为True,表示用户已认证。
- 当我们使用session对象添加cookie时,数据会使用程序的秘钥对其进行签名,加密后的数据存储在一块名为session的cookie里。
- 使用session对象存储的cookie,用户可以看到其加密后的值,但无法修改它。因为session中的内容使用秘钥进行签名,一旦数据被修改,签名的值也会变化。这样在读取时就会验证失败,对应的session值也会随之失效。所以,除非用户知道秘钥,否则无法对session cookie的值进行修改。
- 默认情况下,session_cookie会在用户关闭浏览器时删除。通过将session.permanent属性设为True可以将session的有效期延长为Flask.permanent_session_lifetime属性值对应的datetime.timedelta对象,也可通过配置变量PERMANENR_SESSION_LIFETIME设置,默认为31天。
- 尽管session对象会对cookie进行签名加密,但这种方式仅能确保session的内容不会被篡改,加密后的数据借助工具仍能被轻易读取。因此决不能在session中存储敏感信息,比如用户密码。
Flask上下文
- 我们可以把程序中的上下文理解为当前环境的快照;
- Flask中有2中上下文:应用上下文和请求上下文;应用上下文存储了程序运行所需的信息;当客户端发送请求时,请求上下文就登场了,请求上下文包含了请求中的所有信息,如URL,HTTP方法等。
上下文全局变量
- 我们在全局导入时request只是一个普通的Python对象,为什么在处理请求时,视图函数中的request就会自动包含请求的数据?这是因为Flask会在每个请求产生后自动激活当前请求的上下文,激活请求上下文后,request被临时设为全局可访问。而当每个请求结束后,Flask就自动销毁对应的请求上下文。
- 在多线程服务器中,在同一时间可能会有多个请求在处理。假设有3个客户端同时向服务器发送请求,这时每个请求都有各自不同的请求报文,所以请求对象也必然是不同的。因此,请求对象只在各自的线程内是全局的。
- 应用上下文变量
- current_app 指向处理请求的当前应用实例;
- 替代Python的全局变量用法,确保仅在当前请求中可用,用于存储全局数据,每次请求都会重设。
- 请求上下文
- request 封装客户端发出的请求报文数据。
- session 用于记住请求间的数据,通过签名的Cookie实现。
- 这4个对象都是代理对象,可以调用get_current_object()获取被代理的真实对象。
current_app 当前应用实例:
- 既然有了应用实例app,为什么还需要current_app变量?
- 在不同的视图函数中,request对象表示和视图函数对应的请求,即当前请求。而程序也会有多个程序实例的情况,为了获取对应的程序实例,而不是固定的某一程序实例,我们就需要使用current_app实例。
g
- g存储在应用上下文,而应用上下文会随着每一个请求的进入而激活,随着每一个请求的处理完毕而销毁,所以每次请求都会重设这个值。
- 通常使用g结合请求钩子来保存每个请求处理前所需的全局变量,比如当前登入的用户对象,数据库连接等。
- g也支持字典类似get()、pop()以及setdefault()方法进行操作。
激活上下文
- 以下情况将自动激活应用上下文:
- 使用flask run启动程序时;
- 使用就得app.run()方法启动程序时;
- 使用@app.cli.command()装饰器注册的Flask命令时;
- 使用flask shell命令启动python shell时;
- 当请求进入时,请求上下文被自动激活,我们可以使用request和session变量,与此同时应用上下文也被自动激活。
- 手动激活上下文:
- 应用上下文对象使用app.app_context()获取,我们可以使用with语句执行上下文操作;
>> from app import app >> from flask import current_app >> with app.app_context(): .... current_app.name 'app'
- 或是显式地使用push()方法推送(激活)上下文;在执行完毕时使用pop()方法销毁上下文
>> from app import app >> from flask import current_app >> app_ctx = app.app_context() >> app_ctx.push() >> current_app.name 'app' >> app_ctx.pop()
- 通过app.test_request_context(),激活请求上下文:
>> from app import app >> from flask import current_app >> with app.test_request_context('/hello'): ... request.method 'GET'
- 也可以使用push()和pop()激活和销毁上下文。
- 应用上下文对象使用app.app_context()获取,我们可以使用with语句执行上下文操作;
上下文钩子
- teardown_appcontext钩子,使用它注册的回调函数会在程序上下文被销毁时调用;
- teatdowm_appcontext装饰的回调函数需要接收异常对象作为参数,当请求正常处理时,这个值为None,此函数的返回值将被忽略。
获取上一个界面的URL
- HTTP referer:当用户在某个站点单击链接,浏览器向新链接所在服务器发起请求,请求的数据中包含HTTP_REFERER字段,记录了用户所在的原站点URL:return redirect(request.referrer)
- 但很多情况下referrer字段会是空值,我们需要添加一个备胎:return redirect(request.referreer or url)
- 除了自动从referrer获取,更常见的方式是在URL中手动加入当前页面的URL查询参数,这个查询参数一般命名为next.
对URL进行安全验证
- 鉴于refferrer和next查询参数容易被篡改的特性,如果我们部队这些值进行验证,就会形成开放重定向漏洞。
- 确保URL安全的关键就是判断URL是否属于程序内部。
使用AJAX技术渲染异步请求
- AJAX可以避免每次都渲染整个界面,不仅可以增强用户体验,也降低了服务器的负载。
- JQuery提供的请求发送方法:用于发送GET请求的get()方法,用于发送POST请求的post()方法;用于获取json数据的getjson()方法,还有直接用于获取脚本的getscript()方法。这些方法都是基于ajax()方法实现的。
- 程序中某些接收AJAX请求的视图并不需要返回数据给客户端,比如用来删除资源的视图。这时我们可以直接返回空值,并将状态码指定为204(表示无内容):return '',204
- Jinjia2提供的generate_lorem_ipsum()返回由随机字符组成的虚拟文章。
HTTP服务器端推送
不论是传统的HTTP请求-响应模式,还是异步的AJAX请求,服务器端始终处于被动的应答状态,只有在客户端发出请求的情况下,服务器端才会响应。这种模式被称为客户端拉取(client pull)。在这种模式下,用户只能通过刷新页面或主动单价加载按钮来拉取数据。但是在趣多关注实行性的场景下,我们需要的通信模式是服务器端主动推送(server push),比如社交网站在导航栏实时显示新题型和私信的数量,用户的在线状态更新,股票行情监控,显示商品库存信息,多人游戏,文档协作等。
常用的HTTP Server Push技术
- 传统轮询:在特定的时间间隔内,客户端使用AJAX技术不断向服务器端发起HTTP请求,然后获取新的数据并更新网页;
- 长轮询:和传统轮询类似,但是如果服务器端没有返回数据,那就保持连接一直开启,知道有数据时才返回。取回数据后再次发送另一个请求。
- Server-Sent Event(SSE):SSE通过H5中的EventSource API来实现。SSE会在客户端和服务器端建立一个单向的通道,客户端监听来自服务器端的数据,而服务器端可以在任何时间发送数据,两者建立类似订阅发布的通信模式。
轮询(pulling)这类使用AJAX技术模拟服务器端推送的方法实现起来比较简单,但是通常会造成服务器上资源的浪费,增加服务器的负载,而且由于频繁地发送AJAX请求,会让用户的设备浪费更多地电量。SSE效率更高,基本支持所有的主流浏览器,但浏览器通常会限制标签页的连接数量。
H5 WebSocket协议
在HTML5的API中还包含了一种WebSocket协议,和HTTP不同,它是基于TCP的全双工通信协议。和服务器端推送技术相比,WebSocket实时性更强,而且可以实现双向通信。WebSocket的浏览器兼容性要强于SSE。
WEB安全防范
对于WEB程序的安全问题,一个首要的原则是:永远不要相信你的用户。大部分WEB安全问题都是因为没有对用户输入的内容进行“消毒”造成的。
- 注入攻击
- 包含系统命令注入、SQL注入、NoSQL注入、ORM注入等;
- SQL注入攻击原理:在编写SQL时,如果直接将用户传入的数据作为参数使用字符串拼接的方式插入到SQL查询中,那么攻击者可以通过SQL语句做任何事:获取敏感数据、删除数据、删除数据库表...;
- 在SQL中;用来结束一行语句;“--”用来注释后面的语句;
- 主要防范方法:①使用ORM可以一定程度上避免SQL注入问题;②验证输入类型;③参数化查询(使用各类接口库提供的参数化查询方法,如sqlite3:db.execute('SELECT * FROM students WHERE password=?', password))。④转义字符:比如引号,分号和横线等。使用参数化查询时各种接口库会为我们做转义工作。
- XSS攻击
- XSS(跨站脚本)是注入攻击的一种,攻击者通过将代码注入被攻击者的网站中,用户一旦访问网页便会执行被注入的恶意脚本。XSS攻击主要分为反射型XSS攻击和存储型XSS攻击两类。
- 典型的反射型XSS攻击:通过URL注入攻击脚本,只有当用户访问这个URL时才会执行攻击脚本。如:http://localhost:5000/?name=%3Cscript%3Ealert(%27heihei%27);%3C/script%3E
- 如果网站A存在XSS漏洞,攻击者将包含攻击代码的链接发送给网站A的用户Jack,当Jack访问这个链接就会执行攻击代码,从而受到攻击。
- 存储型XSS,会把攻击代码存到数据库中,任何用户访问包含攻击代码的页面都会被殃及。比如某个网站通过表单接收用户的留言,如果服务器接收数据后未经处理就存储到数据库中,那么用户可以在留言中插入任意的JS代码。例如,攻击者在留言板中加入一行重定向代码:<script>window.location.href="http://attacker.com"</script>用户一旦访问该页面就会执行JS脚本,被重定向到攻击者写入的网站。
- 防范措施:
- HTML转义:防范XSS攻击最主要的方式是对用户输入内容进行HTML转义,转移后可以确保用户输入的内容在浏览器中作为文本显示,而不是作为代码解析;Jinjia2提供的escape()函数可以对用户输入的数据进行转义,文本中的特殊字符都将被转义为HTML实体,这行文本最终在浏览器中会被显示为文本形式的"<script>alert("Bingo")</script>";
- 验证用户输入:除了转义用户输入外,我们还需要对用户的输入数据进行类型验证。如一个转义无法避免的XSS攻击:<a href="{{ url }}">Website</a> ----> <a href="javascript:alert('bingo')">Website</a>;<img src="{{ url }}"> ----> <img src="123" οnerrοr="alert('Bingo!')">;
- 对用户输入的数据进行过滤:仅保留少量允许使用的HTML标签,同时还需要注意过滤HTML标签的属性;
- CSRF攻击
- 攻击原理:CSRF攻击的大致方式如下:某用户登录了A网站,认证信息保存在cookie中。当用户访问攻击者创建的B网站时,攻击者通过在B网站发送一个伪造的请求到A网站服务器上,让A网站服务器误以为请求来自于自己的网站,于是执行相应的操作,该用户的信息遭到了篡改。总结起来就是,攻击者利用用户在浏览器中保存的认证信息,向对应的站点发送伪造请求。
- 防范措施:
- 正确使用HTTP方法:GET方法属于安全方法,不会改变资源状态,仅用于获取资源。页面中所有可以通过链接发起的请求,都是GET请求;POST方法用于创建、删除和修改资源。在HTML中通过form标签创建表单并设置提交方法为POST,在提交时会创建POST请求。
- 使用HTTP referer获取请求来源,但HTTP referer容易被修改和伪造,所以不能作为主要防控措施。
- CSRF令牌校验:在客户端页面中加入伪随机数来防御CSRF攻击,这个伪随机数通常被称为csrf token(CSRF令牌)。CSRF令牌在用户向包含表单的页面发起GET请求时创建,并在一定时间内过期,一般情况下攻击者无法获取到这个令牌值,所以我们可以有效地区分出请求的来源是否安全。通常使用扩展实现CSRF令牌的创建和验证工作,比如Flask-WTF。