Openstack Keystone 认证流程(四)--Filter流水线

本文详细解析了OpenStack Keystone中的admin_api认证流水线,从access_log的日志记录,sizelimit的大小检查,url_normalize的路径规范化,到token_auth和admin_token_auth的令牌处理,xml_body和json_body的数据格式转换,再到ec2_extension、s3_extension的扩展路由和crud_extension的CRUD操作。最后,admin_service作为流水线终点,负责实际业务处理。
摘要由CSDN通过智能技术生成

1. admin_api 流水线

在上一章中, 我们讲了WEB服务器的创建及使用。这一章我们将进入Keystone的实际业务认证流程。

我们以admin_api做为一个实例,从Paste Deployment配置文件的流水线开始。

先来看看配置的流水线过程。

[pipeline:admin_api]
pipeline = access_log sizelimit url_normalize token_auth admin_token_auth xml_body json_body ec2_extension s3_extension crud_extension admin_service

可以看出, 这里有很多的流水线处理, 我们一个一个来简单过一下。

1.1 access_log

[filter:access_log]
paste.filter_factory = keystone.contrib.access:AccessLogMiddleware.factory

从这里可以看出, 这个点处理的是AccessLogMiddleware, 打开这个类, 所有的代码如下:

import webob.dec

from keystone.common import wsgi
from keystone import config
from keystone.openstack.common import log as logging
from keystone.openstack.common import timeutils


CONF = config.CONF
LOG = logging.getLogger('access')
APACHE_TIME_FORMAT = '%d/%b/%Y:%H:%M:%S'
APACHE_LOG_FORMAT = (
    '%(remote_addr)s - %(remote_user)s [%(datetime)s] "%(method)s %(url)s '
    '%(http_version)s" %(status)s %(content_length)s')


class AccessLogMiddleware(wsgi.Middleware):
    """Writes an access log to INFO."""

    @webob.dec.wsgify
    def __call__(self, request):
        data = {
            'remote_addr': request.remote_addr,
            'remote_user': request.remote_user or '-',
            'method': request.method,
            'url': request.url,
            'http_version': request.http_version,
            'status': 500,
            'content_length': '-'}

        try:
            response = request.get_response(self.application)
            data['status'] = response.status_int
            data['content_length'] = len(response.body) or '-'
        finally:
            # must be calculated *after* the application has been called
            now = timeutils.utcnow()

            # timeutils may not return UTC, so we can't hardcode +0000
            data['datetime'] = '%s %s' % (now.strftime(APACHE_TIME_FORMAT),
                                          now.strftime('%z') or '+0000')

            LOG.info(APACHE_LOG_FORMAT % data)
        return response

可以看出, 它里面就只是定义一个__call__ 的方法,然后写一条info log到日志文件中。但是这里, 却没有factory的方法。 我们继续打开它的父类。

class Middleware(Application):
    @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

    ...

在这个类中, 我们找到了factory这个类的静态方法,它返回一个用于创建本类的一个方法。到此为止, access_log就已经结束。它的作用就是写一条info log.

1.2 sizelimit

[filter:sizelimit]
paste.filter_factory = keystone.middleware:RequestBodySizeLimiter.factory
class RequestBodySizeLimiter(wsgi.Middleware):
    """Limit the size of an incoming request."""

    def __init__(self, *args, **kwargs):
        super(RequestBodySizeLimiter, self).__init__(*args, **kwargs)

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

        if req.content_length > CONF.max_request_body_size:
            raise exception.RequestTooLarge()
        if req.content_length is None and req.is_body_readable:
            limiter = utils.LimitingReader(req.body_file,
                                           CONF.max_request_body_size)
            req.body_file = limiter
        return self.application

这个看起来更简单, 只是检查下大小, 然后把内容读出来放到req.body_file中。 这样body_file就存放了HTTP请求的实际内容。

1.3 url_normalize

[filter:url_normalize]
paste.filter_factory = keystone.middleware:NormalizingFilter.factory
class NormalizingFilter(wsgi.Middleware):
    """Middleware filter to handle URL normalization."""

    def process_request(self, request):
        """Normalizes URLs."""
        # Removes a trailing slash from the given path, if any.
        if (len(request.environ['PATH_INFO']) > 1 and
                request.environ['PATH_INFO'][-1] == '/'):
            request.environ['PATH_INFO'] = request.environ['PATH_INFO'][:-1]
        # Rewrites path to root if no path is given.
        elif not request.environ['PATH_INFO']:
            request.environ['PATH_INFO'] = '/'

