OAuth 是一个开放的授权标准,允许客户端代表一个资源所有者获得访问受保护服务器资源的访问权。资源所有者可以是另一个客户端或最终用户。OAuth 还可以帮助最终用户将对其服务器资源的访问权限授权给第三方,而不必共享其凭据,比如用户名和密码。本系列文章遵循 RFC 6749 中所列出的 OAuth 2.0 授权框架。可以在 Internet Engineering Task Force 的网站上找到 RFC 6749 中列出的完整 OAuth 2.0 授权框架(请参阅 参考资料 )。
授权批准是一种凭据,可代表资源所有者用来访问受保护资源的权限。客户端使用此凭据获取访问令牌。访问令牌最终与请求一起发送,以便访问受保护资源。OAuth 2.0 定义了四种授权类型:
本文是由四部分组成的系列中的第 1 部分,将引导您使用上面列出的每种授权类型在 Java™ 编程中实现 OAuth 2.0 客户端。在第 1 部分中,我会告诉大家如何实现资源所有者密码凭据授权。本文详细介绍各种授权,并解释示例客户端代码,此代码可用于兼容 OAuth 2.0 的任何服务器接口,以支持此授权。在本文的最后,您应该对客户端实现有全面的了解,并准备好下载示例客户端代码,自己进行测试。
当资源所有者对客户端有高度信任时,资源所有者密码凭据授权类型是可行的。此授权类型适合于能够获取资源所有者的用户名和密码的客户端。对于使用 HTTP 基础的现有企业客户端,或者想迁移到 OAuth 的摘要式身份验证,该授权最有用。然后,通过利用现有凭据来生成一个访问令牌,然后就可以实现迁移。
例如,Salesforce.com 添加了 OAuth 2.0 作为对其现有基础架构的一个授权机制。对于现有的客户端转变为这种授权方案,资源所有者密码凭据授权将是最方便的,因为他们只需使用现有的帐户详细信息(比如用户名和密码)来获取访问令牌。
图 1. 资源所有者密码凭据流
在 图 1 中所示的流程包括以下步骤:
对应于第二个步骤的访问令牌请求如 图 1 所示。
客户端对令牌端点(授权服务器)发出请求,采用 application/x-www-form-urlencoded 格式发送以下参数。
grant_type
:必选项。必须将其值设置为 “password” username
:必选项。资源所有者的用户名。 password
:必选项。资源所有者密码。 scope
:可选项。访问请求的范围 如果客户端类型是机密的,或客户端获得了客户端凭据(或者被分配了其他身份验证要求),那么客户端必须向授权服务器进行身份验证。例如,客户端使用传输层安全性发出下列 HTTP 请求。
清单 1. 向授权服务器进行身份验证
POST /token HTTP/1.1 Host: server.example.com Authorization:Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded grant_type=password&username=varun&password=ab32vr
对应于上述步骤 C 的访问令牌响应如 图 1 所示。如果访问令牌请求是有效的,并且获得了授权,那么授权服务器将返回访问令牌和一个可选的刷新令牌。清单 2 显示了一个成功响应的示例。
清单 2. 成功的访问令牌响应
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache { "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
如果请求无效,或者是未经授权的,那么授权服务器将会使用代码返回一个相应的错误消息。
示例 OAuth 2.0 客户端被附加为可导入 Eclipse 环境中的 Java 项目。您需要将第三方依赖关系 JAR 文件下载到 Java 项目中的 lib 文件夹中。
该项目使用以下 JAR 文件:
在前六项中提到的 JAR 文件可以在 Http Components JAR 文件中找到。下载这些文件和 json-simple-1.1.1.jar 文件的链接,请参见 更多下载 。确保已将下面这些可供下载的 JAR 文件复制到 Java 项目的 lib 文件夹。
下载 Eclipse IDE for Java EE developers,以便设置开发环境,并导入附加项目。相关的链接请参见 更多下载 。
此处讨论的 OAuth 2.0 客户端实现了资源所有者密码凭据授权。本系列文章的后续部分将描述其余授权类型,并继续更新客户端代码。
使用在示例客户端代码下载(请参阅 Download )中提供的 Oauth2Client.config 属性文件向客户端提供所需的输入参数。
scope
:这是一个可选参数。它代表访问请求的范围。由服务器返回的访问令牌只可以访问 scope 中提到的服务。 grant_type
:需要将这个参数设置为 "password"
,表示资源所有者密码凭据授权。 username
:用于登录到资源服务器的用户名。 password
:用于登录到资源服务器的密码。 client_id
:注册应用程序时由资源服务器提供的客户端或使用者 ID。 client_secret
:注册应用程序时由资源服务器提供的客户端或使用者的密码。 access_token
:授权服务器响应有效的和经过授权的访问令牌请求时返回的访问令牌。作为该请求的一部分,您的用户名和密码将用于交换访问令牌。 refresh_token
:这是一个可选参数,由授权服务器在响应访问令牌请求时返回。然而,大多数端点(比如 Salesforce、IBMWebSphere® Application Server 和 IBM DataPower)对资源所有者密码凭据授权不返回刷新令牌。因此,我的客户端实现不打算考虑刷新令牌。 authenticatation_server_url
:这表示令牌端点。批准和重新生成访问令牌的所有请求都必须发送到这个 URL。 resource_server_url
:这表示需要联系的资源服务器的 URL,通过将授权标头中的访问令牌传递给它来访问受保护的资源。 清单 3. Oauth2Client 代码
Properties config = OAuthUtils.getClientConfigProps (OAuthConstants.CONFIG_FILE_PATH); String resourceServerUrl = config.getProperty(OAuthConstants.RESOURCE_SERVER_URL); String username = config.getProperty(OAuthConstants.USERNAME); String password = config.getProperty(OAuthConstants.PASSWORD); String grantType = config.getProperty(OAuthConstants.GRANT_TYPE); String authenticationServerUrl = config .getProperty(OAuthConstants.AUTHENTICATION_SERVER_URL); if (!OAuthUtils.isValid(username) || !OAuthUtils.isValid(password) || !OAuthUtils.isValid(authenticationServerUrl) || !OAuthUtils.isValid(grantType)) { System.out .println("Please provide valid values for username, password, authentication server url and grant type"); System.exit(0); } if (!OAuthUtils.isValid(resourceServerUrl)) { // Resource server url is not valid. //Only retrieve the access token System.out.println("Retrieving Access Token"); OAuth2Details oauthDetails = OAuthUtils.createOAuthDetails(config); String accessToken = OAuthUtils.getAccessToken(oauthDetails); System.out .println("Successfully retrieved Access token for Password Grant:" + accessToken); } else { // Response from the resource server must be in Json or //Urlencoded or xml System.out.println("Resource endpoint url:" + resourceServerUrl); System.out.println("Attempting to retrieve protected resource"); OAuthUtils.getProtectedResource(config); }
在 清单 3 中的客户端代码读取 Oauth2Client.config 文件中所提供的输入参数。 username
、 password
、 authentication server url
和 grant type
的有效值是强制性的。如果配置文件中所提供的资源服务器 URL 是有效的,那么客户端会尝试检索该 URL 中提供的受保护资源。否则,客户端只对授权服务器发出访问令牌请求,并取回访问令牌。以下部分说明了负责检索受保护资源和访问令牌的代码。
清单 4 中的代码演示了如何使用访问令牌来访问受保护的资源。
清单 4. 访问受保护资源
String resourceURL = config.getProperty(OAuthConstants.RESOURCE_SERVER_URL); OAuth2Details oauthDetails = createOAuthDetails(config); HttpGet get = new HttpGet(resourceURL); get.addHeader(OAuthConstants.AUTHORIZATION, getAuthorizationHeaderForAccessToken(oauthDetails .getAccessToken())); DefaultHttpClient client = new DefaultHttpClient(); HttpResponse response = null; int code = -1; try { response = client.execute(get); code = response.getStatusLine().getStatusCode(); if (code >= 400) { // Access token is invalid or expired. // Regenerate the access token System.out.println("Access token is invalid or expired.Regenerating access token...."); String accessToken = getAccessToken(oauthDetails); if (isValid(accessToken)) { // update the access token // System.out.println("New access token:" + accessToken); oauthDetails.setAccessToken(accessToken); get.removeHeaders(OAuthConstants.AUTHORIZATION); get.addHeader(OAuthConstants.AUTHORIZATION, getAuthorizationHeaderForAccessToken(oauthDetails .getAccessToken())); get.releaseConnection(); response = client.execute(get); code = response.getStatusLine().getStatusCode(); if (code >= 400) { throw new RuntimeException("Could not access protected resource. Server returned http code:"+ code); } } else { throw new RuntimeException("Could not regenerate access token"); } } handleResponse(response);
OauthDetails
bean。 HttpGet
方法。 DefaultHttpClient
对资源服务器发出一个 get
请求。 OauthDetails
bean 中的访问令牌值。用新的访问令牌值替换 get
方法中现有的 Authorization 标头。 清单 5 中的代码将会处理已过期访问令牌的重新生成。
清单 5. 重新生成过期的访问令牌
HttpPost post = new HttpPost( oauthDetails.getAuthenticationServerUrl()); String clientId = oauthDetails.getClientId(); String clientSecret = oauthDetails.getClientSecret(); String scope = oauthDetails.getScope(); List<BasicNameValuePair> parametersBody = new ArrayList<BasicNameValuePair>(); parametersBody.add(new BasicNameValuePair(OAuthConstants.GRANT_TYPE, oauthDetails.getGrantType())); parametersBody.add(new BasicNameValuePair(OAuthConstants.USERNAME, oauthDetails.getUsername())); parametersBody.add(new BasicNameValuePair(OAuthConstants.PASSWORD, oauthDetails.getPassword())); if (isValid(clientId)) { parametersBody.add(new BasicNameValuePair (OAuthConstants.CLIENT_ID,clientId)); } if (isValid(clientSecret)) { parametersBody.add(new BasicNameValuePair( OAuthConstants.CLIENT_SECRET, clientSecret)); } if (isValid(scope)) { parametersBody.add(new BasicNameValuePair (OAuthConstants.SCOPE,scope)); } DefaultHttpClient client = new DefaultHttpClient(); HttpResponse response = null; String accessToken = null; try { post.setEntity(new UrlEncodedFormEntity(parametersBody, HTTP.UTF_8)); response = client.execute(post); int code = response.getStatusLine().getStatusCode(); if (code >= 400) { System.out.println("Authorization server expects Basic authentication"); // Add Basic Authorization header post.addHeader( OAuthConstants.AUTHORIZATION, getBasicAuthorizationHeader(oauthDetails.getUsername(), oauthDetails.getPassword())); System.out.println("Retry with login credentials"); post.releaseConnection(); response = client.execute(post); code = response.getStatusLine().getStatusCode(); if (code >= 400) { System.out.println("Retry with client credentials"); post.removeHeaders(OAuthConstants.AUTHORIZATION); post.addHeader( OAuthConstants.AUTHORIZATION, getBasicAuthorizationHeader( oauthDetails.getClientId(), oauthDetails.getClientSecret())); post.releaseConnection(); response = client.execute(post); code = response.getStatusLine().getStatusCode(); if (code >= 400) { throw new RuntimeException( "Could not retrieve access token for user:" oauthDetails.getUsername()); } } } Map<String, String> map = handleResponse(response); accessToken = map.get(OAuthConstants.ACCESS_TOKEN); } catch (ClientProtocolException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return accessToken;
HttpPost
请求,并获得身份验证服务器的 URL。 Post
请求以 URL 编码参数的形式发送 username
、 password
,以及可选的 scope
,将它们作为有效载荷的一部分。 client_id
和 client_secret
作为此请求有效载荷的一部分。 client_id
、 client_secret
和 scope
的值不为空,那么它们也将作为有效载荷的一部分被发送。 在本节中,我将讨论如何建立一个 OAuth 2.0 兼容的端点并用它来测试客户端。
Salesforce.com 对于资源所有者密码凭据授权是一个很好的用例。如果用户有登录 Salesforce.com 的凭据,并希望自己的客户端转变为 OAuth 2.0 身份验证,那么他需要做的就是在 Salesforce 注册自己的应用程序,以获得客户端凭据。现在可以用这些客户端凭据以及他现有的登录凭据从授权服务器中获得访问令牌。
现在,您已经完成了 Salesforce.com 中的注册,您可以测试客户端并从服务器检索受保护的信息。
username
、 password
(追加安全令牌)、 client_id
、 client_secret
和 authorization server URL
的值。 您应该在控制台窗口看到下面的输出。
Retrieving Access Token encodedBytes dmVybi5vamhhQGdtYWlsL....... ********** Response Received ********** instance_url = https://ap1.salesforce.com issued_at = 1380106995639 signature = LtMjTrmoBbvVfZ6+qT5Un1UioHaV9KIOK7ayQTmJzCg= id = https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO access_token = 00D90000000mQaY!AQ8AQEn0rLDMvxrP9WgY3Blc....... Successfully retrieved Access token for Password Grant:00D90000000mQaY!AQ8AQEn0rLDMvxrP9WgY3Bl......
现在,您已经有了访问令牌和 ID,可以向 Salesforce.com 发出请求,通过使用 OAuth 2.0 进行身份验证来访问您的帐户信息。
id
值来填充资源服务器 URL 属性。 您应该在控制台窗口看到类似于下面的输出。
清单 6. 输出
Resource endpoint URL: https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO Attempting to retrieve protected resource ********** Response Received ********** photos = {"thumbnail":"https:////c.ap1.content.force.com//profilephoto//005//T","picture":"https:/// /c.ap1.content.force.com//profilephoto//005//F"} urls = {"enterprise":"https:////ap1.salesforce.com//services//Soap//c//{version}//00D90000000mQaY","sobjects": "https:////ap1.salesforce.com//services//data//v{version}//sobjects//","partner":"https:/// /ap1.salesforce.com//services//Soap//u//{version}//00D90000000mQaY","search":"https:/// /ap1.salesforce.com//services//data//v{version}//search//","query":"https:////ap1.salesforce.com/ /services//data//v{version}//query//","users":"https:////ap1.salesforce.com//services//data//v{version}/ /chatter//users","profile":"https:////ap1.salesforce.com//00590000001HCB7AAO","metadata":"https:/// /ap1.salesforce.com//services//Soap//m//{version}//00D90000000mQaY","rest":"https:////ap1.salesforce.com/ /services//data//v{version}//","groups":"https:////ap1.salesforce.com//services//data//v{version}/ /chatter//groups","feeds":"https:////ap1.salesforce.com//services//data//v{version}//chatter//feeds", "recent":"https:////ap1.salesforce.com//services//data//v{version}//recent//","feed_items":"https:// //ap1.salesforce.com//services//data//v{version}//chatter//feed-items"} asserted_user = true active = true organization_id = 00D90000000mQaYEAU nick_name = vern.ojha1.... display_name = varun ojha user_type = STANDARD user_id = *********** status = {"body":null,"created_date":null} last_name = ojha username = vern.ojha..... utcOffset = -28800000 language = en_US locale = en_US first_name = varun last_modified_date = 2013-06-04T07:43:42.000+0000 id = https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO email = vern.ojha@gmail.com
如您所见,您可以通过使用 OAuth 2.0 进行身份验证,成功获取用户信息。在配置文件中提供的访问令牌过期后,客户端将会自动重新生成访问令牌,并使用它来检索在资源服务器 URL 中提供的受保护资源。
客户端也已经成功通过 OAuth 2.0 兼容的 IBM 端点的测试,即 IBM WebSphere Application Server 和 IBM DataPower。请参阅 参考资料 的链接,“使用 OAuth:在 WebSphere Application Server 中启用 OAuth 服务提供程序”,这是一个非常好的资源,介绍了如何在 WebSphere Application Server 上设置 OAuth 2.0。
在您的应用服务器上设置了 OAuth 2.0 后,客户端所需的输入与 Salesforce.com 演示中的相同。
本文是该系列文章的第一部分,阐述了资源所有者密码凭据授权的基础知识。本文演示了如何在 Java 编程中编写一个通用的 OAuth 2.0 客户端,以连接到 OAuth 2.0 兼容的多个端点,并从中获取受保护的资源。示例客户端被附加为一个 Java 项目,以使您能够迅速导入项目到 Eclipse 工作区,并开始测试。在本系列文章的后续部分中,我将介绍在 OAuth 2.0 授权框架中列出的其余三种授权类型。在后续文章中,客户端代码将会获得更新,以反映这些授权类型及特性,比如发布到某个资源服务器和处理 SSL。
描述 | 名字 | 大小 |
---|---|---|
示例客户端代码 | OAuth20.zip | 19KB |