Keystone服务简析(下篇)

达芬奇密码2018-07-25 10:35


token认证

用户拿到token之后,在访问OpenStack API时在请求头X-Auth-Token字段中带上token,以挂载卷请求为例,nova在收到请求后,会提取请求头中token,并调用keystoneclient来对token认证,如果认证成功,会将认证后的信息保存在context中,在接下来的一些权限、策略检查都可以通过context携带的信息来实现。

curl -i 'http://pubbetaapi.beta.server.163.org:8774/v2/9bd69066f3bd4c9e8c56d4ca7e314330/servers/c980c207-6597-4a82-b58b-bb188c3f31b5/os-volume_attachments' -X POST -H "X-Auth-Project-Id: Project_qa_admin" -H "User-Agent: python-novaclient" -H "Content-Type: application/json" -H "Accept: application/json" -H "X-Auth-Token: 1bc6547317b34bd689db88bf78d8967f" -d '{"volumeAttachment": {"device": null, "volumeId": "f878801c-88ab-47ec-abb2-3197f4cb2f49"}}'

通过/etc/nova/api-paste.ini配置文件可以知道,对token认证的方法为keystoneclient.middleware.auth_token

[filter:authtoken]
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
# keystoneclient.middleware.auth_token
def app_factory(global_conf, **local_conf):
    conf = global_conf.copy()
    conf.update(local_conf)
    return AuthProtocol(None, conf)

具体来看下类AuthProtocol的__call__方法。

class AuthProtocol(object):
    def __call__(self, env, start_response):
        ···
        try:
            # 去掉请求头中的X-User-Id、X-User-Name等信息,防止fake
            self._remove_auth_headers(env)
            # 提取请求头X-Auth-Token中的token
            user_token = self._get_user_token_from_header(env)
            # 对token进行验证
            token_info = self._validate_user_token(user_token, env)
            env['keystone.token_info'] = token_info
            # 将通过验证后的user_id、roles、project_id等信息添加到请求头中
            user_headers = self._build_user_headers(token_info)
            self._add_headers(env, user_headers)
            # 交给下一个app继续处理
            return self.app(env, start_response)

        except # 异常处理
            ···

首先是从请求头中去掉X-User-Id在内的一些信息,以免用户信息被fake,然后从请求头中拿到token,并调用_validate_user_token进行认证,认证通过后会重新生成一个包含X-User-Id在内的请求头,后面会由nova.api.auth:NovaKeystoneContext提取这些信息组装成一个RequestContext类型的context。主要来看下_validate_user_token方法。

class AuthProtocol(object):
    def _validate_user_token(self, user_token, env, retry=True):
        token_id = None

        try:
            token_id = cms.cms_hash_token(user_token)
            # 先尝试从memcached中获取token
            cached = self._cache_get(token_id)
            if cached:
                return cached
            if cms.is_ans1_token(user_token):
                verified = self.verify_signed_token(user_token)
                data = jsonutils.loads(verified)
            else:
                # 没有从memcached中拿到,则通过查询数据库认证
                data = self.verify_uuid_token(user_token, retry)
            # 确认是否过期
            expires = confirm_token_not_expired(data)
            self._confirm_token_bind(data, env)
            # 将token存入memcached中
            self._cache_put(token_id, data, expires)
            return data
        ···
        # 异常处理

首先尝试从memcached中获取tokne信息,如果缓存里没有则通过给Keystone来认证,认证通过后把token以及相关的信息放入缓存中,以便做到下次快速的token验证。其中verify_uuid_token给Keystone发送的GET请求URL为'/v2.0/tokens/{token_id}',接着来看下Keystone这边的处理流程。

# keystone.token.controller.py
class Auth(controller.V2Controller):
    @controller.v2_deprecated
    @controller.protected()
    def validate_token(self, context, token_id):
        belongs_to = context['query_string'].get('belongsTo')
        # TODO(ayoung) validate against revocation API
        
        return self.token_provider_api.validate_v2_token(token_id, belongs_to)

根据前面的分析可以知道validate_v2_token方法在keystone.token.provider:Manger类中实现。

# keystone.token.provider:Manager
@dependency.provider('token_provider_api')
@dependency.requires('assignment_api', 'revoke_api')
class Manager(manager.Manager):
    def validate_v2_token(self, token_id, belongs_to=None):
        if self._needs_persistence:
            # 根据uuid从数据库中取出token数据
            unique_id = utils.generate_unique_id(token_id)
            token_ref = self._persistence.get_token(unique_id)
            # 调用具体driver的validate_v2_token方法,对domain、version做检查
            token = self._validate_v2_token(token_ref)
        else:
            v3_token_ref = self.validate_non_persistent_token(token_id)
            v2_token_data_helper = providers.common.V2TokenDataHelper()
            token = v2_token_data_helper.v3_to_v2_token(v3_token_ref)

        # these are common things that happen regardless of token provider
        token['access']['token']['id'] = token_id
        self._token_belongs_to(token, belongs_to)
        # 检查token格式是否合法,以及有没有过期
        self._is_valid_token(token)
        # 返回token_data
        return token

经过认证后,返回的token信息示例如下:

{'access':
    {'metadata': 
        {'is_admin': 0,
         'roles': ['3bd57ec2cb414d9887eafb6a0c9b4d7b']},
         'serviceCatalog': [
            {'endpoints': [
                {'adminURL': 'http://127.0.0.1:8776/v2/66835960ec3241aca6421b62ae0b7e1d',
                 'id': '426d1cd5e1af47bd950d51ae841d3068',
                 'internalURL': 'http://127.0.0.1:8776/v2/66835960ec3241aca6421b62ae0b7e1d',
                 'publicURL': 'http://127.0.0.1:8776/v2/66835960ec3241aca6421b62ae0b7e1d',
                 'region': 'RegionOne'}],
                 'endpoints_links': [],
                 'name': 'cinder',
                 'type': 'volumev2'},
                 ···
         ],
        'token': {'expires': '2017-04-23T08:13:26Z',
                  'id': '17174ebc963c4ce294775f97e2802ed8',
                  'issued_at': '2017-04-22T08:13:26.486627',
                  'tenant': {'description': None,
                             'enabled': True,
                             'id': '66835960ec3241aca6421b62ae0b7e1d',
                             'name': 'admin'}},
        'user': {'id': '6f0cdd4e82f4435ca41c7314b152ad22',
                 'name': 'admin',
                 'roles': [{'name': 'admin'}],
                 'roles_links': [],
                 'username': 'admin'}}}

相关阅读:

Keystone服务简析(上篇)

本文来自网易实践者社区,经作者廖跃华授权发布。