看到这里, 发现问题来了, 在这个类中, 我们没有定义__call__ 方法, 只是定义了一个process_request。 前面提到过, WSGI要求返回一个可调用的对象。这里NormalizingFilter明显不是一个可调用的对象。 这时我们又得返回到Middleware这个父类中。

@webob.dec.wsgify(RequestClass=Request)
    def __call__(self, request):
        try:
            response = self.process_request(request)
            if response:
                return response
            response = request.get_response(self.application)
            return self.process_response(request, response)
        except exception.Error as e:
            LOG.warning(e)
            return render_exception(e,
                                    user_locale=request.best_match_language())
        except TypeError as e:
            LOG.exception(e)
            return render_exception(exception.ValidationError(e),
                                    user_locale=request.best_match_language())
        except Exception as e:
            LOG.exception(e)
            return render_exception(exception.UnexpectedError(exception=e),
                                    user_locale=request.best_match_language())

很好,在这里我们找到了__call__ 方法, 这样NormalizingFilter本质上还是一个可调用的对象。然后我们再详细看看Middleware的__call__ 方法, 可以看出, 它先调用本类的process_request方法。如果这个方法有返回值。那么就作为一个Response对象返回给Client. 否则的话继续处理process_response。

到此, NormalizingFilter的process_request方法被调用, 然后把HTTP请求中的PATH_INFO给规范化,即转化为/aaa/bbb的形式, 以/打头, 把路径的最后一个/给去掉。NormalizingFilter的处理全部结束。

但是, 细心的读者应该发现, 在__call__ 方法中, 还调用了process_response方法。继续回到代码:

def process_request(self, request):
       return None

def process_response(self, request, response):
    return response

可以发现, 在Middleware中, 定义了两个默认的方法, 什么事情也不做, 但也不会破坏流水线的结构。

到此为此, 整个Middleware的方法也都全被呈现了出来。

1.4 token_auth

[filter:token_auth]
paste.filter_factory = keystone.middleware:TokenAuthMiddleware.factory
class TokenAuthMiddleware(wsgi.Middleware):
    def process_request(self, request):
        token = request.headers.get(AUTH_TOKEN_HEADER)
        context = request.environ.get(CONTEXT_ENV, {})
        context['token_id'] = token
        if SUBJECT_TOKEN_HEADER in request.headers:
            context['subject_token_id'] = (
                request.headers.get(SUBJECT_TOKEN_HEADER))
        request.environ[CONTEXT_ENV] = context

从这里可以看出, 它也只是把token_id从HTTP的请求中取出来,然后存入request.environ[CONTEXT_ENV]全局变量中, 以备后用。

1.5 admin_token_auth

[filter:admin_token_auth]
paste.filter_factory = keystone.middleware:AdminTokenAuthMiddleware.factory
class AdminTokenAuthMiddleware(wsgi.Middleware):
    def process_request(self, request):
        token = request.headers.get(AUTH_TOKEN_HEADER)
        context = request.environ.get(CONTEXT_ENV, {})
        context['is_admin'] = (token == CONF.admin_token)
        request.environ[CONTEXT_ENV] = context

这是个和之前类似的中间件,取出HTTP请求中的token,然后存入request.environ[CONTEXT_ENV]全局变量中。

1.6 xml_body

[filter:xml_body]
paste.filter_factory = keystone.middleware:XmlBodyMiddleware.factory
class XmlBodyMiddleware(wsgi.Middleware):
    def process_request(self, request):
        """Transform the request from XML to JSON."""
        incoming_xml = 'application/xml' in str(request.content_type)
        if incoming_xml and request.body:
            request.content_type = 'application/json'
            try:
                request.body = jsonutils.dumps(
                    serializer.from_xml(request.body))
            except Exception:
                LOG.exception('Serializer failed')
                e = exception.ValidationError(attribute='valid XML',
                                              target='request body')
                return wsgi.render_exception(e)

从代码中可以看出, 这个中间件的作用就是把XML请求转换为JSON数据类型。然后内部统一使用JSON的格式进行处理。

1.7 json_body

[filter:json_body]
paste.filter_factory = keystone.middleware:JsonBodyMiddleware.factory
class JsonBodyMiddleware(wsgi.Middleware):
    def process_request(self, request):
        # Abort early if we don't have any work to do
        params_json = request.body
        if not params_json:
            return

        # Reject unrecognized content types. Empty string indicates
        # the client did not explicitly set the header
        if request.content_type not in ('application/json', ''):
            e = exception.ValidationError(attribute='application/json',
                                          target='Content-Type header')
            return wsgi.render_exception(e)

        params_parsed = {}
        try:
            params_parsed = jsonutils.loads(params_json)
        except ValueError:
            e = exception.ValidationError(attribute='valid JSON',
                                          target='request body')
            return wsgi.render_exception(e)
        finally:
            if not params_parsed:
                params_parsed = {}

        params = {}
        for k, v in params_parsed.iteritems():
            if k in ('self', 'context'):
                continue
            if k.startswith('_'):
                continue
            params[k] = v

        request.environ[PARAMS_ENV] = params

