转载

Java 编程中的 OAuth 2.0 客户端,第 3 部分: 认证码授权

概述

OAuth 是一个开放的授权标准,允许客户端代表一个资源所有者获得访问受保护服务器资源的访问权。资源所有者可以是另一个客户端或最终用户。OAuth 还可以帮助最终用户将对其服务器资源的访问权限授权给第三方,而不必共享其凭据,比如用户名和密码。本系列文章遵从 RFC6749 中列出的 OAuth 2.0 授权框架。可以在 Internet Engineering Task Force 的网站上找到 RFC 6749 中列出的完整 OAuth 2.0 授权框架(请参阅 参考资料)。

授权批准

授权批准是一种凭据,可代表资源所有者用来获得访问受保护资源的访问权。客户端使用此凭据获取访问令牌,而且此访问令牌最终将与请求一起发送,以便访问受保护的资源。OAuth 2.0 定义了四种授权类型:

  1. 授权码
  2. 隐式
  3. 资源所有者密码凭据
  4. 客户端凭据

这个文章系列由四部分组成,将会引导您使用上面列出的每种授权类型在 Java™ 编程中实现 OAuth 2.0 客户端。在第 3 部分中,我将解释如何实现认证码授权。本文详细介绍此类授权,并解释示例客户端代码,此代码可用于兼容 OAuth 2.0 的任何服务器接口,以便支持此授权。在本文的最后,您应该对客户端实现有全面的了解,并准备好下载示例客户端代码,自己进行测试。


认证码授权

此授权已针对机密性客户端进行了优化,用于获得访问令牌和刷新令牌。这是一个基于重定向的流程,因此,客户端必须能够与资源所有者的用户代理(通常是 Web 浏览器)进行交互,并且还必须能够(通过重定向)接收来自授权服务器的传入请求。

认证码授权如 图 1 所示。

图 1. 授权码流程
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

图 1 中所示的流程包括以下步骤:

  • (A) 客户端(通常是一个 Web 应用程序)发起流程,将资源所有者的用户代理(通常是 Web 浏览器)定位到授权端点。客户端的请求包括客户端标识符、请求范围、本地状态和一个重定向 URI。在访问获得批准(或拒绝)之后,授权服务器将用户代理(通常是 Web 浏览器)定向回到重定向 URI。
  • (B) 资源所有者通过用户代理对授权服务器进行身份验证,并批准或拒绝客户端的访问请求。
  • (C) 如果资源所有者批准了访问请求,授权服务器将会使用先前(在请求或客户端注册时)提供的重定向 URI 将用户代理(通常是 Web 浏览器)重定向回客户端。重定向 URI 包括客户之前提供的授权码以及所有本地状态。
  • (D) 客户端从授权服务器的令牌端点发出访问令牌请求,其中包括上一步中收到的授权码。在发出请求时,客户端使用客户端凭据与授权服务器进行身份验证。客户端还包括用于获得验证授权码的重定向 URI。
  • (E) 授权服务器对客户端进行身份验证。它验证授权码,并确保所收到的重定向 URI 匹配在步骤 (C) 中用于重定向客户端的 URI。如果有效,授权服务器将会返回访问令牌作为响应,并且如果请求离线访问,可以返回刷新令牌。

授权码请求

对应于步骤 (A) 和 (B) 的授权码请求如 图 1 所示。在步骤 (A) 中,客户端采用 application/x-www-form-urlencoded 格式向授权服务器发出一个请求,如 清单 1 所示。

清单 1. 授权码请求的示例
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1 Host: server.example.com

该请求必须包含以下参数:

  • response_type:必选项。该值必须设置为 code
  • client_id:必选项。客户端 ID。
  • redirect_uri:必选项。用于用户代理重定向。
  • scope:可选项。访问请求的范围。
  • state:可选项。保持请求和回调之间的状态。

在授权服务器验证请求后,服务器将一个 HTTP 重定向代码 302 响应发送回客户端。该响应还将在 http Location 标头中包括一个重定向 URI。在步骤 (B) 中,客户端必须将用户代理(通常是 Web 浏览器)重定向到此 URI。这种重定向 URI 通常是一个登录页面,资源所有者可以使用其凭据进行登录,并批准/撤销客户端的访问请求。

授权码响应

授权码响应该如 图 1 的步骤 (C) 中所示。如果资源所有者批准了访问请求,授权服务器会发出一个授权码。授权服务器将用户代理重定向到步骤 (A) 中作为请求的一部分的重定向 URI,并将授权码包含为重定向 URI 的查询组件的一部分,这里采用的是 application/x-www-form-urlencoded 格式。

URI 参数如下:

  • Code:必选项。由授权服务器生成的授权码。该代码是临时的,并且必须在生成后很快过期。客户不得多次使用授权码。使用相同代码进行的任何进一步请求都应该被授权服务器撤销。授权码被绑定到客户端标识符和重定向 URI。
  • State:必选项。如果客户端的授权码请求中存在 state 参数,此参数必须设置为与从客户端接收的值完全相同。

访问令牌请求

这对应于 图 1 中的步骤 (D)。客户端采用 application/x-www-form-urlencoded 格式向令牌端点(授权服务器)发出一个请求,如 清单 2 所示。

清单 2. 访问令牌请求的示例
POST /token HTTP/1.1 Host: server.example.com Authorization:Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded               grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA              &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom&client_id=c342

