Keystone服务在OpenStack框架中负责身份认证,负责身份验证、服务规则和服务令牌的功能,它实现了OpenStack的Identity API。下面我们以挂载卷为例,简单的介绍下Keystone在整个流程起到的作用。
从上图中可以看到Keystone提供了两个主要的功能,一个是身份认证,一个是token验证,还有一个没有体现的功能是各个服务的endpoint的管理,如nova需要通过keystone来获取cinder服务正确的endpoint地址,才能发送相关的HTTP请求。
首先,token是用户的一种凭证,用户需要向Keystone提供正确的用户名/密码才能获取。采用tokne作为凭证的原因之一是,如果每次请求都采用用户名/密码认证,容易造成信息的泄漏。所以用户在访问OpenStack各个API之前,必须先获取其token,然后用token作为用户凭据访问 OpenStack API。 下面来看一下Keystone是如何对用户名/密码进行验证并返回token的。
获取token的示例请求:
curl -i -X POST http://pubbetaapi.beta.server.163.org:5000/v2.0/tokens -H "Content-Type: application/json" -H "User-Agent: python-keystoneclient" -d '{"auth": {"tenantName": "admin", "passwordCredentials": {"username": "admin", "password": "fake-passwd"}}}'
请求响应处理:
# keystone.token.Controller.py
@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
'resource_api', 'role_api', 'token_provider_api',
'trust_api')
class Auth(controller.V2Controller):
@controller.v2_auth_deprecated
def authenticate(self, context, auth=None):
# 对请求body格式进行检查,必须包含auth属性
if auth is None:
raise exception.ValidationError(attribute='auth',
target='request body')
if "token" in auth:
# 如果提供了token,则尝试根据token进行验证
auth_info = self._authenticate_token(
context, auth)
else:
# 尝试外部认证
try:
auth_info = self._authenticate_external(
context, auth)
except ExternalAuthNotApplicable:
# 本地用户名密码认证
auth_info = self._authenticate_local(
context, auth)
# 通过验证后,返回的信息包括用户信息、租户信息、元数据、过期时间等
user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id = auth_info
# 检查用户是否被disable
try:
self.identity_api.assert_user_enabled(
user_id=user_ref['id'], user=user_ref)
if tenant_ref:
self.resource_api.assert_project_enabled(
project_id=tenant_ref['id'], project=tenant_ref)
except AssertionError as e:
six.reraise(exception.Unauthorized, exception.Unauthorized(e),
sys.exc_info()[2])
···
auth_token_data = self._get_auth_token_data(user_ref,
tenant_ref,
metadata_ref,
expiry,
audit_id)
···
# 生成token信息
(token_id, token_data) = self.token_provider_api.issue_v2_token(
auth_token_data, roles_ref=roles_ref, catalog_ref=catalog_ref)
# NOTE(wanghong): We consume a trust use only when we are using trusts
# and have successfully issued a token.
if CONF.trust.enabled and 'trust_id' in auth:
self.trust_api.consume_use(auth['trust_id'])
return token_data
默认情况下Keystone支持以下四种认证方式,在我们的项目中,实际使用的就是password方法,即根据用户名(用户id)/密码的方式进行认证,这里重点看下_authenticate_local和issue_v2_token方法。
methods = external,password,token,oauth1
@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
'resource_api', 'role_api', 'token_provider_api',
'trust_api')
class Auth(controller.V2Controller):
def _authenticate_local(self, context, auth):
# 对请求body格式的一些检查
if 'passwordCredentials' not in auth:
raise exception.ValidationError(
attribute='passwordCredentials', target='auth')
if "password" not in auth['passwordCredentials']:
raise exception.ValidationError(
attribute='password', target='passwordCredentials')
password = auth['passwordCredentials']['password']
if password and len(password) > CONF.identity.max_password_length:
raise exception.ValidationSizeError(
attribute='password', size=CONF.identity.max_password_length)
# userId和username两者至少提供一个
if (not auth['passwordCredentials'].get("userId") and
not auth['passwordCredentials'].get("username")):
raise exception.ValidationError(
attribute='username or userId',
target='passwordCredentials')
user_id = auth['passwordCredentials'].get('userId')
if user_id and len(user_id) > CONF.max_param_size:
raise exception.ValidationSizeError(attribute='userId',
size=CONF.max_param_size)
username = auth['passwordCredentials'].get('username', '')
if username:
if len(username) > CONF.max_param_size:
raise exception.ValidationSizeError(attribute='username',
size=CONF.max_param_size)
try:
# 因为用户名在数据库并不是主键,因此还需要传入
# domain_id来唯一确定一个user
user_ref = self.identity_api.get_user_by_name(
username, CONF.identity.default_domain_id)
user_id = user_ref['id']
except exception.UserNotFound as e:
raise exception.Unauthorized(e)
try:
# self.identity_api由装饰器@dependency.requires注入,
# 具体driver则由配置项[identity] driver = 确定,我们采用是
# [identity] = keystone.identity.backends.sql.Identity
user_ref = self.identity_api.authenticate(
context,
user_id=user_id,
password=password)
except AssertionError as e:
raise exception.Unauthorized(e.args[0])
metadata_ref = {}
tenant_id = self._get_project_id_from_auth(auth)
tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref(
user_id, tenant_id)
expiry = provider.default_expire_time()
bind = None
audit_id = None
return (user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id)
_authenticate_local方法主要的功能是根据用户id和密码对用户进行认证,如果成功则返回认证后的信息(用户信息、租户信息、元数据、过期时间等)。首先是对请求body进行检查,必须提供password字段,必须username和user_id其中的一个。如果用户传入的是username则需要加上domain_id来唯一确定一个user,因为username在数据库中不是主键。self.identity_api.authenticate则是根据实际配置的drvier来验证用户id和密码,其中self.identity_api赋值由装饰器@dependency.requires负责,其值是装饰器@dependency.provider('identity_api')装饰的Manager类(keystone.identity.core.py:Manager)。authenticate这个方法根据配置项driver来指定,我们配置的是keystone.identity.backends.sql.Identity。来看下具体的实现。
# keystone.identity.backends.sql:Identity.authenticate
@dependency.requires('assignment_api')
class Identity(sql.Base, identity.Driver):
# Identity interface
def authenticate(self, user_id, password):
with sql.session_for_read() as session:
user_ref = None
try:
# 取出user相关的数据
user_ref = self._get_user(session, user_id)
except exception.UserNotFound:
raise AssertionError(_('Invalid user / password'))
# 检查密码
if not self._check_password(password, user_ref):
raise AssertionError(_('Invalid user / password'))
return identity.filter_user(user_ref.to_dict())
authenticate实现很简单,就是根据user_id从数据库冲取出用户数据,然后做密码检查,密码做SHA hash,如果成功则返回用户信息。验证完成后,就调用issue_v2_token来生成一个token。
# keystone.token.provider:Manager
@dependency.provider('token_provider_api')
@dependency.requires('assignment_api', 'revoke_api')
class Manager(manager.Manager):
def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None):
# 调用具体driver的issue_v2_token后端来获取token_id和token_data
token_id, token_data = self.driver.issue_v2_token(
token_ref, roles_ref, catalog_ref)
# 是否需要持久化(写入到后端存储中)
if self._needs_persistence:
data = dict(key=token_id,
id=token_id,
expires=token_data['access']['token']['expires'],
user=token_ref['user'],
tenant=token_ref['tenant'],
metadata=token_ref['metadata'],
token_data=token_data,
bind=token_ref.get('bind'),
trust_id=token_ref['metadata'].get('trust_id'),
token_version=self.V2)
self._create_token(token_id, data)
return token_id, token_data
还是调用具体driver的issue_v2_token来获取token_id和token_data,token有多种形式可以选择,由配置项[token] provider设置,我们项目中使用的是uuid,因此self.driver在这里被初始化为keystone.token.providers.uuid:provider,issue_v2_token方法在其父类keystone.token.providers.common:BaseProvider实现。
@dependency.requires('catalog_api', 'identity_api', 'oauth_api',
'resource_api', 'role_api', 'trust_api')
class BaseProvider(provider.Provider):
def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None):
if token_ref.get('bind') and not self._supports_bind_authentication:
msg = _('The configured token provider does not support bind '
'authentication.')
raise exception.NotImplemented(message=msg)
metadata_ref = token_ref['metadata']
trust_ref = None
if CONF.trust.enabled and metadata_ref and 'trust_id' in metadata_ref:
trust_ref = self.trust_api.get_trust(metadata_ref['trust_id'])
token_data = self.v2_token_data_helper.format_token(
token_ref, roles_ref, catalog_ref, trust_ref)
token_id = self._get_token_id(token_data)
token_data['access']['token']['id'] = token_id
return token_id, token_data
核心方法就是调用_get_token_id生成token_id,而uuid形式的tokne实际上就是通过uuid.uuid4().hex来生成一个uuid作为token。
# keystone.token.providers.uuid:provider
class Provider(common.BaseProvider):
def __init__(self, *args, **kwargs):
super(Provider, self).__init__(*args, **kwargs)
def _get_token_id(self, token_data):
return uuid.uuid4().hex
uuid形式的token,最终还需要调用_create_token将token信息(id、过期时间、user_id等)写入到后端存储当中(sql,memcached,redis..),我们用的是sql,实际调用的是方法是。
# keystone.token.persistence.backends.sql
class Token(token.persistence.TokenDriverV8):
def create_token(self, token_id, data):
data_copy = copy.deepcopy(data)
if not data_copy.get('expires'):
data_copy['expires'] = provider.default_expire_time()
if not data_copy.get('user_id'):
data_copy['user_id'] = data_copy['user']['id']
token_ref = TokenModel.from_dict(data_copy)
token_ref.valid = True
with sql.session_for_write() as session:
session.add(token_ref)
return token_ref.to_dict()
总结一下当前我们的keystone配置,存储后端用的是MySQL,认证形式是token,token的形式uuid,获取token的过程实际上是对用户提供的用户id/密码做认证,认证通过后生成一个uuid形式的token,同时将token信息保存到数据库中,最后将上面这些信息返回给用户,其中token部分的响应示例如下。
"token": {
"expires": "2017-04-21T08:29:45Z",
"id": "6ba0a649540b461ebbf824c8411a892b",
"issued_at": "2017-04-20T08:29:45.732712",
"tenant": {
"description": null,
"enabled": true,
"id": "66835960ec3241aca6421b62ae0b7e1d",
"name": "admin"
}
相关阅读:
本文来自网易实践者社区,经作者廖跃华授权发布。