内容


使用 Liberty JAAS 登录模块实现 Bluemix 单点登录

可以减少登录和身份验证负载的强大组合

Comments

Java Authentication and Authorization Services (JAAS) 框架为用户身份验证和授权提供一个 API。JAAS 从用户身份验证和授权机制解耦应用程序代码,这样您就可以轻松地配置不同的登录模块,而无需更改应用程序代码。

使用传统的 Liberty Web 应用程序进行身份验证

若使用传统的 Liberty Web 应用程序,身份验证包括收集用户凭证,检查缓存中的 Subject,调用 JAAS 服务来对配置的注册表执行身份验证,如果在缓存中没有 Subject,则创建一个。该进程还会创建一个 HTTP Session 对象,跨多个请求识别用户。

使用 Bluemix Liberty Web 应用程序进行身份验证

Web 应用程序在 IBM Bluemix 云平台 上运行,利用 Liberty for Java 运行时,使用 Single Sign On (SSO) 服务 进行身份验证和授权,由于使用了 OAuth2 协议处理身份验证,它没有访问用户凭证的权限。对于 Bluemix Liberty Web 身份验证,Liberty 服务器不知道,因此不会产生任何 Subject 和 HTTP Session 对象来识别用户。在这种情况下,该应用程序必须负责识别用户,代码必须知道底层的登录机制,因为没有创建 HTTP Servlet API(SessionPrincipal)。 Bluemix 服务利用 Bluemix User Account and Authentication Service (UAA) 执行 SSO,这要求一个 OAuth clientID 和 Bluemix UAA 内部的客户端密码。

本教程介绍了如何将 Liberty 可用的身份验证添加到您的应用程序。您将看到一个示例应用程序如何使用 Bluemix SSO 服务实现用户登录。Liberty 服务器在身份验证成功时创建所有必要对象(SubjectPrincipalSession)。从登录机制中抽象出应用程序代码,并用 Liberty 减少用户识别的负担。

它如何工作

自定义 JAAS 登录模块使用 Bluemix SSO 服务执行身份验证,并用自定义凭据(用户名、访问令牌)创建 Subject。由服务器创建一个 Session 对象。使用 Subject API 可从下游代码访问自定义的凭据和原则。 此登录模块替换 system.WEB_INBOUND 配置的 Liberty 默认模块。该解决方案假设:

  • 有一个 Bluemix SSO 服务实例被添加到 Liberty 应用程序。
  • 已提供 SSO 客户端配置(比如 clientIDclientSecret)。

JAAS 登录模块中已配置了 SSO 客户端配置数据。一个 Liberty 应用程序使用两个 REST API(/dashboard /sso_dashboard)与此登录模块和 SSO 服务通信。

下面的步骤与上图中的数字对应:

  1. 用户调用 /dashboard API 来启动 login 进程。
  2. 该 API 用 SSO 服务启动一个 OAuth 流,调用 /authorize 端点 URL,含客户端 ID、状态(防止跨站请求伪造),以及回调 REST API。
  3. 在一个成功的 OAuth 流之后...
  4. 回调 API (/sso_dashboard) 被 SSO 服务调用,含临时的身份验证码和 state 值。
  5. 回调 REST API 调用自定义 JAAS 登录模块,并传递身份验证码。
  6. 登录模块请求一个访问令牌。
  7. 登录模块检索来自 SSO 服务的访问令牌。
  8. 登录模块还检索用户名。
  9. 登录模块用一个 Principal 和凭据创建一个 Subject
  10. /sso_dashboard API (或任何下游代码)可以访问来自 Subject API 或 Servlet API (HttpServlet.getUserPrincipal()) 的信息。如此一来,Liberty 服务器就知道这个登录进程,并识别来自 Subject 缓存的通过身份验证的用户。

构建应用程序所需要的内容

运行应用程序获取代码

关于示例应用程序

示例 Liberty 应用程序 LoginTestApp(单击上面的 获取代码 按钮)调用自定义登录模块 (CustomLoginModule),它利用 Bluemix SSO 服务执行 JAAS 身份验证。登录成功后,应用程序代码可以调用任何与身份验证相关的 API,比如                  HttpServletRequest.getUserPrincipal(),不需要知道底层的身份验证机制。

当您运行示例应用程序时,会出现一个 HTML 页面,里面有一个使用 Bluemix SSO 的 JAAS 登录链接。单击链接,以调用该应用程序的 /dashboard REST API 并启动身份验证进程。然后,您可以看到 Bluemix SSO 登录页面 ,您在那里输入 Bluemix 凭据。身份验证成功后,会显示您的 Bluemix ID 和 Liberty 会话 ID。

步骤 1. 创建一个简单的 Bluemix Liberty JAX-RS 应用程序