访问令牌请求必须设置下列参数:

  • grant_type:必选项。该值必须设置为 authorization_code
  • client_id:必选项。客户端 ID。
  • client_secret:可选项。密码,用于与授权服务器进行身份验证。
  • code:必选项。从服务器收到的授权码。
  • redirect_uri:必选项。在步骤 (A) 中发送的完全一样。

授权服务器验证该代码和重定向 URI 是有效的。在存在机密性客户端的情况下,授权服务器也使用在其请求的主体或 Authorization 标头中传递的客户端凭据来对客户端进行身份验证。

访问令牌响应

这对应于 图 1 中的步骤 (E)。如果访问令牌请求是有效的,而且获得了授权,授权服务器会在一个访问令牌响应中返回访问令牌。成功的响应示例如 清单 3 所示。

清单 3. 成功的访问令牌响应示例
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache  {   "access_token":"2YotnFZFEjr1zCsicMWpAA",   "token_type":"Bearer",   "expires_in":3600,   "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",   "example_parameter":"example_value" }

如果请求无效,或者未经授权,那么授权服务器将会使用代码返回一个相应的错误消息。

刷新访问令牌请求

这是一个可选步骤,如果客户端请求离线访问,并在访问令牌请求中提供一个 refresh_token,则可以此阿勇此步骤。访问令牌是暂时性的,通常在一个小时后到期。访问令牌到期后,客户端需要重复身份验证过程,资源所有者需要进行登录,并提供授权,让客户可以再次发出访问令牌请求。

如果客户需要刷新访问令牌,而资源所有者没有位于浏览器上,无法进行登录和身份验证,则客户端可以采用离线访问。客户端可以在发出第一个授权码请求时请求离线访问(参见步骤 (A))。根据这项计划,除了访问令牌之外,授权服务器还会返回刷新令牌。刷新令牌是一个长寿令牌,不会过期,除非明确由资源所有者撤销。每当访问令牌到期时,客户端可以使用刷新令牌来重新生成一个访问令牌,资源所有者无需登录和授权访问请求。

客户端采用 application/x-www-form-urlencoded 格式向令牌端点(授权服务器)发出一个请求,如 清单 4 所示:

清单 4. 请求令牌端点
POST /token HTTP/1.1 Host: server.example.com Authorization:Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW Content-Type: application/x-www-form-urlencoded  grant_type=refresh_token&refresh_token=tGzv3JOkF0XG5Qx2TlKWIA

请求参数的定义如下:

  • grant_type:必选项。该值必须设置为 refresh_token
  • refresh_token:必选项。这是之前从访问令牌请求获得的令牌。
  • scope:可选项。访问请求的范围。

授权服务器验证刷新令牌并发出一个新的访问令牌。

刷新访问令牌响应

如果请求成功,授权服务器将会返回一个新的访问令牌。成功的响应示例如 清单 5 所示。

清单 5. 刷新访问令牌响应
HTTP/1.1 200 OK Content-Type: application/json;charset=UTF-8 Cache-Control: no-store Pragma: no-cache  {   "access_token":"2YotnFZFEjr1zCsicMWpAA",   "token_type":"Bearer",   "expires_in":3600,   "example_parameter":"example_value" }

如果请求无效,或者未经授权,那么授权服务器将会使用代码返回一个相应的错误消息。


设置

示例 Outh2.0 客户端是一个动态的 Web 项目。您可以从 下载 部分下载包含项目和源代码的 .war 文件。将 .war 文件导入您的 Eclipse 环境。

先决条件

您需要安装 Eclipse IDE for Java EE developers,以便设置开发环境,并导入附加项目。您可以从 Eclipse 下载页面 下载 Eclipse。

您需要 Apache Tomcat JSP/Servlet 容器来运行 OAuth 2.0 Web 客户端。您可以从 Apache Tomcat 下载页面 下载 Tomcat。

依赖关系

客户端代码项目取决于以下 JAR 文件:

  1. commons-codec-1.6.jar
  2. commons-logging-1.1.1.jar
  3. httpclient-4.2.5.jar
  4. httpclient-cache-4.2.5.jar
  5. httpcore-4.2.4.jar
  6. httpmime-4.2.5.jar
  7. json-simple-1.1.1.jar

第 1 至 6 点中提到的 JAR 文件可以在 HttpComponents JAR 文件中找到,您可以从 HttpComponents Downloads 下载页面 下载该文件。可以从 JSON Simple 项目 下载 json-simple-1.1.1.jar 文件。确保这些 jar 文件被复制到 Apache Tomcat 安装目录的 lib 文件夹中。Tomcat 中默认情况下可能已经提供了一些必要的 JAR 文件,所以您只需复制缺少的文件即可。

将 project .war 文件导入 Eclipse

在安装了 Eclipse 和 Apache Tomcat 之后,需要导入来自 下载 部分的 .war 文件。为了导入 war 文件,请遵循 Eclipse 网站的 "导入 Web 压缩 (WAR) 文件" 中列出的这些简单步骤。

在将 .war 文件导入到 Eclipse 时,请确保关联了 Tomcat 服务器与您的项目。您需要选择 Tomcat 版本,并提供 Tomcat 安装根目录的路径,以便完成配置。

您可以在 "创建一个 Tomcat 服务器 " 中了解有关如何关联 Tomcat 服务器与项目的更详细说明。

