第十章 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的生成机制:

  1. session的sid和超时时间max_ts组成待加密的字符串msg
  2. 数据库的密钥作为加密密钥secret
  3. 使用hmac的sha1算法以secret为密钥,msg为待加密字符进行加密
  4. 算出的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中。

results matching ""

    No results matching ""