Openstack Keystone 认证流程(五)--路由

1. 路由实现

在上一章中, 我们一起过了admin_api的所有流水线的处理, 其中有好几个点是用来添加路由信息的。这一章我们来详细看看路由的具体实现。

路由是MVC架构中非常重要的一个关键节点, 可以说, 如果没有路由,就不可能去实现一个很好的MVC架构, 路由也是整个系统中最先处理的节点。如果想弄清楚Keystone的整体架构, 路由是必须先搞清楚的。

看具体实现之前, 我们先看看路由的具体用法。比如说有如下一条路由:

import routes.middleware
...
class Ec2Extension(wsgi.ExtensionRouter):
    def add_routes(self, mapper):
        ec2_controller = controllers.Ec2Controller()
        # validation
        mapper.connect(
            '/ec2tokens',
            controller=ec2_controller,
            action='authenticate',
            conditions=dict(method=['POST']))

假设Keystone的服务器为127.0.0.1:35357, 那么如果我们访问如下地址时:
http://127.0.0.1:35357/ec2tokens
经过路由分发, 代码就会跑到类ec2_controller的authenticate方法中。

知道了怎么使用之后, 我们再来看看它的具体实现。我们找到Ec2Extension的路由父类:

class ExtensionRouter(Router):
    def __init__(self, application, mapper=None):
        if mapper is None:
            mapper = routes.Mapper()
        self.application = application
        self.add_routes(mapper)
        mapper.connect('{path_info:.*}', controller=self.application)
        super(ExtensionRouter, self).__init__(mapper)

    def add_routes(self, mapper):
        pass

    @classmethod
    def factory(cls, global_config, **local_config):
        def _factory(app):
            conf = global_config.copy()
            conf.update(local_config)
            return cls(app, **local_config)
        return _factory

在ExtensionRouter的__init__ 方法中, routes.Mapper创建了一个Mappper对象。这是一个实际完成路由功能的模块, 这里我们不去管里面的具体实现, 有兴趣的可以参考routes – Route and Mapper core classes
至此,可以看到, 在ExtensionRouter的factory方法中,创建一个ExtensionRouter的一个实例, 并在其它构造方法中,创建Mapper对象,并把所有的路由信息全部增加到Mapper对象中,然后调用其它父类的构造方法。继续看看Router类的构造方法。

class Router(object):
    def __init__(self, mapper):
        if CONF.debug:
            logging.getLogger('routes.middleware')

        self.map = mapper
        self._router = routes.middleware.RoutesMiddleware(self._dispatch,self.map)

    @webob.dec.wsgify(RequestClass=Request)
    def __call__(self, req):
        return self._router

    @staticmethod
    @webob.dec.wsgify(RequestClass=Request)
    def _dispatch(req):
        match = req.environ['wsgiorg.routing_args'][1]
        if not match:
            return render_exception(
                exception.NotFound(_('The resource could not be found.')),
                user_locale=req.best_match_language())
        app = match['controller']
        return app

在Route类的__init__ 中使用, 使用Router._dispath方法作为一个中间件的应用程序初始化一个中间件。找到这个中间件的实现方法。在routes.middleware.py 中, 其实现如下:

class RoutesMiddleware(object):
        self.app = wsgi_app
        self.mapper = mapper
        self.singleton = singleton
        self.use_method_override = use_method_override
        self.path_info = path_info
        self.log_debug = logging.DEBUG >= log.getEffectiveLevel()
        if self.log_debug:
            log.debug("Initialized with method overriding = %s, and path "
                      "info altering = %s", use_method_override, path_info)

    def __call__(self, environ, start_response):
        """Resolves the URL in PATH_INFO, and uses wsgi.routing_args
        to pass on URL resolver results."""
        old_method = None
        if self.use_method_override:
            req = None

            # In some odd cases, there's no query string
            try:
                qs = environ['QUERY_STRING']
            except KeyError:
                qs = ''
            if '_method' in qs:
                req = Request(environ)
                req.errors = 'ignore'
                if '_method' in req.GET:
                    old_method = environ['REQUEST_METHOD']
                    environ['REQUEST_METHOD'] = req.GET['_method'].upper()
                    if self.log_debug:
                        log.debug("_method found in QUERY_STRING, altering "
                                  "request method to %s",
                                  environ['REQUEST_METHOD'])
            elif environ['REQUEST_METHOD'] == 'POST' and is_form_post(environ):
                if req is None:
                    req = Request(environ)
                    req.errors = 'ignore'
                if '_method' in req.POST:
                    old_method = environ['REQUEST_METHOD']
                    environ['REQUEST_METHOD'] = req.POST['_method'].upper()
                    if self.log_debug:
                        log.debug("_method found in POST data, altering "
                                  "request method to %s",
                                  environ['REQUEST_METHOD'])

        # Run the actual route matching
        # -- Assignment of environ to config triggers route matching
        if self.singleton:
            config = request_config()
            config.mapper = self.mapper
            config.environ = environ
            match = config.mapper_dict
            route = config.route
        else:
            results = self.mapper.routematch(environ=environ)
            if results:
                match, route = results[0], results[1]
            else:
                match = route = None

        if old_method:
            environ['REQUEST_METHOD'] = old_method

        if not match:
            match = {}
            if self.log_debug:
                urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
                                     environ['PATH_INFO'])
                log.debug("No route matched for %s", urlinfo)
        elif self.log_debug:
            urlinfo = "%s %s" % (environ['REQUEST_METHOD'],
                                 environ['PATH_INFO'])
            log.debug("Matched %s", urlinfo)
            log.debug("Route path: '%s', defaults: %s", route.routepath,
                      route.defaults)
            log.debug("Match dict: %s", match)

        url = URLGenerator(self.mapper, environ)
        environ['wsgiorg.routing_args'] = ((url), match)
        environ['routes.route'] = route
        environ['routes.url'] = url

        if route and route.redirect:
            route_name = '_redirect_%s' % id(route)
            location = url(route_name, **match)
            log.debug("Using redirect route, redirect to '%s' with status"
                      "code: %s", location, route.redirect_status)
            start_response(route.redirect_status,
                           [('Content-Type', 'text/plain; charset=utf8'),
                            ('Location', location)])
            return []

        # If the route included a path_info attribute and it should be used to
        # alter the environ, we'll pull it out
        if self.path_info and 'path_info' in match:
            oldpath = environ['PATH_INFO']
            newpath = match.get('path_info') or ''
            environ['PATH_INFO'] = newpath
            if not environ['PATH_INFO'].startswith('/'):
                environ['PATH_INFO'] = '/' + environ['PATH_INFO']
            environ['SCRIPT_NAME'] += re.sub(
                r'^(.*?)/' + re.escape(newpath) + '$', r'\1', oldpath)

        response = self.app(environ, start_response)

        # Wrapped in try as in rare cases the attribute will be gone already
        try:
            del self.mapper.environ
        except AttributeError:
            pass
        return response

