LOADING

Follow me

Keystone 用户认证
十一月 1, 2015|KeystoneOpenstack

Keystone 用户认证

Keystone 用户认证

Keystone 用户认证

1、keystone简介

keystone的认证,因为版本原因,同样也分为version 2与version 3,并且version2与3分属不同模块(下述所指模块均基于keystone),version2的代码集中于keystone.token模块,version3的代码集中于keystone.auth模块。

keystone各部件的代码组成主要包括routers、controllers、core、backends(包括sql、kvs等),其中routers主要负责url与action映射,core负责manager api的操作,通俗来讲就是为controller提供api接口,为backends各种后端驱动封装接口,是一种适配器的功能,backends主要提供各种后端存储等操作。

认证的路径URL分别如下:

version2:

/tokens对应操作:authenticate

/tokens/revoked对应操作:revocation_list

/tokens/{token_id} 对应validation_token,validate_token_head,delete_token

/tokens/{token_id}/endpoints 对应操作:token_controller

/certificates/ca对应操作ca_cert

/certificates/signing对应操作signing_cert

version3:

/auth/tokens

对应操作:validate_token,checl_token,authenticate_for_token,revoke_token

/auth/tokens/OS-PKI/revoked

对应操作:revocation_list

/auth/catalog

对应操作:get_auth_catalog

/auth/domains

对应操作:get_auth_domains

上述URL定义以及对应操作的映射分别在keystone.auth.routers和Keystone.token.routers,所映射的操作分别在keystone.auth.controllers和keystone.token.controllers.

其中,auth部件,主要负责各种认证,token部件负责是token的认证,持久化等操作,所以才有了auth与token功能交叉的部分,也就是V2与V3在token认证上的差异.

2、keystone认证

keystone默认配置的认证方法主要有external、password、token、oauth1.

而auth.plugins中目前支持的增加了saml2,也就是liberty版本中的WEBSSO.

下面我们讨论的主要包括password、token、以及external,在对单个认证方式分析时,会同时分析v2与v3的差异。

V3中涉及的概念:project,domain,group,user,trust等,其中domain管理project,group,group管理user,因此,在检测group是否enabled的时候,需要首先检测其所归属集合是否enabled,这个编程涉及思路贯彻始终。

接下来,我们首先介绍version 3的认证流程。

2.1 keystone v3认证

处理函数:authenticate_for_token

def anthenticate_for_token(self, context, auth=None):

# context就是上下环境,auth是输入的字典

# auth的类型有如下几种,这些都是来至官网,对照代码理解无误,请放心!

第一种是username 配合password,因为username不是主键,所以要搭domain,domain可以id或者name方式

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "domain": {
                        "name": "example.com"
                    },
                    "name": "Joe",
                    "password": "secretsecret"
                }
            }
        }
    }
}

第二种是username 配合password domain id

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "domain": {
                        "id": "1789d1"
                    },
                    "name": "Joe",
                    "password": "secretsecret"
                }
            }
        }
    }
}

第三种是userid配合password

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": "0ca8f6",
                    "password": "secretsecret"
                }
            }
        }
    }
}

第四种是token id

{
    "auth": {
        "identity": {
            "methods": [
                "token"
            ],
            "token": {
                "id": "e80b74"
            }
        }
    }
}

第五种是获取scoped token 是scoped project

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": "0ca8f6",
                    "password": "secretsecret"
                }
            }
        },
        "scope": {
            "project": {
                "id": "263fd9"
            }
        }
    }
}

第六种 获取scoped token 是 scoped domain

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": "0ca8f6",
                    "password": "secretsecret"
                }
            }
        },
        "scope": {
            "domain": {
                "id": "263fd9"
            }
        }
    }
}

第七种是获取scoped token是提供project name,因为是name,所以需要提供domain,还是因为不是主键,不好辨别

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": "0ca8f6",
                    "password": "secretsecret"
                }
            }
        },
        "scope": {
            "project": {
                "domain": {
                    "id": "1789d1"
                },
                "name": "project-x"
            }
        }
    }
}

第八种是scoped token,提供project name以及domain name

{
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": "0ca8f6",
                    "password": "secretsecret"
                }
            }
        },
        "scope": {
            "project": {
                "domain": {
                    "name": "example.com"
                },
                "name": "project-x"
            }
        }
    }
}

 

下面我们列出代码,一一道来。我的讲解是一代码加注释的形式进行的,谢谢!

因为认证方法都是通过plug-ins这种形式就是适配,那么是怎么适配的呢?

我们通过前期stevedore.DriverManager加载指定模块,这里的模块来至于/etc/keystone.conf

