转载:http://book.odoomommy.com/chapter5/README10.html
第十章 http.py
我们知道每个odoo请求在controller中都可以用request来处理,那么从请求到controller中的request,系统是如何处理这步中的session和数据库等变量的呢?关于request,还有多少内容是我们不知道的呢?本章就来揭开Request的面纱。
Request
我们在controller中常用的request实际上是WebRequest的一个实例,WebRequest是odoo中所有Request对象的父类。WebRequest有两个子类,JsonRequest和HttpReqeust,对应我们Web请求中的Http请求和Json请求。
WebRequest
WebReqeust是odoo中所有Request的父类型,其作用是进行请求的初始化封装。我们在进行一个web请求的过程中肯定需要执行对应的数据库名称、当前的用户id等变量,这些都是在WebRequest中进行加载的。
我们先来看一下WebRequest的初始化方法:
def __init__(self, httprequest):
self.httprequest = httprequest
self.httpresponse = None
self.disable_db = False
self.endpoint = None
self.endpoint_arguments = None
self.auth_method = None
self._cr = None
self._uid = None
self._context = None
self._env = None
# prevents transaction commit, use when you catch an exception during handling
self._failed = None
# set db/uid trackers - they're cleaned up at the WSGI
# dispatching phase in odoo.service.wsgi_server.application
if self.db:
threading.current_thread().dbname = self.db
if self.session.uid:
threading.current_thread().uid = self.session.uid
我们常用的request并非最原始的web请求,原始的web请求被封装在httprequest变量中。odoo的web服务器使用的werkzeug,这里httprequest就是werkzeug的请求对象。
cr
WebRequest的cr属性返回当前数据库的游标。如果当前尚未绑定数据库则会引发异常。
uid
uid返回当前请求对象的用户UID
env
返回请求的环境变量对象env。
lang
返回当前的上下文的语言设置。
csrf_token
我们在请求web也页面时,odoo会给我们返回一个防跨域请求的token值,每次请求都要带着这个放跨域的请求值才会被认为合法请求。那么csrf_token是如何生成的呢?
WebRequest的内部有一个crsf_token的方法,其代码如下:
def csrf_token(self, time_limit=3600):
""" Generates and returns a CSRF token for the current session
:param time_limit: the CSRF token should only be valid for the
specified duration (in second), by default 1h,
``None`` for the token to be valid as long as the
current user's session is.
:type time_limit: int | None
:returns: ASCII token string
"""
token = self.session.sid
max_ts = '' if not time_limit else int(time.time() + time_limit)
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
hm = hmac.new(secret.encode('ascii'), msg.encode('utf-8'), hashlib.sha1).hexdigest()
return '%so%s' % (hm, max_ts)
从上面的定义我们可以看出来csrf_token的生成机制:
- session的sid和超时时间max_ts组成待加密的字符串msg
- 数据库的密钥作为加密密钥secret
- 使用hmac的sha1算法以secret为密钥,msg为待加密字符进行加密
- 算出的16进制结果+o+超时时间即为crsf_token。
crsf_token的验证机制
前面讲到了csrf_token的生成机制,那么odoo又是如何对csrf_token进行验证的呢?
def validate_csrf(self, csrf):
if not csrf:
return False
try:
hm, _, max_ts = str(csrf).rpartition('o')
except UnicodeEncodeError:
return False
if max_ts:
try:
if int(max_ts) < int(time.time()):
return False
except ValueError:
return False
token = self.session.sid
msg = '%s%s' % (token, max_ts)
secret = self.env['ir.config_parameter'].sudo().get_param('database.secret')
assert secret, "CSRF protection requires a configured database secret"
hm_expected = hmac.new(secret.encode('ascii'), msg.encode('utf-8'), hashlib.sha1).hexdigest()
return consteq(hm, hm_expected)
验证的机制也很简单,首先验证请求中的token的时间是否小于当前时间,如果是,那么意味着该token已经失效,需要重新获取。否则将结果进行拆分,因为request.session.sid和secret都不会变,则重新计算结果跟传入的值是否一致即可。
关于csrf_token的生成方式:QWeb端已由系统处理,即默认的页面会自己带着csrf_token。Json请求的话,使用
require(web.core).csrf_token
获取即可。
JsonRequest
JsonRequest用来处理jsonrpc 2.0的请求,一个典型的jsonrpc请求如下:
{
"jsonrpc": "2.0",
"method": "call",
"params": {"context": {},
"arg1": "val1" },
"id": null}
返回值:
{
"jsonrpc": "2.0",
"result": { "res1": "val1" },
"id": null}
如请求中包含错误,那么返回值是:
{
"jsonrpc": "2.0",
"error": {"code": 1,
"message": "End user error message.",
"data": {"code": "codestring",
"debug": "traceback" } },
"id": null}
那么JsonRPC是如何调用后台的方法的呢?
实际上不论是HTTP请求还是JSONRPC请求,其内部都是调用了WebRequest的 _call_function方法。
_call_function方法会将请求中的model、method匹配到对应的模型和方法,然后将调用的结果回传给前台。
HttpRequest
HttpRequest 用来处理http类型的请求,查询参数和表单参数,文件等都通过关键字参数形式传递给处理函数。
HttRequest的返回内容可以是可以被当作false的值,这种情况下,返回的状态码将会是204,也可以是werkzeug的返回对象,这个对象将被渲染诚HTML显示在页面上。
在原生的httprequest对象中,Query参数是args,form参数在form参数中,文件参数在files中,而在HttpRequest中这些参数都集合到了参数params中,params是个有序字典。
HttpRequest的核心方法:
def dispatch(self):
if self._is_cors_preflight(request.endpoint):
headers = {
'Access-Control-Max-Age': 60 * 60 * 24,
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
}
return Response(status=200, headers=headers)
if request.httprequest.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE') \
and request.endpoint.routing.get('csrf', True): # csrf checked by default
token = self.params.pop('csrf_token', None)
if not self.validate_csrf(token):
if token is not None:
_logger.warning("CSRF validation failed on path '%s'",
request.httprequest.path)
else:
_logger.warning("""No CSRF validation token provided for path '%s'
......
""", request.httprequest.path)
raise werkzeug.exceptions.BadRequest('Session expired (invalid CSRF token)')
r = self._call_function(**self.params)
if not r:
r = Response(status=204) # no content
return r
其内部同样调用了WebRequest中的_call_function方法。
HttpRequest还有另外一个两个方法:
- make_response: 生产响应内容。
- render: 渲染QWeb。
make_response
make_response方法用来产生HTML响应内容或者非HTML响应内容。可以通过此方法定制响应头和cookies。
def make_response(self,data,headers=None, Cookies=None):
pass
- data: 响应体内容
- headers: HTTP响应头
- cookies: Cookies(Mapping)
render
render方法用来渲染QWeb模板。
def render(self, template, qcontext=None, lazy=True, **kw):
pass
- template: 要渲染的模板
- qcontext: 渲染模板需要的上下文变量context
- lazy: 是否延迟到最后一刻进行渲染
- kw: 转发给werkzeug的响应对象
EndPoint
EndPoint对象指的是Web中的访问入口,其主要包含如下几个属性:
- method: 方法
- original: 源方法
- routing: 路由
- arguments: 参数
class EndPoint(object):
def __init__(self, method, routing):
self.method = method
self.original = getattr(method, 'original_func', method)
self.routing = routing
self.arguments = {}
@property
def first_arg_is_req(self):
# Backward for 7.0
return getattr(self.method, '_first_arg_is_req', False)
def __call__(self, *args, **kw):
return self.method(*args, **kw)
从代码中可以得出,EndPoint调用即调用自身的method方法。
Response
Response是Controller返回给调用者的响应结果,odoo的Response对象是在werkzeug的Response对象上继承而来,添加了额外的用来渲染QWeb的参数。
初始化参数列表:
- template: 模板
- qcontext: 渲染模板需要的上下文
- uid: 用来请求ir.ui.view的用户ID,不填则使用请求的uid
Session
odoo的Session机制是在werkzeug的Session基础上拓展而来的,默认的Session存储方式是使用文件存储。存储的路径可以通过配置文件的data_dir节点设置,不同的系统默认的路径不同,比如Ubuntu默认的存储文件路径在当前用户的主目录下的.local文件中:
~/.local/share/Odoo
配置文件中的data_dir不但指session的存储文件夹,还包括附件和第三方模块拓展包。如果你看过data_dir文件夹下的内容你就会发现,sessions文件夹里存储的是session文件,filestore文件夹内存储的是附件,addons是第三方模块。
默认情况下,Odoo的Session过期时间是一周。当一个请求过来时,Odoo会检查其携带的session_sid参数,如果 session_sid存在则将其对应的session返回,否则创建一个新的session并返回。
Odoo判断Session过期的原理是判断session的存储文件的最后更新时间与当前的时间差,如果超过session定义的时间(默认一周)则会将session文件删除。
另外Session对象提供了一个用来验证用户账号的方法:authenticate
def authenticate(self, db, login=None, password=None, uid=None):
"""
Authenticate the current user with the given db, login and
password. If successful, store the authentication parameters in the
current session and request.
:param uid: If not None, that user id will be used instead the login
to authenticate the user.
"""
if uid is None:
wsgienv = request.httprequest.environ
env = dict(
base_location=request.httprequest.url_root.rstrip('/'),
HTTP_HOST=wsgienv['HTTP_HOST'],
REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
)
uid = odoo.registry(db)['res.users'].authenticate(db, login, password, env)
else:
security.check(db, uid, password)
self.rotate = True
self.db = db
self.uid = uid
self.login = login
self.session_token = uid and security.compute_session_token(self, request.env)
request.uid = uid
request.disable_db = False
if uid: self.get_context()
return uid
其内部用使用了res.users对象的authenticate方法完成对用户账号密码的认证,如果用户合法,那么系统将把uid等参数附加到session中。