写一个小 Liberty JAX-RS 应用程序,使用两个 REST API(/cloudLogin/dashboard 和 /cloudLogin/sso_redirect),如下面的代码示例所示。作为起点的 /dashboard API 启动身份验证进程(第 44 行至第 45 行)。该 API 调用 SSO 服务授权 API,有一个 OAuth 客户端 ID、一个任意字符串 (state) 和一个回调 REST API (/sso_redirect)。

在成功的 OAuth 流之后,Bluemix SSO 服务用一个临时授权码和任意字符串 (state) 调用 /sso_redirect API。然后,该 API 通过调用 HttpServletRequest.login(code, client_secret) 方法(第 63 行),调用 Liberty 自定义登录模块,并传递授权码。API 通过调用 getUserPrincipal() 方法检查登录是否成功。在成功登录后,从 Subject 可访问有关用户(名)和登录 (access-tokenSession) 的信息。第 85 行显示,Liberty 服务器创建了一个 Session 对象,跨多个请求识别用户。

43.@GET
44.@Path("/dashboard")
45. public Response dashboard() throws URISyntaxException {
46. 	System.out.println("Dashboard GET request headers:" + headersAsString(request));
47. 	String stateId = "anyUniqueString";
48. 	URI uri = new URI(AUTH_END_POINT + "?state=" + stateId 
49. 			+ "&scope=profile&response_type=code&client_id=" 
50. 			+ CLIENT_ID + "&redirect_uri=" 
51. 			+ buildRESTApiUrlString(SSO_END_POINT_PATH));
52. 	System.out.println("Dashboard GET redirect URI:" + uri);
53. 	return Response.seeOther(uri).build();
54.}
55.@GET
56.@Produces(MediaType.TEXT_HTML)
57.@Path(SSO_END_POINT)
58. public Response ssoDashboard(@QueryParam("code") String code, @QueryParam("state") String state) 
59. 		throws URISyntaxException, UnsupportedEncodingException, WSSecurityException {
60. 	System.out.println("ssoDashboard GET request headers:" + headersAsString(request));		
61. 	try {
62. 		// pass authorization code as user name to CustomeLoginModule
63. 		request.login(code, code);
64. 	} catch (ServletException e) {
65. 		e.printStackTrace();
66. 	}
67. 		if (request.getUserPrincipal() == null) {
68. 		// user is not logged in.
69. 		return Response.status(Status.UNAUTHORIZED).
70. 				type(MediaType.TEXT_HTML).
71. 				entity("You are not authorized.").build();
72. 	}
73. 
74. 	Subject subject = WSSubject.getCallerSubject();
75. 	Set<Object> credentials = subject.getPublicCredentials();
76. 	String userName = request.getUserPrincipal().getName();
77. 	String accessToken = null;
78. 	for (Object obj : credentials) {
79. 		if (obj instanceof HashMap) {
80. 			HashMap<String, String> cred = (HashMap) obj;
81. 			accessToken = cred.get("access-token");
82. 			// create application session with these information.
83. 		}
84. 	}
85. 	String libertySessionId = request.getSession().getId();
86. 	String msg = "<p>Welcome " + userName 
87. 			+ ".You are successfully authenticated using Bluemix SSO service.</p> " 
88. 			+ "<p> Your Liberty session ID :"+ libertySessionId +"</p>";
89. 	return Response.status(Status.OK).type(MediaType.TEXT_HTML).entity(msg).build();

步骤 2. 实现 Bluemix SSO 登录模块

登录模块需要特定的 URL(用户配置文件端点、令牌端点、回调登录 REST API)、一个 OAuth clientID,以及 OAuth 客户端密码作为选项传递。这些值被存储在类变量中。

44. public void initialize(Subject subject, CallbackHandler callbackHandler, 
       Map sharedState, Map options) {
45. 		this.subject = subject;
46. 		this.callbackHandler = callbackHandler;
47. 		this.sharedState = sharedState;
48. 
49. 		if (options.containsKey("PROFILE_END_POINT")) {
50. 			profileEndPoint = (String) options.get("PROFILE_END_POINT");
51. 		}
52. 
53. 		if (options.containsKey("CLIENT_SECRET")) {
54. 			clientSecret = (String) options.get("CLIENT_SECRET");
55. 		}
56. 
57. 		if (options.containsKey("SSO_END_POINT_PATH")) {
58. 			ssoEndPointPath = (String) options.get("SSO_END_POINT_PATH");
59. 		}
60. 
61. 		if (options.containsKey("CLIENT_ID")) {
62. 			clientId = (String) options.get("CLIENT_ID");
63. 		}
64. 		
65. 		if (options.containsKey("TOKEN_END_POINT")) {
66. 			tokenEndPoint = (String) options.get("TOKEN_END_POINT");
67. 		}
68. 	}

登录方法 与 Bluemix SSO 服务通信,并执行身份验证。首先,使用NameCallback 对象收集临时身份验证码,如第 76 行至第 77 行所示。

71. public boolean login() throws LoginException {
72. 		
73. 	System.out.println("Login module invoked");
74. 
75. 	// authorization code is passed as user name param value.  
76. 	NameCallback nameCallback = new NameCallback("User:");	
77. 	Callback callbacks[] = new Callback[1];
78. 	callbacks[0] = nameCallback;	
79. 
80. 	try {
81. 		callbackHandler.handle(callbacks);
82. 	} catch (IOException e1) {
83. 		e1.printStackTrace();
84. 	} catch (UnsupportedCallbackException e1) {
85. 		e1.printStackTrace();
86. 	}
87. 
88. 	// get the authorization code
89. 	String code = nameCallback.getName();
92.    CloudAccessToken accessToken = 
93. 		getAccessToken(clientId,
94. 				clientSecret,
95. 				tokenEndPoint,
96. 				code,
97. 				ssoEndPointPath);
98. 		
99. 	// get user name
100. 	HashMap<String, String> headerList = new HashMap<String, String>();
101. 	headerList.put("Authorization", accessToken.getFullAccessToken());
102. 	ASMRestClient restClient = new ASMRestClient();
103. 	ASMRestResponse response = restClient.doGet(profileEndPoint, headerList, null);
104. 	if (response.getStatusCode() != Status.OK.getStatusCode()) {
105. 		System.out.println("User profile end point returned status code:" 
106. 				+ response.getStatusCode());
107. 		return false;
108. 	}
109. 			
110. 	CloudUserInfo userInfo = response.getEntity(CloudUserInfo.class);
111. 	String name = userInfo.getUserDisplayName()[0];
112. 		
113. 	// add access token and user info as JAAS subject credentials.
114. 	HashMap<String, String> map = new HashMap<String, String>();		
115. 	map.put("access-token", accessToken.getAccessToken());
116. 	map.put("access-token-type", accessToken.getTokenType());		
117. 			
118. 	subject.getPublicCredentials().add(map);
119. 		
120. 	java.util.Hashtable<String, Object> customProperties = 
121. 			(java.util.Hashtable<String, Object>) sharedState.
122. 			get(AttributeNameConstants.WSCREDENTIAL_PROPERTIES_KEY);
123. 	customProperties = new java.util.Hashtable<String, Object>();
124. 		
125. 	customProperties.put(AttributeNameConstants.WSCREDENTIAL_UNIQUEID, name);
126. 	customProperties.put(AttributeNameConstants.WSCREDENTIAL_SECURITYNAME, name);
127. 	sharedState.put(AttributeNameConstants.WSCREDENTIAL_PROPERTIES_KEY,
128. 			customProperties);

创建一个 JAAS SubjectPrincipal 及凭据,如上所示:

  • 第 92 行至第 97 行:通过用 clientIDclientSecret、授权码和回调登录 REST API URL 对 SSO 服务令牌端点发出一个 HTTP 请求来检索 accessToken
  • 第 102 行至第 111 行:这个 accessToken 被用作配置文件端点请求的授权头,以检索用户名。
  • 第 114 行至第 128 行:使用从 Bluemix SSO 服务检索到的已验证的用户信息构成 JAAS Principal 和凭据。
  • 第 114 行至第 118 行:一个公共凭据被添加到 Subject,其中包含访问令牌及其类型。
  • 第 125 行至第 128 行:含用户信息的哈希表被添加到共享状态变量(在初始化方法中传递),以创建 JAAS Principal。
  • 第 127 行:Liberty 框架使用在共享状态变量中定义的密钥来搜索哈希表对象的值,并创建 Principal

步骤 3. 添加 Bluemix SSO 服务并生成 SSO 客户端配置

现在是时候将示例应用程序推送到 Bluemix 并将 SSO 服务与它绑定了。 遵循 单点登录入门 中的步骤,生成 SSO 客户端配置并提供 /sso_redirect URL 作为回调 URI 值。

记录 clientID 和 client-secret 值,因为在下一步中需要它们。

步骤 4. 配置自定义 JAAS 登录模块

自定义登录模块是在示例 Liberty 库应用程序中配置的。server.xml 文件中负责加载自定义登录模块的代码片段如下所示。此登录模块的库驻留在服务器的 lib 文件夹中,并且被 customLoginLib 元素引用(第 25 行)。 sytem.WEB_INBOUND 配置的默认登录模块被自定义登录和一个哈希表模块取代,如第 30 行所示。 第 33 行至第 40 行显示如何加载自定义登录模块实现,以及它预期的选项(cliendIDclient-secret、用户配置文件 URL、回调 REST API URL,以及令牌端点 URL)。

24. 	
25. 	<library id="customLoginLib">
26. 		<fileset dir="${server.config.dir}/lib" includes="*.jar"/>
27. 	</library>
28. 	
29. 	
30.   	<jaasLoginContextEntry id="system.WEB_INBOUND" loginModuleRef="custom, hashtable" 
31. 	name="system.WEB_INBOUND"/>
32. 	
33. 	<jaasLoginModule className="login.module.CloudLoginModule" controlFlag="REQUIRED" 
34. 		id="custom" libraryRef="customLoginLib">
35.   		<options CLIENT_ID="${client.id}"
36.   			 CLIENT_SECRET="${client.secret}"
37.   			 PROFILE_END_POINT="${profile.end.point}" 
38.   			 SSO_END_POINT_PATH="${sso.end.point}"
39.   			 TOKEN_END_POINT="${token.end.point}"/>
40.   	</jaasLoginModule>

步骤 5. 构建应用程序并将它推送到 Bluemix

构建并运行示例应用程序:

  1. 创建一个 Bluemix Liberty 应用程序:
    1. 登录到 Bluemix 并创建一个 Liberty 应用程序:
    2. 使用 cf 命令将一个 Bluemix SSO 服务添加到您的空间:
      cf create-service SingleSignOn standard sso_service
    3. 使用 cf 命令将该服务绑定到您的应用程序:
      >cf bind-service app_name sso_service
    4. 生成 SSO 客户端配置:
      1. 提供的 Redirect URI 值如下:
        https:<hostname>.mybluemix.net:443/api/cloudlogin/sso_redirect
      2. 记录 clientIdclient-secret
  2. 将该项目复制为一个新的 JazzHub 项目。
  3. 下载示例应用程序:
    1. 设置您的 Eclipse IDE,从 JazzHub 下载此示例应用程序(请参见 设置本地 Eclipse 客户端以使用 Jazz 源控制)。
    2. 将示例项目导入您的 Eclipse IDE。
    3. 下载 Liberty 配置文件运行时(请参见 下载 Liberty 配置文件运行时)。
    4. 下载 cf 命令行程序,将您的应用程序推送到 Bluemix(请参见 命令行界面)。
    5. 下载并配置用于 Liberty 配置文件的 Eclipse 插件(请参见 在 Eclipse 中下载 Liberty 配置文件)。
    6. 创建一个 Liberty 服务器,从 Eclipse 服务器视图部署您的应用程序(例如, Liberty\usr\servers\loginapp)。
  4. 构建您的应用程序:
    1. 在以下 ant 构建脚本中指定 Liberty 运行时位置和 Liberty 服务器位置:
      • CustomLoginModule\build.xml
      • LoginTestApp\build.xml
          <property name="Liberty.location" value="C:\Liberty"/>
          <property name="server.name" value="loginapp"/>
    2. 指定 OAuth clientId 和客户端密码,这是在 SSO 客户端配置步骤中记录在 LoginTestApp\Config.xml 文件中的。
      <server>
        <variable name='client.id' value='YOUR_CLIENT_ID'/>
        <variable name='client.secret' value='YOUR_CLIENT_SECRET'/>
    3. 为了构建项目,执行一个 LoginTestApp/build.xml 的 ant 构建。您会注意到,您的服务器文件夹(例如,Liberty\usr\servers\loginapp)更新了依赖关系和配置。
  5. 将您的应用部署到 Bluemix:
    1. 更新 manifest.yml 文件中的应用名称、主机名和 Liberty 服务器路径(例如,C:\Liberty\usr\servers\loginapp)。
    2. 使用 cf 推送命令将您的应用程序推送到 Bluemix:
      C:\Program Files (x86)\CloudFoundry>cf push -f c:\manifest.yml

在部署完成后,您可访问您的应用程序 (https://<hostname>.mybluemix.net/)。

步骤 6. 运行并测试示例应用程序

测试该示例应用程序:

  1. 导航到您的应用程序 URL (https://<hostname>.mybluemix.net/)。将出现一个包含一个 Web 链接 "JAAS login with Bluemix SSO service" 的页面。
  2. 会显示来自您的身份提供商(比如 IBM 或 Facebook)的登录,提供您的凭据。
  3. SSO 服务将通过电子邮件发送一个安全访问码。
  4. 提供访问码,并在用 Bluemix SSO 成功登录后,您会看到类似下面的消息:
    You are successfully authenticated using Bluemix SSO, Session ID :g-IZDSgUdVZXfH8E3NAf4fw

结束语

在本教程中,我们展示了 Bluemix Liberty 应用程序如何使用 Bluemix SSO 服务实现基于 JAAS 的身份验证,从而添加单点登录功能。您可以将示例程序中的配置作为基础,继续探索更多的好处。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Security, Cloud computing, Java technology
ArticleID=1005144
ArticleTitle=使用 Liberty JAAS 登录模块实现 Bluemix 单点登录
publish-date=05052015