本文简要介绍Cloud Foundry的用户认证过程,以及相关项目组件,并介绍同企业LDAP认证集成的方法。碍于篇幅,本文将众多代码和配置文件信息用github链接的方式给出,具体到行,需要查阅。因为时间有限,如有错误,请指出
由于博客园不支持markdown格式,本文内容转自本人新Blog http://tiewei.github.io/ ,转载请标明作者
Cloud Foundry 认证
Cloud Foundry 作为如今炙手可热的开源项目,借助其可以方便搭建企业内部PaaS平台,然而在集成时首先遇到的问题就是同企业内部系统中的授权系统(如LDAP)集成。与认证活动的相关的组件,主要有以下4个:
- vmc : V1版客户端,由于V2近期将release,将采用cf客户端,但原理和功能与vmc一样,但由于最新的0.5.x在请求认证api上有些差异,不能正常请求login-server,本文所述方法vmc需要0.4.x或0.3.x版本。vmc是一个Ruby项目,代码见 https://github.com/cloudfoundry/vmc
- cloud_controller : VCAP的控制组件V1版本,主要功能是告知客户端(vmc)进行用户认证请求的地址,并且根据用户TOKEN请求UAA认证。cloud_controller是一个ROR项目,代码见https://github.com/cloudfoundry/cloud_controller
- uaa : 用户认证模块,Cloud Foundry进行用户管理/认证的核心模块,后台DB保存用户信息,对外提供多种认证接口,如OAuth 2和SCIM,以及JWT格式的支持。uaa是一个Java Spring项目,代码见https://github.com/cloudfoundry/uaa
- login-server : 如果需要额外的外部授权方式以及定制登录页面,可采用login-server进行,只进行授权,不进行用户管理,无db保存用户数据。login-server是一个Java Spring项目,代码见https://github.com/cloudfoundry/login-server
认证系统的发展史随着Cloud Foundry的发展逐渐成熟起来的。
起初,Cloud Foundry的认证系统也从最初的在Cloud Controller组件中,用户将用户名和密码存在Cloud Controller 的数据库中,当登录时提供用户名和密码,并获取一个token,后续操作需要提供token进行验证后方能进行,如图所示:
进而增加了UAA (User Account and Authentication) 组件和ACM (Access Control Management)专门用于用户认证和访问控制(后者本文暂不涉及),UAA采用多种开放的标准协议,支持OAuth验证,TOKEN采用标准的JWT(JSON Web TOKEN)格式封装,并对外开放SCIM(System for Cross-domain Identity Management)接口进行用户操作。在此基础上,需要授权与认证的组件就相当于OAuth验证的活动参与者,VMC作为客户,Cloud Controller作为第三方客户端,UAA则扮演服务提供方的角色,同时基于Cloud Foundry开发的系统也可以通过OAuth协议请求UAA授权和认证。此时结构如图所示:
然而,在私用云中往往涉及外部认证的场景,一方面来自企业认证(LDAP等方法)的用户在登录时需要能够进行外部验证以保证用户身份,另一方面用户在访问cloud foundry时需要能够继续通过UAA进行认证,为此又加入了login-server组件来支持外部授权,同时作为UAA的一个特殊客户端,可以申请UAA的TOKEN。最终形成了一个支持扩展的认证组件集合,如图所示。
认证过程
如上图所示,认证过程参与者众多,登录的流程简单来说包含图中所示的10个步骤:
-
vmc -> cloud_controller : GET /info
在返回的JSON信息中包括
"authorization_endpoint": "http://login.cf.com"
,vmc会根据此信息申请验证 -
vmc -> login-server: GET /login
此处是请求login-server获取需要验证的信息的提示,如
"prompts": { "username": [ "text", "Email" ], "password": [ "password", "Password" ] }
该提示信息的处理逻辑在
org.cloudfoundry.identity.uaa.login.RemoteUaaController
中,根据prompts
属性,首先选取prompts
属性,如果没有被设定,则请求UAA uaaBaseUrl(配置项中的uaa.url
见代码),如果请求失败,则采用默认值Email+Password,相关代码见RemoteUaaController#getLoginInfo。如果要修改提示信息,可以在spring-servlet.xml中注入属性值,或调整最后默认值。 -
vmc -> login-server : POST /oauth/authorize?client_id=vmc&response_type=token
vmc根据先前获取的prompts信息提示用户输入用户名/密码,在请求body中包括了类似验证信息
credentials={"username":"foo","password":"bar"}
-
login-server -> ldap
login-server对ldap和OAuth的请求和验证是通过Spring Security实现的。对应的filter是AuthzAuthenticationFilter(spring配置),会根据spring_profile确定是直接使用uaa的oauth验证还是请求外部验证。对于ldap类型的验证,将采用UsernamePasswordExtractingAuthenticationManager进行(spring配置代码),实际还是通过
org.springframework.security.ldap.authentication.LdapAuthenticationProvider
代理来进行实际的LDAP验证。如果验证成功,进入RemoteUaaController#startAuthorization进行响应。 -
login-server -> uaa : POST /oauth/authorize?client_id=vmc&response_type=token&source=login&username=foo
请求的消息体在RemoteUaaController#startAuthorization中组装,通过
org.springframework.security.oauth2.client.OAuth2RestTemplate
发送请求,设置在spring配置中.HTTP HEAD中包含[{response_type=[token], redirect_uri=[https://uaa.cloudfoundry.com/redirect/vmc], client_id=[vmc], source=[login], username=[foo]}]
表示源请求来自vmc,由login-server向uaa请求验证,用户名为foo
-
uaa -> uaa-db -> login-server
uaa也是利用Spring Security实现的认证和授权功能。请求中包含
source=login
向uaa声明来源来自login-server,被声明在login-server-security.xml中的loginAuthorizeRequestMatcher命中,这里声明了两个filter<custom-filter ref="oauthResourceAuthenticationFilter" position="PRE_AUTH_FILTER" /> <custom-filter ref="loginAuthenticationFilter" position="FORM_LOGIN_FILTER" />
前者声明为org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint的filter,在uaa.yml中配置了Login-server应具有的权限操作oauth.login 后者是AuthzAuthenticationFilter的一个实例,会从request中抽取用户的username,将实际的认证操作代理给loginAuthenticationMgr中,声明为LoginAuthenticationManager的一个实例,根据spring配置,传入两个重要参数,其中”addNewAccounts“用于判断是否在用户不存在时根据Login传入的用户信息新建用户,对应
uaa.yml
中的login.addnew
的值,”userDatabase“则根据配置文件中的database信息代理uaa-db的操作。在LoginAuthenticationManager#authenticate中,代码如下
@Override public Authentication authenticate(Authentication request) throws AuthenticationException { if (!(request instanceof AuthzAuthenticationRequest)) { logger.debug("Cannot process request of type: " + request.getClass().getName()); return null; } AuthzAuthenticationRequest req = (AuthzAuthenticationRequest) request; Map<String, String> info = req.getInfo(); logger.debug("Processing authentication request for " + req.getName()); SecurityContext context = SecurityContextHolder.getContext(); if (context.getAuthentication() instanceof OAuth2Authentication) { OAuth2Authentication authentication = (OAuth2Authentication) context.getAuthentication(); if (authentication.isClientOnly()) { UaaUser user = getUser(req, info); try { user = userDatabase.retrieveUserByName(user.getUsername()); } catch (UsernameNotFoundException e) { // Not necessarily fatal if (addNewAccounts) { // Register new users automatically publish(new NewUserAuthenticatedEvent(user)); try { user = userDatabase.retrieveUserByName(user.getUsername()); } catch (UsernameNotFoundException ex) { throw new BadCredentialsException("Bad credentials"); } } else { throw new BadCredentialsException("Bad credentials"); } } Authentication success = new UaaAuthentication(new UaaPrincipal(user), user.getAuthorities(), (UaaAuthenticationDetails) req.getDetails()); publish(new UserAuthenticationSuccessEvent(user, success)); return success; } } logger.debug("Did not locate login credentials"); return null; }
代码很简单,首先验证传入请求的类型是否是AuthzAuthenticationRequest并确认是OAuth2类型的验证,根据请求中包含的user信息,要求name和email字段至少二者有其一,如果name为null,则将email作为name,反之如果email为null,则根据name中是否包含@进行判断,如果包含,email=name,否则email=name@unknown.org,而givenName和familyName如果不存在,则分别取email字段的@前后两部分,具体代码见此。之后查询uaa-db中是否包含username=foo的用户,如果找到则返回验证成功。否则如果允许添加新用户,则发布新增用户的事件,由ScimUserBootstrap负责处理事件,新增用户,当用户添加成功后返回验证成功,否则验证失败。
简单介绍一下UAA中的事件机制,这里新增用户和记录Log等操作都基于Spring的事件机制,uaa项目内部总共有三类事件,AbstractUaaEvent + AuthenticationFailureBadCredentialsEvent (Spring的事件,UAA监听用于发布AbstractUaaEvent的事件实例以便log) + NewUserAuthenticatedEvent ,分别对应三个ListenerAuditListener + BadCredentialsListener + ScimUserBootstrap。SCIM在提供用户操作的REST标准接口之外,也监听新建用户的事件。其中AbstractUaaEvent主要利用JdbcFailedLoginCountingAuditService和LoggingAuditService,前者监听UserAuthenticationSuccess/PasswordChangeSuccess/UserAuthenticationFailure当用户登录后修改密码或登录失败时操作sec_audit表删除认证信息,后者则进行log的管理和统计记录等功能,NewUserAuthenticatedEvent则仅仅用户新建用户。
当通过这些Filters验证后,由org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize进行一番查询操作后返回token信息。
-
login-server -> vmc ->.vmc
login-server将token返回给vmc, vmc将其记录在~/.vmc/tokens.yml中。除token外还包括token类型、超时时间和JTI(JSON web Token Id)
token_type=bearer&expires_in=604799&jti=1815ccfe-68a4-4d1d-a16a-2eff55622002
-
vmc -> cloud_controller : GET /apps
当用户对cloud controller进行操作时(以/apps请求为例),vmc在HTTP HEAD中包含token信息
authorization : bearer [tokens]
-
cloud_contorller -> uaa : GET /token_key
cloud controller是一个典型的ROR工程,在所有的Controller都继承自ApplicationController,其中的
before_filter :fetch_user_from_token
将验证用户的TOKEN,首先需要解码token- 验证token,根据uaa.url和uaa.client_secret发送请求到uaa,获取token key
- 根据token信息解码,根据token_key解码token,获取user的email
-
cloud_contorller -> cc-db -> vmc
然后查询cc-db确定用户,查询token中包含的用户名是否在cc-db中存在,如果存在继续由对应请求的Controller处理,如 GET /apps由AppsController处理,具体的路由规则可以在config/routes.rb中查看
这里存在一个问题,当用户是通过
vmc register
方法注册用户时,会请求 POST /users 来创建用户,在UsersController中会根据uaa的配置在uaa和cc-db中创建用户,之后登录时能够通过uaa验证,发送其他请求时ApplicationController可以在cc-db中查找到用户,因而请求可以正常进行。 然而如果用户是从LDAP导入到UAA中去的,省去了注册环节,用户是在login-server向UAA请求token时加入uaa-db的,但cc-db中并没有该用户的数据,在这一步根据email查找用户时会失败,所以返回401错误。同时这里cloud controller查找用户并没有特殊的含义,只是记录用户访问cloud controller的时间(当使用uaa时cc-db的active字段都是false),并不通过该记录验证任何消息。因此我们进行了如下的代码调整。
代码调整
相关的代码调整增加对应本人fork cloud_controller的repo中,https://github.com/TieWei/cloud_controller/commit/49fc960330dc881adc199021b33c9c83d25fd85e
if (!user_email.nil?)
CloudController.logger.debug("user_email decoded from token is #{user_email.inspect}")
@current_user = ::User.find_by_email(user_email)
if @current_user.nil? && uaa_enabled?
CloudController.logger.debug("#{user_email.inspect} from uaa is not in CloudController DB, Try to create a proxy one")
user = ::User.new :email => user_email
# the password is encrypt with (user_email + current time)
user.set_and_encrypt_password(user_email + Time.now.to_s)
if user.save
@current_user = ::User.find_by_email(user_email)
CloudController.logger.info("proxy user #{user_email.inspect} from uaa is added into CloudController DB")
else
@current_user = nil
CloudController.logger.warn("proxy user #{user_email.inspect} from uaa is not added into CloudController DB")
end
end
end
如果用户不存在,且采用UAA的方式进行验证,则根据用户的email和当前时间生成一个代理用户(只用于让cloud controller知道该用户存在),并存入cc-db中。
实现虽然略显dirty,但是总归是能work了。
配置选项
-
使用cloud controller的master分支,最新版加入了login-server的支持,关键代码在此
-
使用 cloudfoundry-identity-uaa-1.4.1.war ,理论上login-server的spring_profile中有ldap即可,关键代码在此
-
使用 cloudfoundry-login-server-1.2.1.war,uaa有login-server-security.xml响应来自login-server的请求即可,关键代码在此。
-
编辑
login.yml
,设定如下spring_profiles: ldap ldap: base: url: 'ldap://your.domain.com:389/dc=domain,dc=com' userDnPattern: 'CN={0},ou=Employees, ou=Users'
PS: 默认的userDnPattern是
uid={0},ou=people
,如果需要调整(如上设定),需编辑此处代码为<value>${ldap.base.userDnPattern:uid={0},ou=people}</value>
-
如果需要从LDAP向UAA导入用户,需要编辑
uaa.yml
,设定如下login addnew: true
这样,Cloud Foundry就可以正确将用户登录信息向LDAP请求验证。
Reference:
-
OAuth 2 - token based authentication for web applications and APIs. Defines the client software as a role. Separates issuing tokens from how you use a token. Token issuance is defined both for browsers and for REST clients using a username/password. Token format is not defined by OAuth2, but one proposed standard format is JWT.
-
JWT - JSON Web Tokens, an upcoming standard format for structured tokens (containing data) which are integrity protected and optionally encrypted.
-
SCIM - cross-domain user account creation and management. REST API for CRUD operations around user accounts
-
https://github.com/TieWei/uaa/blob/master/docs/UAA-CC-ACM-VMC-Interactions.rst
- http://blog.cloudfoundry.com/2012/07/23/introducing-the-uaa-and-security-for-cloud-foundry
- http://blog.cloudfoundry.com/2012/11/05/how-to-integrate-an-application-with-cloud-foundry-using-oauth2
- http://blog.cloudfoundry.com/2013/02/19/open-standards-in-cloud-foundry-identity-services
- http://blog.cloudfoundry.com/2012/10/09/securing-restful-web-services-with-oauth2
- http://blog.cloudfoundry.com/2012/07/24/high-level-features-of-the-uaa