OpenStack Keystone如何处理长时间操作过程中的token过期问题

背景介绍

本文梳理 Keystone 的 token 过期的处理方式。Token 是有期限的,而且不能延期,过期以后,身份验证就会被拒绝。这种设计是token安全性的要求。那么,如果用户通过验证以后,所执行的服务操作费时很长,中途再次需要使用用户的已过期的token,该怎么处理?

早期版本的Keystone是不支持这种情况的,一般通过其他的设计来绕开这一局限性,比如把过期时间设置得足够长等。这实际上降低了token的安全性。比如,Horizon的用户就会遇到由于token过期而导致被log out的情况。对于那些长时间使用Horizon的用户而言,这使得Horizon比“正常”的web应用要显得繁琐一些。 在服务之间调用时,这一问题更严重。比如,当用户调用Nova API创建一个服务器时,Nova需要调用Glance服务来得到镜像,需要调用cinder来创建磁盘等等,也就是服务之间需要调用。当用户使用了合法(未过期)的token调用了Nova API,而Nova在调用后续其他服务API时,如果操作时间很长,那么用户的Token可能会过期。早期的设计中,这种请求下请求会超时失败,而且很难意识到是由于token过期导致的。

好消息是这一问题已经解决了。在现在的设计中,Keystone会区分user token和service token。对于服务之间的调用(Horizon也是一个服务),keystone会校验service token来判断身份验证是否通过,然后使用user token来获得用户的信息,即使该user token已过期。

因此,区分用户token和服务token是处理这种场景的关键。本文详细介绍OpenStack Keystone是如何实现这一需求的。

Keystone 解决这一问题的过程

  1. 2014.4.22, Adam Young, 提出了Horizon中session活跃而token过期的问题,他提出的解决办法是 unscoped token 可以生产新的 scoped token,他称之为 [Session extendable tokens](https://blueprints.launchpad.net/keystone/+spec/session-extendable-tokens) 。这种解决办法后来并没有被采用 [review#96648](https://review.openstack.org/#/c/96648/)。
  2. 2014.6.6, 有用户在 "Ask OpenStack"上问类似的问题 [How to do api token renewal](https://ask.openstack.org/en/question/31563/how-to-do-api-token-renewalor-expiration-extension/)。得到的回答是上面Adam Young所提出的session token的概念。可是,REST API是没有session概念的,这也使得session token的应用范围变窄,而且非常特殊,不是一个好的办法。
  3. 2015.2.13, Jamie Lennox 提出了 [request helpers](https://blueprints.launchpad.net/keystonemiddleware/+spec/request-helpers),初步提出了使用X-Service-Token和X-OpenStack-Request-id来辅助解决服务间调用的token问题。这一想法中关于 Service Token的部分后来被实现了,而关于 X-OpenStack-Request-id的部分则没有继续实现。原因是使用这一request id并不能解决所谓同一请求来源,进而也并没有加强安全性。
  4. 2015.4, 用户在openstack horizon的用户邮件中讨论这一恼人的问题 [Keystone token expiration causes user to be logged out](http://lists.openstack.org/pipermail/openstack-dev/2015-April/061512.html),答案是“这个就是这样的”。
  5. 2016.10.4,Jamie Lennox 在上一个提案的基础上,进一步提出了采取 service token的办法来解决user token 过期的问题,即 [allow fetching an expired token](https://blueprints.launchpad.net/keystone/+spec/allow-expired)。这一方案能统一解决Web应用和API服务中的user token过期问题。版本是Ocata。该方案主要修改了以下几个部分。
  1. keystonemiddleware.auth_token, [Add service token to user token plugin](https://review.openstack.org/#/c/141614/), 把 X-Auth-Token 复制为 X-Service-Token。这种做法主要是为了概念上突出验证者的身份是服务而不是普通用户。该变更本身不影响身份验证结果。在auth_token中,会区分request是service_token还是user_token, 如果是service token,那么在get user token info的时候,会传入 allow_expired=1,从而允许取得已过期的user token信息。在keystone自己内部使用该middleware时,其处理过程被修改了,只保留了user token这一条线,也就是说keystone内部没有service token的概念。只有在非keystone的其他服务中使用时,service token机制才会起作用。因此,如果有某个第三方的服务希望使用keystone的service token机制,可是并不是python语言开发的,用不了这个middleware,则可以自己实现类似的逻辑,在取得用户的token信息时设置allow_expired=1。
  2. Ocata Keystone, Allow retrieving an expired token, https://review.openstack.org/#/c/381361/, 以及实现 https://review.openstack.org/#/c/382098/
  3. keystone.token, [Don't validate token expiry in the persistence backend](https://review.openstack.org/#/c/381380/)
  4. keystone.middleware.auth, [Ignore unknown arguments to fetch_token](https://review.openstack.org/#/c/379035/)

总结

面对token过期的问题,用户和社区开发者各自想办法,如设置长的过期时间、开发专门处理web session的token,允许token renewal等等,有的会降低token的安全性,有的解决的问题不多,带来的概念上的问题更多。而最终社区认识到了应该去区分用户和服务token的用途,形成了简洁清晰的设计,在没有降低安全性的基础上,解决了这一问题。整个过程历时两年多,作者们对问题的认识也是逐渐完善。可见,一个好的想法和设计,哪怕最后看起来很简单和理所当然,期间的打磨和付出也是很大的。