Keystone认证公共库(keystoneauth)
keystoneauth是一个认证的公共库,它把和keystone进行认证的逻辑单独抽出来,提供了一个标准的认证过程,其实这个库就是把keystoneclient里的一部分和认证相关的代码拿了出来,单独作为一个库,然后就可以在各个服务的client中复用,这么做有下面的好处:
- 各个服务在和keystone进行认证时,就可以直接使用复用这个库,不用关心认证的逻辑了
- 安全相关的代码维护在一个地方,如果有问题,可以很快的进行修复
keystoneauth抽象出来3个概念:
- session:它封装了requests.session,并且引用了auth_plugin,通过auth_plugin拿到认证信息,然后由session向一个URL发起HTTP请求, session是可以复用的
- auth_plugin:抽象出了多种认证的方式,包括keystone的v2, v3的password, token等认证,还有v3中的联合认证
- loader:loader主要用来加载auth_plugin,即用来根据配置项实例化一个auth_plugin,每一个auth_plugin使用的认证参数都不一样,loader对这些参数进行了抽象,可以根据选择的plugin,来加载相应的配置项;loader还可以用来load session
举个例子:
1 | from keystoneauth1.identity import v3 |
设计
keystoneauth中抽象运用的非常经典,尤其是对auth_plugin的抽象,这是一个非常不错的学习素材,本节主要围绕上面的3个概念,对其设计和实现进行剖析。
Session
理解Session非常重要,因为它贯穿了认证的整个过程,联系了keystonemiddleware, keystoneauth, keystoneclient等多个库。Session从功能上理解就是用来发送http请求的,它封装了requests这个库的session,并尽可能提供足够多的参数,与Session紧密相关的还有一个Adapter,它是Session的一个适配器,对Session进行简单的封装,主要是增加了几个用于过滤endpoint的属性。下面是Session相关的类图:
Session中的session属性就是request.session对象,用来发送http请求,auth属性就是Auth类的对象,用来获取认证需要的信息的,Auth相关的内容在下一个小节详细介绍。这里重点介绍一下request()这个方法,这个方法就是收集header和相关的参数,然后调用requests.session发送http请求的,其原型如下:
1 |
|
其中比较重要的是url和endpoint_filter,还有authenticated这三个参数:
- authenticated, 当为False时,即不需要认证,那么就不会去获取认证的header信息,即X-Auth-Token
- endpoint_filter,这个是用来从keystone的endpoint列表中过滤出需要用到的endpoint,有下面几个过滤项:
- service_type
- service_name
- interface
- region_name
- url,http请求的路径,这个url可以有两种:
- 相对路径,即只有一个request_path,这种情况,会使用上面的endpoint_filter参数去keystone请求endpoint,然后拼接成一个完整的url地址
- 全路径,即是一个完整的uri地址,这种情况,就不会再去请求endpoint了,直接使用这个完整的url就可以了
为什么会有这些开关和判断逻辑呢?要知道session是复用的,比如在session中去向某一个url发送请求时,如果是相对路径,那么还要去获取endpoint,而获取endpoint这个操作也是在相同的session中做的,而获取endpoint的url就是一个全路径了,就是keystone的服务地址,获取到endpoint之后,再继续执行之前的动作,这类似于一个递归操作。
Adapter除了增加了几个本地化的属性之外,还实现了和Session一样的接口,在外界看来,Adapter其实就可以当作是一个Session了,是透明的,唯一的区别在哪里呢?区别就在那几个本地化的属性,在Adapter的request()方法中,将那几个属性组成了endpoint_filter,传递给了Session的request()方法:
1 | def _set_endpoint_filter_kwargs(self, kwargs): |
这样通过这个adapter发出去的请求,就只能发送到某个固定的endpoint了,比如service_type=’identity’, interface=’admin’, version=’v3’,那通过这个adapter发出去的请求就都是请求到keystone的v3的admin地址了。为什么这么做呢?因为Session是可以复用的,多个client可能使用同一个session,但是每一个client所使用的endpoint地址不一样,也即endpoint_filter不一样,那这些信息不能保存在全局的session中,因为session是共享的,只能加一层封装,放到session的外部,也就是这个adapter了,所以可以见到多个adapter,一个session的情况。在各个client中,都会使用这个Adapter来封装Session,如果没有使用keystoneauth中的Adapter,也会自己写一个类似的,比如keystoneclient中就是自己写了一个(其实是原来就有的,只不过keystoneauth是从keystoneclient抽出去的)
Auth Plugin
Keystone提供很多种认证的方式, 可以通过password,还可以直接通过token进行认证,并且每种还提供v2和v3版本,除此之外,还有OpenID认证,联合认证以等,这些认证方式都被keystoneauth抽象出来以插件的形式存在,可以根据自己的情况来选择相应的插件。那这些插件到底做了什么工作呢?一句话概括就是根据原始的认证参数,比如username/password,对keystone进行认证,然后获取到相关的信息,比如token, endpoint等。我们这里只对v2和v3的password和token的这几种插件进行分析。
首先我们先来看看最本质的东西,即如何获取一个token,所有的插件其实都是围绕这个主题组织代码的,获取token使用下面的接口:
1 | POST /v3/auth/tokens |
示例如下:
1 | curl -v -X POST -H "Content-Type: application/json" http://localhost:35357/v3/auth/tokens -d '{"auth":{"identity": {"methods": ["password"], "password": {"user":{"name": "admin", "password": "password", "domain": {"name": "Default"}}}}, "scope":{"project":{"name": "admin", "domain": {"name": "Default"}}}}}' | python -m json.tool |
从上面的示例上,我们可以看到一个插件想要进行认证,需要两种信息:一个是认证服务的地址,即auth_url,还有一个就是认证原始信息了,username, password, project_name等,Auth Plugin就是对这些信息进行抽象组装,使用上小节讲的Session去获取token,然后就可以获得endpoint, roles等信息。其静态类图大致如下:
可以看到整个类图的核心逻辑,就是根据原始的认证信息,获取到认证之后的信息,即auth_ref,它包含了token, service_catalog, user_id, project_id等完备的信息。从父类继承下来,分为了三个模块:v2, v3, generic,v2就是使用keystone v2的接口进行认证,v3就是使用keystone v3的接口进行认证,那generic是干嘛的呢?generic就是不显示指定使用v2还是v3,而是根据传递的参数,还有当前keystone支持的API版本信息,来决定使用哪种认证方式的。
v2和v3相对来说比较简单直接,他们就是各自收集自己的参数,然后向auth_url发起认证请求,注意这里的auth_url是需要带版本信息的,比如你使用v2.Password的插件进行认证,那么传递给v2.Password的auth_url就必须得加上”/v2.0”,,比如:auth_url = "http://localhost:5000/v2.0/"
,因为从上面的类图中可以看到,它是直接使用auth_url拼接成的路径,如果不传版本信息,路径就不对了,就会报404了。v3也是类似。
generic就比较复杂一点了,它有个判断使用哪种认证方式的逻辑,首先它构造了一个Discover类,这个类在上面的类图上没有画出来,不过它的作用很简单,就是根据auth_url去keystone请求版本信息,然后解析这些信息,可以根据版本号进行排序,还可以获取指定版本的url,比如auth_url = "http://localhost:5000/v3"
,从keystone获取到的版本信息是:
1 |
|
当然,也可以请求根路径,即auth_url = "http://localhost:5000/"
,这样得到的就是一个版本信息的列表了,包括v2和v3的。这个请求也是通过session发送出去的,因为是全路径,而且authenticated设置为False,所以不需要任何认证认证信息,就可以直接发送出去。所以判断使用哪种认证方式的逻辑大致如下:
- 首先构造Discover,根据auth_url去keystone拿版本信息,这个auth_url可以是带版本的,也可以是不带版本的
- 如果因为某种原因从keystone获取版本信息失败,那么就会直接根据auth_url中带的版本信息来确定使用哪种认证方式,如果auth_url没有带版本信息,那么就报错了
- 如果第1步从keystone正常获取到了版本信息,那么会将解析的结果根据版本信息排序组成一个列表,然后遍历这个列表,会使用第一个符合条件的版本信息来构造认证插件,即使用第一个符合条件的版本的认证方式,这个条件是什么呢?就是不能v2和domain同时存在,因为domain是v3中的概念。比如指定了
auth_url="http://localhost:5000/v2.0"
,然后又指定了domain_id或者domain_name等参数,这是不行的。但是指定了auth_url="http://localhost:5000/"
,然后指定了domain信息,那么它就会判断使用v3的认证方式了。即在满足v3认证的条件下,优先使用v3。这里的逻辑有点多,但是并不复杂,下面的代码片段描述了完整的逻辑:
1 | def _do_create_plugin(self, session): |
Loader
在各个client中使用上面的两个概念就完全足够了,loader是用来做什么的呢?上面的认证插件,每一个都有对应的一个loader,即这个loader是专门用来加载这个认证插件的,并且最重要的是每一个loader都维护了一个要初始化其对应的认证插件的参数列表,使用这些参数列表,就可以实例化一个认证插件了。这些参数可以注册到配置文件中,然后loader在实例化这个认证插件时,就可以从配置文件中读取参数信息,然后实例化。这套机制主要是用在keystonemiddleware中的,即认证中间件,用在每一个服务的API中。下面是Loader相关的类图:
可以看到跟上面Auth Plugin的类图基本上是一一对应的,其用法大致如下:
1 | plugin_loader = loading.get_plugin_loader(plugin_name) # 根据plugin_name使用stevedore实例化一个loader |
数据流
通过上面的分析,可以看到这里面的关系其实是很乱的,尤其是还有递归的操作,很容易绕进去绕不出来了,下面我们就以最开始的例子为例,分析下这个过程的数据流,以及各个组件之间是如何交互的。下面是novaclient和keystoneauth之间的关系类图:
SessionClient继承自keystoneauth的Adapter,并且v2.Client引用了SessionClient,跟我们在上面在Session小节中的分析是一致的,即在每一个client中,都会使用Adapter来封装Session,目的是为了维护endpoinit_filter等参数,SessionClient中enpoint_filter分别为: service_type=”compute”,interface=”publicURL”,v2.Client中发出的请求,都是通过SessionClient来发出的,SessionClient又调用Session。
好,下面我们就让这些都“动”起来,并且以v2, v3, generic的Password插件为例,每一个都举一个例子:
v2
先来看v2,例子如下:
1 | from keystoneauth1.identity import v2 |
整个过程的序列图大致如下:
注意上面的Client指的是nova中的v2.Client,而SessionClient其实是一个keystoneauth.Adapter,重点是Session和v2.Password的交互:
- 第一步Session要通过v2.Password拿headers,然后v2.Password又通过Session去向Keystone发送请求,拿到一个token,然后v2.Password才将headers返回给Session,这里的headers就是X-Auth-Token
- 第二步因为Sessin要向nova发送请求,但是现在只知道部分url,即“/servers”,并不知道nova的endpoint是多少,因此又通过v2.Password去请求nova的endpoint,其实service_catalog已经包含在第一步请求的token中了,只需要用endpoint_filter过滤出nova的endpoint就行了
- Session拿到了headers和endpoint,就可以向nova发送请求了
v3
v3的例子如下:
1 | from keystoneauth1.identity import v3 |
v3和v2其实在流程上并没有什么大的区别,只是抽象不同,请求的路径不同:
generic
generic的情况稍微特殊一点,它有个discover的过程,diccover首先会去根据auth_url请求版本信息,这会向Keystone发起一次请求,然后会根据参数创建v2或者v3的认证插件,然后接下来就和v2, v3的过程是一样的了。
代码示例如下(假设keysone是支持v2和v3的):
1 | from keystoneauth1.identity import generic |
其序列图如下:
通过这个图,可以看到,Discover从Keystone获得版本信息,然后generic.Password根据这些信息,结合自己的参数决定应该使用v3的Password认证插件,于是使用新的auth_url创建了v3.Password,然后由v3.Password去跟Session和Keystone交互拿到token,也就拿到了auth_ref,之后就跟前两个一样了。
参数
每一个auth plugin的参数都不一致,每个loader也分别维护了对应的plugin的参数列表,可以通过loader拿到参数列表,然后将这些参数注册到配置文件中,这是在keystonemiddleware中的做法,通常将这些配置项注册在keystone_authtoken这个section下面;也可以像上面的例子一样,直接实例化auth plugin,无论哪种做法,每个plugin对应的参数列表都是一样的。下面就整理成一个表格,方便查阅,这样我们就可以知道在使用哪种插件的时候,该配置哪种参数。
v2 | v3 | generic | |
---|---|---|---|
common | auth_url tenant_id tenant_name trust_id |
auth_url domain_id domain_name project_id project_name project_domain_id project_domain_name trust_id |
auth_url domain_id domain_name project_id project_name project_domain_id project_domain_name trust_id default_domain_id default_domain_name |
token | token | token | token |
password | username user_id password |
user_id user_name user_domain_id user_domain_name password |
user_id user_name user_domain_id user_domain_name password |
此外在loading中还有两个配置项:
- auth_type/auth_plugin,选择哪种认证插件(auth_plugin被标记为废弃,应该使用auth_type),目前有下面几种可以选择:
- password, 即generic password
- token, 即generic token
- v2password
- v2token
- v3password
- v3token
- auth_section,auth的配置项所在的section,默认为空,如果不配置的话,就是keystone_authtoken
在上面的参数中,最让人迷惑的就是auth_url了,到底应该是用5000还是35357?到底应不应该加版本信息?根据上面的分析其实很容易决断,auth_url主要是用来获取token的,使用5000和35357都可以,但是如果配置在keystone_authtoken中,是给各个服务使用的,一般都配置成admin的接口,也即35357,如果是给普通用户使用的,比如在各个client中,应该使用public的接口,即5000端口;至于版本信息,如果使用v2的认证插件的话,就必须带上”/v2.0”,如果使用v3的认证插件的话,也必须带上”v3”,如果使用generic的话,可带可不带,带的话,就需要注意信息要匹配,即v2不能和有domain信息的配置项共存,因为domain是v3中的概念,否则就没法决断该用哪个插件了,所以下面的例子都是合法的配置:
v2:
1 | [keystone_authtoken] |
v3:
1 | [keystone_authtoken] |
generic:
1 | [keystone_authtoken] |
generic v2:
1 | [keystone_authtoken] |
generic v3:
1 |
|
但下面的这个是不合法的:
1 | [keystone_authtoken] |
因为auth_url中指定了v2.0,但是在下面又配置了domain等信息,让discover没法判断该使用哪种认证方式。
是不是有点乱?的确,现在的这种做法其实不太友好,不统一,有的必须有,有的必须没有,并且明明auth_type已经选择了v2password,但是在auth_url中,却仍必须要填写上v2.0,这不是重复了吗?社区有Bug在提这件事,但是目前建议还是采用generic的配置方式,auth_url配置到根目录,即不指定版本信息,然后再配置上domain信息,让它选择使用v3的认证方式。
Keystone认证公共库(keystoneauth)