在这个中间件中, 先是对文档类型进行了判断,如果不是JSON的格式, 就抛出ValidationError的Exception。 联合之前的xml处理,可以得出, 如果不是XML或是JSON的格式。那么Keystone就不会进行处理, 换句话说, 它只处理XML和JSON的类型。

把JSON类型转换出来后,就把结果保存在request.environ[PARAMS_ENV]中, 后面的应该就会更方便的使用。

1.8 ec2_extension

[filter:ec2_extension]
paste.filter_factory = keystone.contrib.ec2:Ec2Extension.factory
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']))

        # crud
        mapper.connect(
            '/users/{user_id}/credentials/OS-EC2',
            controller=ec2_controller,
            action='create_credential',
            conditions=dict(method=['POST']))
        mapper.connect(
            '/users/{user_id}/credentials/OS-EC2',
            controller=ec2_controller,
            action='get_credentials',
            conditions=dict(method=['GET']))
        mapper.connect(
            '/users/{user_id}/credentials/OS-EC2/{credential_id}',
            controller=ec2_controller,
            action='get_credential',
            conditions=dict(method=['GET']))
        mapper.connect(
            '/users/{user_id}/credentials/OS-EC2/{credential_id}',
            controller=ec2_controller,
            action='delete_credential',
            conditions=dict(method=['DELETE']))

在这个中间件中, 可以看出它并没有定义process_request和process_response方法。 但是细心的你也应该发现这个中间件的父类也已经不是Middleware了, 而是ExtensionRouter, 这里我们先不管这个类的具体实现。从名字大概可以猜出这个中间件的作用就是Ec2接口增加路由,然后使用ec2_controller来处理EC2风格的认证请求。对于Router,我们将在下一章中继续解读。

1.9 s3_extension

[filter:s3_extension]
paste.filter_factory = keystone.contrib.s3:S3Extension.factory
class S3Extension(wsgi.ExtensionRouter):
    def add_routes(self, mapper):
        controller = S3Controller()
        # validation
        mapper.connect('/s3tokens',
                       controller=controller,
                       action='authenticate',
                       conditions=dict(method=['POST']))

这个中件间的作用和之前的EC2类似,只是增加一条路由

1.10 crud_extension

[filter:crud_extension]
paste.filter_factory = keystone.contrib.admin_crud:CrudExtension.factory
class CrudExtension(wsgi.ExtensionRouter):
    """Previously known as the OS-KSADM extension.

    Provides a bunch of CRUD operations for internal data types.

    """

    def add_routes(self, mapper):
        tenant_controller = identity.controllers.Tenant()
        user_controller = identity.controllers.User()
        role_controller = identity.controllers.Role()
        service_controller = catalog.controllers.Service()
        endpoint_controller = catalog.controllers.Endpoint()

        # Tenant Operations
        mapper.connect(
            '/tenants',
            controller=tenant_controller,
            action='create_project',
            conditions=dict(method=['POST']))
        mapper.connect(
            '/tenants/{tenant_id}',
            controller=tenant_controller,
            action='update_project',
            conditions=dict(method=['PUT', 'POST']))
        mapper.connect(
            '/tenants/{tenant_id}',
            controller=tenant_controller,
            action='delete_project',
            conditions=dict(method=['DELETE']))
        mapper.connect(
            '/tenants/{tenant_id}/users',
            controller=tenant_controller,
            action='get_project_users',
            conditions=dict(method=['GET']))

 ...

这个类的作用很明显, 就是给内部对象增加RESTful 风格的CRUD操作路由。 其中有五个控制器(tenant_controller, user_controller, role_controller, service_controller, endpoint_controller)。

1.11 admin_service

[app:admin_service]
paste.app_factory = keystone.service:admin_app_factory
@fail_gracefully
def admin_app_factory(global_conf, **local_conf):
    conf = global_conf.copy()
    conf.update(local_conf)
    return wsgi.ComposingRouter(routes.Mapper(),
                                [identity.routers.Admin(),
                                    token.routers.Router(),
                                    routers.VersionV2('admin'),
                                    routers.Extension()])

这个factory产生了一个ComposingRouter的对象,这是流水线的最后一级,也是业务的实际处理点。具休的实现我们以后再说。

下一章, 我们将解读Router的具体实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值