[auth]选项下methods = external,password,token,oauth1,加载好之后,你在输入auth字典时候,就要自己选哪种认证方式了,后面会讲到,首先先看password,token实现的认证方法,之后看controller函数如何去调用这两种方法.

password认证方法

def authenticate(self, context, auth_payload, auth_context):
    user_info = auth_plugins.UserAuthInfo.create(auth_payload, METHOD_NAME)
    try:
        self.identity_api.authenticate(context, user_id=user_info.user_id, password=user_info.password)
    except AssertionError:
        msg = _("Invalid username or password")
        raise exception.Unauthorized(msg)
    auth_context['user_id'] = user_info.user_id

首先调用UserInfo类,这个类它是plug-in的core,其create(auth_payload, METHOD_NAME)主要实现的功能就是先初始化UserInfo实例,然后调用本类的_validate_and_normalize_auth_data(auth_payload)方法,那么这个方法是干嘛的呢?先 查看user字典在不在,不在就报错,然后获取user字典,user_id,user_name,上面那么多auth格式已经讲到了有user_name必须有domain,domain不管是domain name还是domain id,如果username不为空,那么就要检测domain看它的enabled字段是否为True,不是就报错,如果user_id不为空,那么获取user_ref,再获取domain_ref,检测domain是否enabled,最终检测user是否enabled,然后将user_ref,user_id,domain_id赋值,这样这个方法就调用完毕,最后最后将’password’赋值给AuthInfo实例的METHOD_NAME属性,这样create静态方法调用完毕.

接下来就是调用identifu_api的authenticate方法,填入参数,关于identity_api的分析,放在后面,如果认证通过,那么最后将user_id填充进auth_context[‘user_id’]字段.

password讲解完毕,接下来就是token的方法.

token的认证方法

def authenticate(self, context, auth_payload, user_context):
    if 'id' not in auth_payload:
        raise exception.ValidationError(attibute='id', target='token')
    if token_ref.is_federated_user and self.federation_api:
        mapped.handle_scoped_token(context, auth_payload, user_context, token_ref, self.federation_api, self.identity_api, self.token_provider_api)
    else:
        token_authenticate(context, auth_payload, user_context, token_ref)

token插件实现Token类,首先,校验auth_payload是否存在id,没有就报错,然后调用本类_get_token_ref(auth_payload)方法,这方法做了什么呢?它将auth_payload[‘id’]赋值给token_id,然后调用token_provider_api的validate_token方法,返回token_data,最后返回的是KeystoneToken实例,这个实例继承dict,那么这个KeystoneToken是用来干嘛的?KeystoneToken是V2 token与V3 token都需要使用的中间类,内存里就是存储并使用的它.

token_provider_api.validate_token(self, token_id, belong_to=None)方法完成的功能:首先,调用utils.generate_unique_id(token_id),对token_id进行处理,如果是asn1或者pkiz token,那么就要hash,否则直接返回即可,然后调用_validate_token(unique_id),这个方法会先判断是否需要持久化,如果不要持久化就直接返回self.driver.validate_v3_token(token_id),也就是说直接调用后端的validate_v3_token(token_id)函数,这个函数的功能等下介绍.如果需要持久化的话,那么就调用self._persistence,get_token(token_id)并将返回结果传入self.driver.get_token_version(token_ref)判断token的版本,如果是v2,那么就调用self.driver.validate_v3_token(token_ref)否则调用self.driver.validate_v2_token(token_ref),最后返回token_ref.

因为没有设federated,所以,对后面的判断直接略过,最后调用token_authenticate,这个函数的作用是:先判断是否为oauth_scoped或者trusted_scoped,如果是就报错,不允许它们转变token,如果不是,就继续检测配置项是否支持scoped token转换,如果不允许,就报错,如果允许,就调用wsgi.validate_token_bind,检测token bind类型,默认为permissive,所以,可以放心通过,(这个地方一直不懂audit_ids是用来干嘛的,现在略有点明白了),然后从调用

token_ref.get(‘audit_ids’, [])[-1]获取最后一列,然后就是设置user_context的各种参数.如下所示:

user_context.setdefault('expires_at', token_ref.expires)
user_context['audit_id'] = token_audit_id
user_context.setdefault('user_id', token_ref.user_id)
user_context['extras'].update(token_ref.get('extras', {}))
user_context['method_names'].extend(token_ref.methods)

到目前为止,我们把password与token简单介绍完毕,我们下面看controller类中的方法如果去完成认证过程,代码如下:

try:
auth_info = AuthInfo.create(context, auth=auth)

# 调用AuthInfo的create方法,先初始化赋值

# self.context = context

# self.auth = auth

# self._scope_data = (None, None, None, None)