代码看起来有点多, 我们只要找关键部分及连接部分。首先这是个可调用的类。在其__call__ 中, 调用其app, 并返回其返回值。
但是这样,肯定是不够的, 因为在之前的步骤中, 我们把所有的路由信息全部交给了它来处理, 也就是说在这里,我们需要找到URL请求中,所对应的controller及action. 仔细查找,可以看到如下代码:

if self.singleton:
            config = request_config()
            config.mapper = self.mapper
            config.environ = environ
            match = config.mapper_dict
            route = config.route
else:
    results = self.mapper.routematch(environ=environ)
    if results:
        match, route = results[0], results[1]
    else:
        match = route = None
...
url = URLGenerator(self.mapper, environ)
environ['wsgiorg.routing_args'] = ((url), match)
environ['routes.route'] = route
environ['routes.url'] = url

这里有两种方法来查找路由。在Keystone的实现中,是通过routes.middleware.RoutesMiddleware(self._dispatch,self.map) 来实现的, 所以self.singleton的值为默认值True.

那么这里又通过config = request_config()创建了一个新的对象。继续找到其实现, 在routes 的__init__.py中, 代码如下:

def request_config(original=False):
        obj = _RequestConfig()
    try:
        if obj.request_local and original is False:
            return getattr(obj, 'request_local')()
    except AttributeError:
        obj.request_local = False
        obj.using_request_local = False
    return _RequestConfig()

它创建了一个_RequestConfig对象,在同一个文件中, 找到这个类的实现。

class _RequestConfig(object):
    __shared_state = threading.local()

    def __getattr__(self, name):
        return getattr(self.__shared_state, name)

    def __setattr__(self, name, value):
        """
        If the name is environ, load the wsgi envion with load_wsgi_environ
        and set the environ
        """
        if name == 'environ':
            self.load_wsgi_environ(value)
            return self.__shared_state.__setattr__(name, value)
        return self.__shared_state.__setattr__(name, value)

    def __delattr__(self, name):
        delattr(self.__shared_state, name)

    def load_wsgi_environ(self, environ):
        """
        Load the protocol/server info from the environ and store it.
        Also, match the incoming URL if there's already a mapper, and
        store the resulting match dict in mapper_dict.
        """
        if 'HTTPS' in environ or environ.get('wsgi.url_scheme') == 'https' \
           or environ.get('HTTP_X_FORWARDED_PROTO') == 'https':
            self.__shared_state.protocol = 'https'
        else:
            self.__shared_state.protocol = 'http'
        try:
            self.mapper.environ = environ
        except AttributeError:
            pass

        # Wrap in try/except as common case is that there is a mapper
        # attached to self
        try:
            if 'PATH_INFO' in environ:
                mapper = self.mapper
                path = environ['PATH_INFO']
                result = mapper.routematch(path)
                if result is not None:
                    self.__shared_state.mapper_dict = result[0]
                    self.__shared_state.route = result[1]
                else:
                    self.__shared_state.mapper_dict = None
                    self.__shared_state.route = None
        except AttributeError:
            pass

        if 'HTTP_X_FORWARDED_HOST' in environ:
            # Apache will add multiple comma separated values to
            # X-Forwarded-Host if there are multiple reverse proxies
            self.__shared_state.host = \
                environ['HTTP_X_FORWARDED_HOST'].split(', ', 1)[0]
        elif 'HTTP_HOST' in environ:
            self.__shared_state.host = environ['HTTP_HOST']
        else:
            self.__shared_state.host = environ['SERVER_NAME']
            if environ['wsgi.url_scheme'] == 'https':
                if environ['SERVER_PORT'] != '443':
                    self.__shared_state.host += ':' + environ['SERVER_PORT']
            else:
                if environ['SERVER_PORT'] != '80':
                    self.__shared_state.host += ':' + environ['SERVER_PORT']