在成功将 .war 文件导入到 Eclipse 工作区后,您会在项目层次中的 Java Resources/src 下找到源代码。

运行 OAuth 2.0 客户端

在成功导入 .war 文件并使用必要的 JAR 文件设置 Tomcat 之后,您可以运行客户端:右键单击项目名称 OAuth2.0_AuthCode 并选择 Run AsRun on Server

这会将客户端部署到服务器,并在 Eclipse 内部浏览器中加载 index.html 页面。就个人而言,我更喜欢在外部浏览器上与 Web 客户端进行交互。

您可以从任何浏览器导航至以下网址,访问 Web 客户端: http://localhost:8080/OAuth2.0_AuthCode

后续章节会详细解释客户端代码,并告诉您如何使用流行的 OAuth 2.0 兼容的服务器(如 Salesforce、Google 和 IBM)测试这个客户端。


OAuth 2.0 客户端代码

本文中的示例 OAuth 2.0 客户端实现了认证码授权。示例客户端代码是一个 Web 应用程序,而不是一个普通的 Java 项目,后者适用于前面文章中所讨论的授权类型。这是因为,认证码授权流程的目的是满足 Web 应用程序,并针对 Web 浏览器的用户代理进行了优化。

输入参数

使用在项目的 src 文件夹中提供的 Oauth2Client.config 属性文件向客户端提供所需的输入参数。输入参数是:

  • scope:这是一个可选参数。它代表访问请求的范围。由服务器返回的访问令牌只可以访问在 scope 中提到的那些服务。
  • state:这是一个可选参数,用于维持客户端请求和来自授权服务器的重定向响应之间的状态,目的是确保客户端的完整性。
  • grant_type:需要将这个参数设置为 authorization_code,表示认证码授权类型。
  • client_id:注册应用程序时由资源服务器提供的客户端或使用者 ID。
  • client_secret:注册应用程序时由资源服务器提供的客户端或使用者的密码。
  • access_token:令牌端点响应有效的和经过授权的访问令牌请求时所返回的访问令牌。由授权服务器返回的授权代码用于交换访问令牌。
  • refresh_token:令牌端点响应有效的和经过授权的访问令牌请求时所返回的刷新令牌。刷新令牌可以用于刷新过期的访问令牌,无需资源所有者的存在就可以再次进行身份验证。客户端需要明确地向服务器请求刷新令牌。
  • redirect_uri:授权服务器将用户代理作为授权码请求的一部分重定向到该 URI。授权码被发送到这个 URI。
  • authentication_server_url:这表示授权服务器。获取授权码的所有请求都需要被发送到这个 URL。
  • token_endpoint_url:这表示令牌端点。通过刷新令牌请求来获取访问令牌和刷新访问令牌的所有请求都需要发送到这个 URL。
  • resource_server_url:这表示需要联系的资源服务器的 URL,通过将 Authorization 标头中的访问令牌传递给它,访问受保护的资源。
  • approval_prompt_key:授权服务器用于定义授权提示条件的属性名。通常,每个授权服务器(Salesforce、Google、IBM 等)都有一个需要作为授权码请求的一部分传递的自定义属性,用于表示该客户端是否要强制执行每个请求的授权提示。Google 的属性名是 approval_prompt。对于 Salesforce,它是 login consent
  • access_type_key:授权服务器用于定义访问类型条件的属性名。通常,每个授权服务器(Salesforce、Google、IBM 等)都有一个自定义方法,客户端可以通过它传达指令,比如它想要一个刷新令牌和访问令牌作为访问令牌请求的一部分。Google 通过提供 access_type 属性执行此方法。Salesforce 要求您输入 refresh_token 值作为 scope 的一部分。
  • access_type_valueaccess_type_key 属性的值。对于 Google,您需要将 offline 值传递给服务器,以包含刷新令牌和访问令牌。

图 2 显示了示例客户端代码的 index.html 页面。在 Eclipse 中成功配置项目并将其部署到 Tomcat 后,您应该看到这个页面。

图 2. 测试客户端首页
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

客户端暴露了您需要了解的有关 OAuth 2.0 的两种最基本操作。首先,客户端显示了如何从服务器获取一个访问令牌。其次,客户端显示了如何使用现有的访问令牌访问来自服务器的受保护资源。

运行客户端:

  • 首先,在 Oauth2Client.config 文件中输入所有所需的值。
  • 单击 Start Test 来获取访问令牌。一个 HTTP GET 请求被发送到 OAuth2Client servlet,可以在以下 URI 访问它,http://localhost:8080/OAuth2.0_AuthCode/handler
  • 用户界面代码传递下面的查询参数,作为调用 OAuth2Client servlet 的一部分: caller=token (access token request), caller=resource (access protected resource request)

清单 6 中的客户端代码段选自 OAuth2Client 类的 doGet 方法。