# 其中self._scope_data = (domain_id, project_id, trust_ref, unscoped)

# 是用于标示auth到底是scoped还是unscoped,又是哪种scoped

# 调用_validate_and_normalize_auth(scoped_only)方法

# 其用于检测auth是否合规,如果scoped为false,那么就要获取self.auth[‘identity’]

# 查看认证方式是否在默认支持的选项中

# 其后就是检测是否为scoped

# self._scoped_data赋予新检测值
auth_context = AuthContext(extras={},
method_names=[],
bind={})

# AuthContext是继承dict的类,用于重构context,防止与认证属性冲突

# 它重构了__setitem__方法,对属性进行检测,如果是IDENTITY_ATTRIBUTES属性

# 且不与原来值相等,那就赋值不成功,且报错,但是‘expires_at’是可以以最早的时间作为替换.
self.authenticate(context, auth_info, auth_context)

# 认证在代码后,单独开辟出来讲解
if auth_context.get('access_token_id'):
auth_info.set_scope(None, auth_context['project_id'], None)

# 根据分析,auth_context是没有access_token_id的,所以直接进入下面的方法
self._check_and_set_default_scoping(auth_info, auth_context)

# 这个方法的目的是检测是否为scoped还是已知为unscoped

# 如果都不是,我们就需要设置为scoped project,当然user必须要默认project
(domain_id, project_id, trust, unscoped) = auth_info.get_scope()
# 这里就是获取scoped tuple
method_names = auth_info.get_method_names()
method_names += auth_context.get('method_names', [])
method_names = list(set(method_names))
expires_at = auth_context.get('expires_at')

metadata_ref = None

token_audit_id = auth_context.get('audit_id')

# 这里就是调用token_provider_api处理成为v3 token

# 在此过程中如果需要持久化,还要调用方法创建token并存储
(token_id, token_data) = self.token_provider_api.issue_v3_token(
auth_context['user_id'], method_names, expires_at, project_id,
domain_id, auth_context, trust, metadata_ref, include_catalog,
parent_audit_id=token_audit_id)

if trust:
self.trust_api.consume_use(trust['id'])
# 这里就是返回头’X--Subject-Token’:token_id了
return render_token_data_response(token_id, token_data,
created=True)
except exception.TrustNotFound as e:
raise exception.Unauthorized(e)``

其中,我们对上述调用的authenticate进行分析,代码如下:

def authenticate(self, context, auth_info, auth_context):

# 如果context环境有remote_user可能就使用的external认证方法

# 这个以后讨论,我们主要关心password与token认证
if context['environment'].get('REMOTE_USER'):
try:
external = get_auth_method('external')
external.authenticate(context, auth_info, auth_context)
except exception.AuthMethodNotSupported:

LOG.debug("No 'external' plugin is registered.")
except exception.Unauthorized:
LOG.debug("Authorization failed for 'external' auth method.")
# 设一个 认证方法的字典
auth_response = {'methods': []}
for method_name in auth_info.get_method_names():

# auth_info是AuthInfo实例,调用get_method_names

# 即获取self.auth[‘identity’][‘methods’]的值
method = get_auth_method(method_name)

# 查看代码你会发现,这是获取对应方法加载的实例对象

# 加载方式是namespace,通过stevedore.DriverManager进行加载

# 有人会问了我没看到调用controllers.load_auth_methods啊?

# 因为上面方法的调用是在keystone.server.backends里面进行的调用,

# 我们在调试的时候,也需要手动的import这些配置类的,昨晚刚搞明白

# 总结一下:我们通过load_auth_methods()加载所有默认提供的方法

# 通过get_auth_method获取需要的认证方法
resp = method.authenticate(context,
auth_info.get_method_data(method_name),
auth_context)

# 这里就是我们获取的方法了,然后调用plug-ins里面相对应的方法

# 认证函数调用后resp应该是None,如果有就说明是有问题的,需要进行其他认证
if resp:
auth_response['methods'].append(method_name)
auth_response[method_name] = resp
# 上面就是将认证的信息进行储存
if auth_response["methods"]:
# 这边就是一个检测过程
raise exception.AdditionalAuthRequired(auth_response)

if 'user_id' not in auth_context:
msg = _('User not found')
raise exception.Unauthorized(msg)

# 在method.authenticaticate检测通过后,会将user_id填入auth_context中

# 如果没有,不就说明是有问题吗?

最后,关于token就是这些,还要就是version 2的token如何认证,数据格式,我们后面会再进行分析.以后关于keystone的文章主要有policy(大体)、token version 2&3(包括格式分析),分层多租户,policy(check细节),以及keystone的整体架构,包括其他部件的分析。还有待提高!

 

 

 

no comments
Share

发表评论