Python Web 模块之 Flask v0.1¶
第 2 部分 源码阅读准备¶
2.3 Flask 工作流程与机制¶
2.3.5 Session¶
2.3.5.1 操作 session¶
在这些配置键中 , SESSION_COOKIE_NAME 也可以通过 Flask 类的属性来设置 , 分别为 session_cookie_name , 但是 PERMANENT_SESSION_LIFETIME (permanent_session_lifetime) 在 0.1 版本中并不存在 。 session cookie 的值 (value) 由下面这行代码生成 :
data = self.serialize(session_expires or expires)
def serialize(self, expires=None):
"""Serialize the secure cookie into a string.
If expires is provided, the session will be automatically invalidated
after expiration when you unseralize it. This provides better
protection against session cookie theft.
:param expires: an optional expiration date for the cookie (a
:class:`datetime.datetime` object)
"""
if self.secret_key is None:
raise RuntimeError('no secret key defined')
if expires:
self['_expires'] = _date_to_unix(expires)
result = []
mac = hmac(self.secret_key, None, self.hash_method)
for key, value in sorted(self.items()):
result.append('%s=%s' % (
url_quote_plus(key),
self.quote(value)
))
mac.update('|' + result[-1])
return '%s?%s' % (
mac.digest().encode('base64').strip(),
'&'.join(result)
)
在 0.10 版本以前 , session 序列化为 cookie 的格式为 pickle 。 之后更换为 JSON 格式是为了增强安全性 , 避免密钥泄露导致的攻击 。
2.3.5.2 Session 起源¶
在上一节我们知道 , session 变量在请求上下文中创建 , 因此为了探寻 session 的起源 , 我们需要将断点设置到创建请求上下文之前 , 比如在Flask类的 __call__ 方法中 。 不过 , 这样的话整个过程就掺杂了太多不相关的操作 , 需要频繁使用 Step Out 按钮 , 作为替代 , 可以采取手动探索的方式来探寻 session 的起源 。
既然 session 变量在 _RequestContext 中创建 , 那么生成 session 对象的操作也应该在这里 。 打开搜索功能 , 找到 _RequestContext 的定义后发现相关的代码在 __init__方法中 , 如代码清单所示 。
class _RequestContext(object):
def __init__(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ)
self.request = app.request_class(environ)
self.session = app.open_session(self.request)
self.g = _RequestGlobals()
self.flashes = None
请求上下文的 __init__() 方法中调用了 open_session() 方法来创建 session , 也就是说 , 一旦接收到请求 , 就会创建 session 对象 。 open_session() 方法接收程序实例和请求对象作为参数 , 我们可以猜想到 , 程序实例是用来获取密钥验证 session 值 , 而请求对象参数是用于获取请求中的 cookie 。 open_session() 方法的定义如代码清单所示 。
[flask.py]
class Flask(object):
def open_session(self, request):
"""Creates or opens a new session. Default implementation stores all
session data in a signed cookie. This requires that the
:attr:`secret_key` is set.
:param request: an instance of :attr:`request_class`.
"""
key = self.secret_key
if key is not None:
return SecureCookie.load_cookie(request, self.session_cookie_name,
secret_key=key)
在这个方法中 , 如果请求的 cookie 里包含 session 数据 , 就解析数据到 session 对象里 , 否则就生成一个空的 session 。 这里要注意的是 , 如果没有设置秘钥 , open_session() 会返回 None , 这时在 push() 方法中会调用 make_null_session 来生成一个无效的 session 对象 (NullSession 类) , 对其执行字典操作时会显示警告 。 最终返回的 session , 就是我们一开始在视图函数里使用的那个 session 对象 , 这就是 session 的整个生命轨迹 。
签名可以确保 session cookie 的内容不被篡改 , 但这并不意味着没法获取加密前的原始数据 。 事实上 , session cookie 的值可以轻易地被解析出来 (即使不知道密钥) , 这就是为什么我们曾频繁提到 session 中不能存入敏感数据 。 下面是使用 itsdangerous 解析 session 内容的示例 :
>>> from itsdangerous import base64_decode
>>> s = 'eyJjc3JmX3Rva2VuIjp7IiBiI...'
>>> data, timstamp, secret = s.split('.')
>>> base64_decode(data)
'{"answer":42}'
Flask 提供的 session 将用户会话存储在客户端 , 和这种存储在客户端的方式相反 , 另一种实现用户会话的方式是在服务器端存储用户会话 , 而客户端只存储一个 session ID 。 当接收到客户端的请求时 , 可以根据 cookie 中的 session ID 来找到对应的用户会话内容 。 这种方法更为安全和强健 , 你可以使用扩展 Flask-Session (https://github.com/fengsp/flask-session) 来实现这种方式的 session 。
2.3.6 模板渲染¶
在视图函数中 , 我们使用 render_template() 函数来渲染模板 , 传入模板的名称和需要注入模板的关键词参数 :
[example]
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/hello')
def hello():
name = 'Flask'
return render_template('hello.html', name=name)
我们在 return 语句这一行设置断点 , 程序运行到断点后的第一次步进会调用 render_template() 函数 。 render_template() 函数的定义在脚本中 , 如下所示 。
[flask]
def render_template(template_name, **context):
"""Renders a template from the template folder with the given
context.
:param template_name: the name of the template to be rendered
:param context: the variables that should be available in the
context of the template.
"""
current_app.update_template_context(context)
return current_app.jinja_env.get_template(template_name).render(context)
这个函数接收的 template_name 参数是文件名 , 而 **context 参数是我们调用 render_template() 函数时传入的上下文参数 。
这个函数先获取程序上下文 , 然后调用程序实例的 Flask.update_template_context() 方法更新模板上下文 , update_template_context() 的定义如代码所示 。
[flask.py]
class Flask(object):
def update_template_context(self, context):
"""Update the template context with some commonly used variables.
This injects request, session and g into the template context.
:param context: the context as a dictionary that is updated in place
to add extra variables.
"""
reqctx = _request_ctx_stack.top
for func in self.template_context_processors:
context.update(func())
承接上文 , 我们使用 context_processor 装饰器注册模板上下文处理函数 , 这些处理函数被存储在 Flask.template_context_processors 列表里 :
[flask.py]
class Flask(object):
self.template_context_processors = [_default_template_ctx_processor]
def context_processor(self, f):
"""Registers a template context processor function."""
self.template_context_processors.append(f)
return f
列表中是函数的名称 , 默认的处理函数是 _default_template_ctx_processor() , 它把当前上下文中的 request 、 session 和 g 注入模板上下文 。
[flask.py]
def _default_template_ctx_processor():
"""Default template context processor. Injects `request`,
`session` and `g`.
"""
reqctx = _request_ctx_stack.top
return dict(
request=reqctx.request,
session=reqctx.session,
g=reqctx.g
)
这个 update_template_context() 方法的主要任务就是调用这些模板上下文处理函数 , 获取返回的字典 , 然后统一添加到 context 字典 。 这里先复制原始的 context 并在最后更新了它 , 这是为了确保最初设置的值不被覆盖 , 即视图函数中使用 render_template() 函数传入的上下文参数优先 。
render_template() 函数最后使用这个 context 字典调用了 render() 函数 。 代码如下所示 :
[flask.py]
def render_template(template_name, **context):
current_app.update_template_context(context)
return current_app.jinja_env.get_template(template_name).render(context)
这里对程序实例 app 调用的 Flask.jinja_env() 方法 , 代码如下所示 :
[flask.py]
self.jinja_env = Environment(loader=self.create_jinja_loader(),
**self.jinja_options)
它调用 jinja2.Environment 类创建了一个 Jinja2 环境 , 用于加载模板 。 这个属性完成了 Jinja2 环境在 Flask 中的初始化 , 向模板上下文中添加了一些全局对象 (比如 url_for() 函数 、 get_flashed_messages() 函数以及 config 对象等) , 更新了一些渲染设置 。
虽然之前已经通过调用 update_template_context() 方法向模板上下文中添加了 request 、 session 、 g (由 _default_template_ctx_processor() 获取) , 这里再次添加是为了让导入的模板也包含这些变量 。
在调用 render() 函数前 , 经过了一段非常漫长的调用过程 : 模板文件定位 、 加载 、 解析等 。 这个函数是 Jinja2 的 render 函数渲染模板 , 并在渲染前后发送相应的信号 。 渲染工作结束后会返回渲染好的 unicode 字符串 , 这个字符串就是最终的视图函数返回值 , 即响应的主体 , 也就是返回给浏览器的 HTML 页面 。
第 3 部分 源码阅读之 App 代码阅读¶
3.1 App 代码¶
阅读的代码以之前的示例代码为例 :
app = Flask(__name__)
@app.route('/hello/<name>/test', methods=['POST', 'GET'])
def hello_test(name):
if name == "Test":
return 'Test'
else:
return 'hello'
@app.route('/hello/<name>', methods=['POST', 'GET'])
def hello(name):
if name == "Test":
return 'Test'
else:
return 'hello'
@app.route('/')
def index():
return "This is index page"
if __name__ == '__main__':
app.run()
3.2 Flask 初始化¶
uml 见 : Flask-__init__
首先 app 为初始化的 Flask 类对象 , 初始化时传入的参数为 __name__ , 实际就是当前文件名 , 当然在实际使用中可以其他名称 , 但是得符合当前的包名 。 看一下初始化代码 :
class Flask(object):
def __init__(self, package_name):
self.debug = False
self.package_name = package_name
self.root_path = _get_package_path(self.package_name)
self.view_functions = {}
self.error_handlers = {}
self.before_request_funcs = []
self.after_request_funcs = []
self.template_context_processors = [_default_template_ctx_processor]
self.url_map = Map()
if self.static_path is not None:
self.url_map.add(Rule(self.static_path + '/<filename>',
build_only=True, endpoint='static'))
if pkg_resources is not None:
target = (self.package_name, 'static')
else:
target = os.path.join(self.root_path, 'static')
self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
self.static_path: target
})
self.jinja_env = Environment(loader=self.create_jinja_loader(),
**self.jinja_options)
self.jinja_env.globals.update(
url_for=url_for,
get_flashed_messages=get_flashed_messages
)
初始化的时候会设置一些属性 , root_path 为当前目录 , 通过 _get_package_path 进行获取 , 其代码为 :
def _get_package_path(name):
"""Returns the path to a package or cwd if that cannot be found."""
try:
return os.path.abspath(os.path.dirname(sys.modules[name].__file__))
except (KeyError, AttributeError):
return os.getcwd()
来测试一下这个方法的实际功能 :
def _get_package_path(name):
"""Returns the path to a package or cwd if that cannot be found."""
try:
print 'name', name
return os.path.abspath(os.path.dirname(sys.modules[name].__file__))
except (KeyError, AttributeError):
return os.getcwd()
print _get_package_path('flask.py')
>>>name __main__
>>>name flask.py
>>>E:\Projects\github\flask
我有些不解的是 name 为何会是 __main__ ? 最终就是获取绝对路径的功能 。
self.template_context_processors 的值为 [_default_template_ctx_processor] , 实际结果是当前请求上下文的参数字典 :
def _default_template_ctx_processor():
"""Default template context processor. Injects `request`,
`session` and `g`.
"""
reqctx = _request_ctx_stack.top
return dict(
request=reqctx.request,
session=reqctx.session,
g=reqctx.g
)
返回的是当前请求上下文的 request , session 和 g 字典 。
self.url_map 是一个 werkzeug.routing.Map 类实例 , 下面后用到 。 当 static_path 为空的时候 , 不做操作 , 但是 static_path 在类里面已经赋值为 static_path = '/static' 它是有值的 , 所以会将 static_path 添加到路由表中 , 端点为 static 。
self.jinja_env 为魔板渲染引擎 jinja 的环境 。
3.3 Flask route¶
uml: Flask-route.puml
@app.route('/hello/<name>/test', methods=['POST', 'GET'])
def hello_test(name):
if name == "Test":
return 'Test'
else:
return 'hello'
def route(self, rule, **options):
def decorator(f):
self.add_url_rule(rule, f.__name__, **options)
self.view_functions[f.__name__] = f
return f
return decorator
以 hello_test 为例 , 在 route 函数中 , rule = '/hello/<name>/test' , options = {'methods': ['POST', 'GET']} , decorator 的参数 f = hello_test , 然后执行步骤为 :
执行 route 函数时 , 直接返回的是 decorator 对象
decorator 对象内部仍有执行步骤 , 首先将 hello_test 对象传入到 decorator 内部 。
将 rule , hello_test 对象的名称 'hello_test' 和 options 作为参数传入到 add_url_rule 函数内部 , 执行相关操作 , 详情见下一节 。
将 hello_test 添加到 view_functions 字典中 , 形如 : {'hello_test': hello_test}
decorator 对象中返回 hello_test 对象
route 函数返回 decorator 对象
3.4 Flask add_url_rule¶
uml: Flask-add_url_rule.puml
def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
self.url_map.add(Rule(rule, **options))
def add_url_rule(self, rule, endpoint, **options):
options['endpoint'] = endpoint
options.setdefault('methods', ('GET',))
a = Rule(rule, **options)
self.url_map.add(Rule(rule, **options))
接着上述分析 , 执行到 add_url_rule 时 , rule = '/hello/<name>/test' , endpoint = 'hello_test' , options = {'methods': ['POST', 'GET']} , 对 add_url_rule 做一下变形 , 方便调试看结果 。
对 a 下断点 , 执行完毕后 url_map 为
Map([[<Rule '/static/<filename>' -> static>, <Rule '/hello/<name>/test' (POST, HEAD, GET) -> hello_test>]])
首先设置 options 字典中 endpoint 字段为 'hello_test' , 同时设置默认 methods 字段为 ('GET',) , 如果代码已经设置 , 就不用修改 , 否则使用默认的 , 最后将路由规则添加到 url_map 中 , 由于使用的是 werkzeug 中的方法 , 这里就不在分析 , 直接看结果 。
3.5 Flask run¶
uml: Flask-run.puml
def run(self, host='localhost', port=5000, **options):
from werkzeug import run_simple
if 'debug' in options:
self.debug = options.pop('debug')
options.setdefault('use_reloader', self.debug)
options.setdefault('use_debugger', self.debug)
return run_simple(host, port, self, **options)
在上述 exam 代码中 , route 添加完毕之后 , 将 App 运行起来需要执行 run 函数 。
首先会判断 debug 是不是在 options 字典里面 , 如果在 , self.debug = debug 字段的值 , 同时将 use_reloader 字段和 use_debugger 字段设置为 self.debug 的值 , 最终执行 werkzeug.run_simple 函数将 App 运行起来 , run_simple 的参数分别是 host , port , self 以及 options 字典 , 其中 host 和 port 都有默认值 , 而 self 参数就是 Flask 类的实例化 , 在 run_simple 中会调用该实例 , 也因此会执行 Flask.__call__() 函数 , 之前的 wsgi 以类的形式实现时说过了 。 接下来执行 __call__() 函数 。
未完待续 ...
上一篇文章 : 上一篇
下一篇文章 : 下一篇