清单 6. 节选自示例客户端的 doGet 方法
String caller = request.getParameter(OAuthConstants.CALLER); String code = request.getParameter(OAuthConstants.CODE);    //Load the properties file Properties config = OAuthUtils.getClientConfigProps(OAuthConstants.CONFIG_FILE_PATH);      //Generate the OAuthDetails bean from the config properties file OAuth2Details oauthDetails = OAuthUtils.createOAuthDetails(config);      //Validate Input List<String> invalidProps = OAuthUtils.validateInput(oauthDetails);  if(invalidProps!=null && invalidProps.size() == 0){   //Validation successful       if(OAuthUtils.isValid(caller)){    //Request was sent from web application.    //Check type of request    if(caller.equalsIgnoreCase(OAuthConstants.TOKEN)){     //Request for Access token     oauthDetails.setAccessTokenRequest(true);     String location =            OAuthUtils.getAuthorizationCode(oauthDetails);           //Send redirect to location returned by endpoint     response.sendRedirect(location);     return;   }   else{    //Request for accessing protected resource     if(!OAuthUtils.isValid(oauthDetails.getResourceServerUrl())){      invalidProps.add(OAuthConstants.RESOURCE_SERVER_URL);            }           if(!OAuthUtils.isValid(oauthDetails.getAccessToken())){      if(!OAuthUtils.isValid(oauthDetails.getRefreshToken())){       invalidProps.add(OAuthConstants.REFRESH_TOKEN);      }            }                 if(invalidProps.size() > 0){      sendError(response, invalidProps);      return;     }           Map<String,String> map = OAuthUtils.getProtectedResource(oauthDetails);     response.getWriter().println(new Gson().toJson(map));     return;    }   }   else if(OAuthUtils.isValid(code)){    //Callback from endpoint with code.    Map<String,String> map = OAuthUtils.getAccessToken(oauthDetails, code);    response.getWriter().println(new Gson().toJson(map));    return;   }   else{    //Invalid request/error response    String queryString = request.getQueryString();    String error = "Invalid request";    if(OAuthUtils.isValid(queryString)){     //Endpoint returned an error     error = queryString;    }    response.getWriter().println(error);    return;         }   }   else{    //Input is not valid.Send error    sendError(response, invalidProps);    return;       }

清单 6 的注意事项:

  • 客户端检索查询参数 callercode。如前面所示,由用户界面发送该请求, caller 参数会有一个有效的值;否则,授权服务器发送该请求作为重定向调用的一部分,而且 code 会有一个有效的值。
  • 然后,客户端通过读取 OAuth2Client.config 文件中提供的属性,创建一个 OAuthDetails bean。
  • 然后,验证此 bean 的正确性和完整性。如果发现任何无效属性或缺失的属性,相应于缺失/不正确的属性的错误响应被发送到用户界面。
  • 然后,客户端代码继续验证所请求的操作,并调用相应的操作。

访问令牌请求

清单 7 中的代码演示了如何发出授权代码请求。

清单 7. 授权码请求代码

点击查看代码清单

清单 7. 授权码请求代码

