Keystone服务简析(上篇)

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

Keystone服务在OpenStack框架中负责身份认证,负责身份验证、服务规则和服务令牌的功能,它实现了OpenStack的Identity API。下面我们以挂载卷为例,简单的介绍下Keystone在整个流程起到的作用。

从上图中可以看到Keystone提供了两个主要的功能,一个是身份认证,一个是token验证,还有一个没有体现的功能是各个服务的endpoint的管理,如nova需要通过keystone来获取cinder服务正确的endpoint地址,才能发送相关的HTTP请求。

获取token

首先,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"
 }

相关阅读:

Keystone服务简析(下篇)

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