用户拿到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'}}}