HttpPost post = new HttpPost(oauthDetails.getAuthenticationServerUrl()); String location = null;  String state = oauthDetails.getState();  List<BasicNameValuePair> parametersBody = new ArrayList<BasicNameValuePair>();  parametersBody.add(new BasicNameValuePair(OAuthConstants.RESPONSE_TYPE,   OAuthConstants.CODE));  parametersBody.add(new BasicNameValuePair(OAuthConstants.CLIENT_ID,   oauthDetails.getClientId()));  parametersBody.add(new BasicNameValuePair(OAuthConstants.REDIRECT_URI,   oauthDetails.getRedirectURI()));  if (isValid(oauthDetails.getScope())) {  parametersBody.add(new BasicNameValuePair(OAuthConstants.SCOPE,    oauthDetails.getScope())); }  if (isValid(oauthDetails.getApprovalPromptValue())) {  parametersBody.add(new BasicNameValuePair(    oauthDetails.getApprovalPromptKey(), oauthDetails      .getApprovalPromptValue())); }    if (isValid(oauthDetails.getAccessTypeValue())) {  parametersBody.add(new BasicNameValuePair(    oauthDetails.getAccessTypeKey(), oauthDetails      .getAccessTypeValue())); }  if (isValid(state)) {  parametersBody.add(new BasicNameValuePair(OAuthConstants.STATE,    oauthDetails.getState())); }  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();  System.out.println("Code " + code);  Map<String, String> map = handleURLEncodedResponse(response);   if (OAuthConstants.HTTP_SEND_REDIRECT == code) {   location = getHeader(response.getAllHeaders(),     OAuthConstants.LOCATION_HEADER);   if (location == null) {    System.out      .println("The endpoint did not pass in valid location header for redirect");    throw new RuntimeException(      "The endpoint did not pass in valid location header for redirect");   }  } else {   System.out     .println("Was expecting code 302 from endpoint to indicate redirect.Recieved httpCode "       + code);   throw new RuntimeException(     "Was expecting code 302 from endpoint to indicate redirect.Recieved httpCode "       + code);  }  } catch (ClientProtocolException e) {  // TODO Auto-generated catch block  e.printStackTrace();  throw new RuntimeException(e.getMessage()); } catch (IOException e) {  // TODO Auto-generated catch block  e.printStackTrace();  throw new RuntimeException(e.getMessage()); }  return location;

有关 清单 7 中的代码的注意事项:

  • 代码在一开始就创建一个 HttpPost 方法,它用于发送 URL 编码的参数。
  • response_typeclient_idredirect_uri 是强制性参数,被包括在请求中。
  • 其他可选参数,比如 statescopeapproval_prompt_key/valueaccess_type_key/value,如果配置文件中提供了它们的有效值,则将它们也包括在内。
  • 使用 DefaultHttpClient 向授权服务器发出请求。
  • 授权服务器验证请求参数,并回复 302 HTTP 重定向代码与 Location 标头。
  • 代码识别从授权服务器收到的 302,并从响应标头中检索 Location 标头。
  • Location 标头包含客户端重定向用户代理(Web 浏览器)所需的 URI。该 URI 通常是一个登录提示,让资源所有者登录并向客户端提供授权。
  • 位置 URI 的值被返回给调用方法 (OAuth2Client.doGet())。
  • Oauth2Client.doGet() 方法将响应重定向到位置 URI。
  • 资源所有者的用户代理(Web 浏览器)现在被重定向到登录页面/授权页面,资源所有者需要登录此页面,并向发出请求的客户端提供授权。
  • 资源所有者为客户端提供授权后,授权服务器将会使用在原始授权码请求中传递的重定向 URI 来发送 code

清单 8 中的代码显示了使用在上一步中接收到的 code 来发出最终的访问令牌请求。

清单 8. 示例代码访问令牌请求
HttpPost post = new HttpPost(oauthDetails.getTokenEndpointUrl());   String clientId = oauthDetails.getClientId();   String clientSecret = oauthDetails.getClientSecret();   String scope = oauthDetails.getScope();   Map<String, String> map = new HashMap<String, String>();    List<BasicNameValuePair> parametersBody = new ArrayList<BasicNameValuePair>();    parametersBody.add(new BasicNameValuePair(OAuthConstants.GRANT_TYPE,     oauthDetails.getGrantType()));    parametersBody.add(new BasicNameValuePair(OAuthConstants.CODE,     authorizationCode));    parametersBody.add(new BasicNameValuePair(OAuthConstants.CLIENT_ID,     clientId));    if (isValid(clientSecret)) {    parametersBody.add(new BasicNameValuePair(      OAuthConstants.CLIENT_SECRET, clientSecret));   }    parametersBody.add(new BasicNameValuePair(OAuthConstants.REDIRECT_URI,     oauthDetails.getRedirectURI()));    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();     map = handleResponse(response);    accessToken = map.get(OAuthConstants.ACCESS_TOKEN);    } catch (ClientProtocolException e) {    // TODO Auto-generated catch block    e.printStackTrace();    throw new RuntimeException(e.getMessage());   } catch (IOException e) {    // TODO Auto-generated catch block    e.printStackTrace();    throw new RuntimeException(e.getMessage());   }    return map;

清单 8 的注意事项:

  • 该代码使用 HttpPost 方法向令牌端点发出访问令牌请求。
  • grant_typecodeclient_idredirect_uri 是强制性参数。
  • 如果 client_secret 是有效的,那么它也会包含在请求中。
  • code 的值是先前的请求中由授权服务器返回的代码。
  • 如果请求是有效的,那么令牌端点将会返回访问令牌,如果请求是离线访问,则会返回刷新令牌。
  • 请注意,作为该请求的一部分发送的重定向 URI 需要与作为授权码请求的一部分发送的重定向 URI 完全相同。令牌端点需要验证该重定向 URI 与客户端的应用程序中所指定的 URI 相匹配,其数据由令牌端点提供。

刷新访问令牌

如前所述,访问令牌通常是临时性的,典型的寿命是一个小时。刷新令牌允许客户端自动刷新过期的访问令牌,无需资源所有者登录,并再次对客户端进行身份验证。清单 9 中的代码说明了这一点。

清单 9. 刷新访问令牌的示例代码
String clientId = oauthDetails.getClientId(); String clientSecret = oauthDetails.getClientSecret(); String scope = oauthDetails.getScope(); String refreshToken = oauthDetails.getRefreshToken(); Map<String, String> map = new HashMap<String, String>();    if (!isValid(refreshToken)) {    throw new RuntimeException(      "Please provide valid refresh token in config file");   }    List<BasicNameValuePair> parametersBody = new ArrayList<BasicNameValuePair>();    parametersBody.add(new BasicNameValuePair(OAuthConstants.GRANT_TYPE,     OAuthConstants.REFRESH_TOKEN));    parametersBody.add(new BasicNameValuePair(OAuthConstants.REFRESH_TOKEN,     oauthDetails.getRefreshToken()));    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;   try {    post.setEntity(new UrlEncodedFormEntity(parametersBody, HTTP.UTF_8));     response = client.execute(post);    int code = response.getStatusLine().getStatusCode();     map = handleResponse(response);    } catch (ClientProtocolException e) {    // TODO Auto-generated catch block    e.printStackTrace();    throw new RuntimeException(e.getMessage());   } catch (IOException e) {    // TODO Auto-generated catch block    e.printStackTrace();    throw new RuntimeException(e.getMessage());   }    return map;

清单 9 的注意事项:

  • 该代码使用 HttpPost 方法向令牌端点发出请求。
  • grant_typerefresh_token 参数是强制性的。
  • 如果有效,client_idclient_secretscope 也被包括在内。
  • 如果请求有效,令牌端点会返回一个新的访问令牌。

访问受保护资源

清单 10 中的代码演示了如何使用访问令牌来访问受保护的资源。如果访问令牌过期,则刷新访问令牌,然后重试访问受保护的资源。

清单 10. 访问受保护资源的示例代码

点击查看代码清单

清单 10. 访问受保护资源的示例代码

String resourceURL = oauthDetails.getResourceServerUrl();    Map<String, String> map = new HashMap<String, String>();    HttpGet get = new HttpGet(resourceURL);   get.addHeader(OAuthConstants.AUTHORIZATION,     getAuthorizationHeaderForAccessToken(oauthDetails       .getAccessToken()));   DefaultHttpClient client = new DefaultHttpClient();   HttpResponse response = null;   String accessToken = null;   int code = -1;   try {    response = client.execute(get);    code = response.getStatusLine().getStatusCode();    if (code == OAuthConstants.HTTP_UNAUTHORIZED      || code == OAuthConstants.HTTP_FORBIDDEN) {     // Access token is invalid or expired.Regenerate the access     // token     System.out       .println("Access token is invalid or expired.Refreshing access token....");     map = refreshAccessToken(oauthDetails);     accessToken = map.get(OAuthConstants.ACCESS_TOKEN);      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 refresh access token");     }     }     map = handleResponse(response);    } catch (ClientProtocolException e) {    // TODO Auto-generated catch block    e.printStackTrace();   } catch (IOException e) {    // TODO Auto-generated catch block    e.printStackTrace();   } finally {    get.releaseConnection();   }    return map;

清单 10 的注意事项:

  • 此方法使用从配置文件检索到的值来接受 OauthDetails bean。
  • 顾名思义,这种方法将尝试使用简单的 HttpGet 从资源服务器中检索受保护的资源。
  • 为了向资源服务器进行身份验证,需要将访问令牌作为 Authorization 标头的一部分进行发送。例如,Authorization:Bearer accessTokenValue
  • 该代码创建了一个 DefaultHttpClient,向资源服务器发出一个 get 请求。
  • 如果从资源服务器收到的响应代码是 401 或 403,那么用于身份验证的访问令牌可能已过期或无效。
  • 下一步是重新创建访问令牌(请参阅 清单 9)。
  • 成功重新生成访问令牌之后,该代码将会更新 OauthDetails bean 中的访问令牌值。该代码还会使用新的访问令牌值替换 get 方法中现有的 Authorization 标头。
  • 该代码现在发出对受保护资源的另一个访问请求。
  • 如果访问令牌有效,而且资源服务器的 URL 也是正确的,那么您应该可以在控制台中看到响应内容。

用 Salesforce.com 测试客户端

在 Salesforce.com 注册

Salesforce.com 是一个流行的软件即服务 (SaaS) 应用程序,支持 OAuth 2.0 的认证码授权类型。

在 Salseforce.com 注册:

  1. 单击 Login,然后单击 Sign up for free
  2. 完成注册,获得您的凭据。
  3. 除了用户名和密码以外,您还会收到一个安全令牌。您为了发出访问令牌请求而提供的密码必须是您的密码和安全令牌的串联(例如,password12312123)。
  4. 请参阅 参考资料,从其中一篇有用的文章链接中了解如何在 salesforce.com 中创建一个应用程序。
  5. salesforce.com 使用您在应用程序创建过程中指定的重定向 URI 与您的认证码授权类型进行通信。

用 Salesforce.com 运行示例客户端

现在,您已经在 Salesforce 设置了 OAuth 2.0 兼容的服务器,您可以测试客户端并从服务器检索受保护的信息。

您可以在 Java resources/src 文件夹下找到 Oauth2Client_salesforce.config 文件。这个配置文件是为 salesforce.com 定制的,可以用作一个提供配置值的模板,以便针对 Salesforce.com 进行测试。

如上文所示,在 Tomcat 服务器上运行 Oauth2Client Web 应用程序,进入测试客户端的开始页面,如 图 3 所示。

图 3. Salesforce 测试客户端页面
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

图 3 显示了测试客户端页面。单击 Start Test 获取访问令牌。接下来,您会看到 Salesforce 访问令牌请求的登录提示,如 图 4 所示。

图 4. Salesforce 访问令牌请求(登录提示)
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

图 4 显示了 Salesforce 登录页面,从 Salesforce.com 收到 302 和 Location 标头后,客户端将用户代理(Web浏览器)重定向到该页面。

在登录后,您会看到 Salesforce 访问令牌请求批准提示,如 图 5 所示。

图 5. Salesforce 访问令牌请求(批准提示)
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

图 5 显示资源所有者的批准提示(登录后),向 VarunOAuth2.0 App 授予访问权限,该应用是请求访问资源所有者的数据的示例代码。

Salesforce 访问令牌输出

在 Salesforce 批准提示上对客户端授予访问权限之后,令牌端点的响应中包括访问令牌和刷新令牌,以及其他 Salesforce 特定的数据。您应该在 Eclipse 控制台窗口中看到输出,如 清单 11 所示。

清单 11. Salesforce 访问令牌输出

点击查看代码清单

清单 11. Salesforce 访问令牌输出

********** Response Received **********   id = https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO   issued_at = 1408356835704   scope = full refresh_token   instance_url = https://ap1.salesforce.com   token_type = Bearer   refresh_token = 5Aep8617VFpoP.M.4vPT_5eQXhIJTvFPNyK2GaBz7xFooRQE590MJSZNVqfTXKUqoiZH_yhm_ZpaVsmp   id_token = eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjE5MCJ9.eyJleHAiOjE0MDgzNTY5NTUsInN1YiI6Imh0dHBzOi8vbG9naW4uc2FsZXNmb3JjZS5jb20vaWQvMDBEOTAwMDAwMDBtUWFZRUFVLzAwNTkwMDAwMDAxSENCN0FBTyIsImF0X2hhc2giOiJ3eWJpZ1NRTEtaNjdiYlRxWUJxNEVnIiwiYXVkIjoiM01WRzlZNmRfQnRwNHhwNXhLcHgxcFBtcFZidnlUV09Gd2FWbkxwOFpDYjRZVjBRX2FJcHI2WTZVazd1ZHcxcWtCTFZQcVVFNWY2blU1NW5xTWljMCIsImlzcyI6Imh0dHBzOi8vbG9naW4uc2FsZXNmb3JjZS5jb20iLCJpYXQiOjE0MDgzNTY4MzV9.W-r2-k-creIijmRmZ5qQff-bonjjtynNivJAv5XaSGedGhPavWlQpmn76B9k5vB7TWUDTX6y2NroTuIpBi-F2jrO0dunN1giPfv-YKyrCYrpy75J7NXmnSnDGrKhhYgkcR7x9s9MLMoMD-Vf1wsJr58XU2He-UwfrihfkdJzvLKiWRNQfz1gCUwlNSws70AQSqrBfB6MHpLqE7ogG1M3SOp6B4hbcA8NC_zwnvJwIDmF4t3_rf6VsgPuPjV_t4PphBhbrln7sm4b9OMcRRycc8WcCvgBvNPjI37uImciDYUGIP25NEy5sRM3mEU392YlmR5AoHUqOVOqdO9DS1ULWxBy3Q-Fp1wyKYyWiCrUMXe5QdtmeBmkpzptCKXwAhfxhLBdai4bBFfh8K3If4UP-WeNcpMwNkiRhVElwntlqtCPaSs4BLiZGonpLLgwET-f_Iyxs4BauCNvyWDbme_2it2V5AEHgJoKvuf2oU6hD8-Sit8MsdEdc2ugf-nk96QJt5px3QvChfDIE8B7W5trzXagkvzVcXXJV06zJbDUf-ioz7zDTI4Popkxlb31cQiaLAtz2IxIUtjZAfAcTXJaxi8txjV8glqS8Z61145bUaitXgGmZhZAqeefrqLneyCDD--EPNWDIdQYSPhRPbtFb5Aa89AMDNIePxTNnIWShNs   access_token = 00D90000000mQaY!AQ8AQMgSI4eZOjvKJGVPuWISCkuS1WC0R6gOWpMg57bMwVWGkyS9_Wraa5ooMrjTfaqMmXmXlgParPk1rjn0vH5BxbNRq3W0   signature = o1VBoC/PzvMw08hZGxYvXLiV/SXQirHBl8qOrk1Mu6Q=

从 Salesforce 服务器检索用户信息

现在,您有了访问令牌,可以发出请求,从 Salesforce 访问资源所有者的帐户信息,该服务器要求使用 OAuth 2.0 进行身份验证。

用访问令牌和刷新令牌更新 Oauth2Client.confg 文件。还要使用您想测试的资源服务器 URL 来设置资源服务器 URL 属性。例如,您可以将其设置为由 Salesforce 作为访问令牌响应的一部分返回的 id 字段。这个 id 字段用于访问资源所有者的帐户信息(例如,https://login.salesforce.com/id/00D90000000mQaYEAU/00590000001HCB7AAO)。

刷新该项目,并在 Tomcat 服务器上再次运行该项目。单击 Start Test 来访问受保护的资源。

您的 Eclipse 控制台窗口应该显示类似于 清单 12 的输出。

清单 12. Salesforce 保护的资源输出

点击查看代码清单

清单 12. Salesforce 保护的资源输出

********** Response Received **********   addr_city = null   email_verified = true   locale = en_US   addr_state = Unknown   asserted_user = true   nick_name = vern.ojha1********   id = https://login.salesforce.com/id/00D9000000QaYEAU/005900001HCB7AAO   first_name = varun   timezone = America/Los_Angeles   username = *******   mobile_phone = null   user_id = ************   addr_street = null   status = {"created_date":null,"body":null}   user_type = STANDARD   urls = {"sobjects":"https:////ap1.salesforce.com//services//data//v{version}//sobjects//","feeds":"https:////ap1.salesforce.com//services//data//v{version}//chatter//feeds","users":"https:////ap1.salesforce.com//services//data//v{version}//chatter//users","query":"https:////ap1.salesforce.com//services//data//v{version}//query//","enterprise":"https:////ap1.salesforce.com//services//Soap//c//{version}//00D90000000mQaY","recent":"https:////ap1.salesforce.com//services//data//v{version}//recent//","feed_items":"https:////ap1.salesforce.com//services//data//v{version}//chatter//feed-items","search":"https:////ap1.salesforce.com//services//data//v{version}//search//","partner":"https:////ap1.salesforce.com//services//Soap//u//{version}//00D90000000mQaY","rest":"https:////ap1.salesforce.com//services//data//v{version}//","groups":"https:////ap1.salesforce.com//services//data//v{version}//chatter//groups","metadata":"https:////ap1.salesforce.com//services//Soap//m//{version}//00D90000000mQaY","profile":"https:////ap1.salesforce.com//0000001CB7AAO"}   mobile_phone_verified = false   is_app_installed = true   photos = {"picture":"https:////c.ap1.content.force.com//profilephoto//005//F","thumbnail":"https:////c.ap1.content.force.com//profilephoto//005//T"}   display_name = varun ojha   last_modified_date = 2013-06-04T07:43:42.000+0000   email = **************   addr_country = IN   organization_id = 00D90000000mQaYEAU   last_name = ojha   utcOffset = -28800000   active = true   language = en_US   addr_zip = 560071

如您所见,您可以通过使用 OAuth 2.0 进行身份验证,从 Salesforce 成功访问资源所有者的帐户信息。在配置文件中提供的访问令牌过期后,客户端会在下一个请求中自动重新生成访问令牌,并使用它来检索资源服务器 URL 中提供的受保护资源。


在 Google 注册

Google 使用 OAuth 2.0 对 API 进行身份验证,这些 API 可以用于访问如 GoogleDrive、TaskQueue 和 CloudSQL 等服务。按照以下网址的说明,您可以设置一个应用程序,在 Google 上测试您的 OAuth 2.0 客户端:https://developers.google.com/accounts/docs/OAuth2?hl=ES

用 Google 运行客户端

现在,您已经设置了 OAuth 2.0 兼容的服务器,您可以测试客户端,并从服务器检索受保护的信息。

您可以在 Java resources/src 文件夹下找到 Oauth2Client_Google.config 文件。这个配置文件被定制为与 Google 服务结合使用,而且可以用作提供配置值的模板。

在 Tomcat 服务器上运行 Oauth2Client Web 应用,如前所示。获取访问令牌操作和访问受保护资源操作的流程和输出如 图 6 所示。

图 6. 访问令牌请求(登录提示)
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

图 6 显示了 Google 登录页面,从 Google 收到 302 和 Location 标头后,客户端将用户代理(Web浏览器)重定向到该页面。

图 7 显示了资源所有者的批准提示(登录后),向 Cast Iron App 授予访问权限,该应用请求访问资源所有者的数据。

图 7. 访问令牌请求(批准提示)
Java 编程中的 OAuth 2.0 客户端,第 3    部分: 认证码授权

资源所有者对客户端授予访问权限后,令牌端点的响应中包含访问令牌和刷新令牌。您应该在控制台窗口中看到类似于 清单 13 所示的输出。

清单 13.
********** Response Received **********   expires_in = 3600   token_type = Bearer   refresh_token = 1/TtCxaFlKMRsHeIlxrY-2ZJIO8DcRmQEiQ_2Wxw8   access_token = ya29.ZQDpI-ahF6TMURwAAABqBu-2-U0_lUWfbwh053j3db3PzaNXV4k_k6fc_VT7uQ

从 Google 服务器检索用户信息

现在,您有了访问令牌,可以发出请求,访问 GoogleDrive 信息,这要求使用 OAuth 2.0 进行身份验证。

用访问令牌和刷新令牌更新 Oauth2Client.confg 文件,并使用您希望对其进行测试的资源服务器 URL 来填充资源服务器 url 属性。我要用 https://www.googleapis.com/drive/v2/about 替换它,这是 GoogleDrive 帐户信息 URL。

现在,刷新该项目,并在 Tomcat 服务器上再次运行它。单击 Start Test 来访问受保护的资源。

您应该在控制台窗口中看到类似于 清单 14 的片段。

清单 14.

点击查看代码清单

清单 14.

********** Response Received **********   name = Varun Ojha   features = [{"featureName":"ocr"},{"featureName":"translation","featureRate":0.5}]   quotaBytesTotal = 16106127360   largestChangeId = 1911   rootFolderId = 0ALupA2Opqyp1Uk9PVA   quotaType = LIMITED   quotaBytesByService = [{"bytesUsed":"229","serviceName":"DRIVE"},{"bytesUsed":"1154860956","serviceName":"GMAIL"},{"bytesUsed":"568174452","serviceName":"PHOTOS"}]   permissionId = 01822620516456565194   quotaBytesUsedInTrash = 119332

如您所见,您可以通过使用 OAuth 2.0 进行身份验证,从 GoogleDrive 成功访问资源所有者的帐户信息。

在配置文件中提供的访问令牌过期后,客户端会在下一个请求中自动重新生成访问令牌,并使用重新生成的访问令牌来检索资源服务器 URL 中提供的受保护资源。


用 IBM 端点来测试客户端

客户端示例代码已经用 OAuth 2.0 兼容的 IBM 端点(比如 IBM Websphere® Application Server 和 IBM DataPower)成功完成了测试。®

关于在 Websphere Application Server 上设置 OAuth 2.0 服务器的说明,请参阅 "使用 OAuth:在 WebSphere Application Server 上启用 OAuth 服务提供程序"。

可以采用与在 Salesforce 和 Google 上进行测试的相同方法对 IBM 端点进行测试。


结束语

本教程系列的第 3 部分解释了认证码授权类型的基础知识。本文演示了如何在 Java 代码中编写一个通用的 OAuth 2.0 客户端,以便连接到 OAuth 2.0 兼容的多个端点,从中获取受保护的资源。来自 下载 的示例客户端 Web 项目可以导入到 Eclipse 环境中,您可以将此作为一个起点,创建一个定制的 Oauth 2.0 客户端。


下载

描述名字大小
Sample Java code for Authorization code grant typeOAuth2.0_AuthCode_part_3.war25KB

正文到此结束
Loading...