在这个类的实现中, 可以看到其只是定义了一些方法, 但是并没有任何调用。
但是再回到RoutesMiddleware的实现中,这里给config对象设置了两个值。

config = request_config()
config.mapper = self.mapper
config.environ = environ
match = config.mapper_dict
route = config.route

这个时候再回头看看_RequestConfig的实现, 可以看到在其setattr 方法中,如果属性名为environ, 它就会调用load_wsgi_environ, 然后就可找到如下代码:

mapper = self.mapper
path = environ['PATH_INFO']
result = mapper.routematch(path)
if result is not None:
    self.__shared_state.mapper_dict = result[0]
    self.__shared_state.route = result[1]
else:
    self.__shared_state.mapper_dict = None
    self.__shared_state.route = None

至此, 可以看到其调用的Mapper对象的routematch方法来查找路由,并将其值放到mapper_dict, route中。最终把它取出来,并放到环境变量中:

match = config.mapper_dict
route = config.route
url = URLGenerator(self.mapper, environ)
environ['wsgiorg.routing_args'] = ((url), match)
environ['routes.route'] = route
environ['routes.url'] = url

现在我们回忆下调用的整体过程:
在服务器收到请求后,交给paste.deployment来处理, 然后paste.deployment调用各个中间件进行处理,直到wsgi.ComposingRouter,
然后调用它。最后会调用Router._dispath方法,在_dispath中,从req.environ[‘wsgiorg.routing_args’][1] 取出match的值,然后取出controller,
app = match[‘controller’]. 这样代码就跑到controller中。

@staticmethod
    @webob.dec.wsgify(RequestClass=Request)
    def _dispatch(req):
        match = req.environ['wsgiorg.routing_args'][1]
        if not match:
            return render_exception(
                exception.NotFound(_('The resource could not be found.')),
                user_locale=req.best_match_language())
        app = match['controller']
        return app

到此为止, 代码可以跑到app中, 也就是Ec2Controller, 也可看到Ec2Controller是Application的一个子类, 在Application中,它实现了一个__call__ 方法,也就是说,它也是一个适用WSGI标准的应用程序。它的__call__ 方法定义如下:

def __call__(self, req):
        arg_dict = req.environ['wsgiorg.routing_args'][1]
        action = arg_dict.pop('action')
        del arg_dict['controller']
        LOG.debug(_('arg_dict: %s'), arg_dict)

        # allow middleware up the stack to provide context, params and headers.
        context = req.environ.get(CONTEXT_ENV, {})
        context['query_string'] = dict(req.params.iteritems())
        context['headers'] = dict(req.headers.iteritems())
        context['path'] = req.environ['PATH_INFO']
        params = req.environ.get(PARAMS_ENV, {})

        for name in ['REMOTE_USER', 'AUTH_TYPE']:
            try:
                context[name] = req.environ[name]
            except KeyError:
                try:
                    del context[name]
                except KeyError:
                    pass

        params.update(arg_dict)

        context.setdefault('is_admin', False)

        # TODO(termie): do some basic normalization on methods
        method = getattr(self, action)

        # NOTE(vish): make sure we have no unicode keys for py2.6.
        params = self._normalize_dict(params)

        try:
            result = method(context, **params)
        except exception.Unauthorized as e:
            LOG.warning(
                _('Authorization failed. %(exception)s from %(remote_addr)s') %
                {'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']})
            return render_exception(e, user_locale=req.best_match_language())
        except exception.Error as e:
            LOG.warning(e)
            return render_exception(e, user_locale=req.best_match_language())
        except TypeError as e:
            LOG.exception(e)
            return render_exception(exception.ValidationError(e),
                                    user_locale=req.best_match_language())
        except Exception as e:
            LOG.exception(e)
            return render_exception(exception.UnexpectedError(exception=e),
                                    user_locale=req.best_match_language())

        if result is None:
            return render_response(status=(204, 'No Content'))
        elif isinstance(result, basestring):
            return result
        elif isinstance(result, webob.Response):
            return result
        elif isinstance(result, webob.exc.WSGIHTTPException):
            return result

        response_code = self._get_response_code(req)
        return render_response(body=result, status=response_code)

可以看出, 它用action = arg_dict.pop('action'),从路由信息中,把action给取了出来,然后通过method = getattr(self, action)把对应的action作为一个可调用的对象取出来,并且调用它。
result = method(context, **params)

到此, 路由已经成功的跑进了我们想要的地方。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值