App Engine中的Google Services身份验证,第2部分

在本教程的第一部分中, 我描述了如何使用OAuth进行Google API服务的访问/身份验证。 不幸的是,正如我稍后发现的那样,我使用的方法是OAuth 1.0,显然现在Google正式弃用了OAuth 1.0,改用OAuth 2.0版本。 显然,发现这一点让我有些沮丧,并承诺我将创建一个新博客条目,其中包含有关如何使用2.0的说明。 好消息是,有了2.0支持,Google添加了一些附加的帮助程序类,这些类使事情变得更加容易,特别是如果您使用的是Google App Engine,这正是我在本教程中使用的。

Google Developers网站现在对如何设置OAuth 2.0进行了很好的描述 但是,事实证明,配置一个真实的示例如何完成它是一个挑战,因此我想记录下我学到的东西。
教程场景在上一个教程中,我创建的项目说明了如何访问用户的Google Docs文件列表。 在本教程中,我做了一些改动,而是使用YouTube的API来显示用户喜欢的视频的列表。 访问用户的收藏夹确实需要使用OAuth进行身份验证,因此这是一个很好的测试。

入门 (可以在此处找到本教程的Eclipse项目)。

您必须做的第一件事是遵循Google官方文档中概述有关使用OAuth 2.0的步骤。 由于我正在创建一个Web应用程序,因此您将需要遵循这些文档中名为“ Web服务器应用程序”的部分。 此外,我之前讨论的用于设置Google App Engine的步骤仍然很重要,因此我将直接跳入代码并跳过这些设置步骤。

(注意:可以在这里找到Eclipse项目-我再次选择不使用Maven,以使那些没有安装Maven或在Maven方面有丰富知识的人保持简单。)

应用程序流程非常简单(假设是首次使用用户):

  1. 用户访问Web应用程序时(假设您正在使用GAE开发人员模拟器在http:// localhost:8888在本地运行该Web应用程序),则他们必须首先使用其gmail或Google域帐户登录到Google。
  2. 登录后,用户将被重定向到一个简单的JSP页面,该页面具有指向其YouTube最喜欢的视频的链接。
  3. 当点击链接时,servlet将启动OAuth流程以获取对其YouTube帐户的访问权限。 此过程的第一部分将重定向到Google Page,该页面会提示他们是否要授予应用程序访问权限。
  4. 假设用户回答肯定,将显示10个带有链接的收藏夹列表。
  5. 如果他们单击链接,将加载视频。

这是前3页流程的描述:

App Engine中的Google Services身份验证,第2部分

这是最后两页(假设用户单击了给定的链接):

App Engine中的Google Services身份验证,第2部分

尽管此示例特定于YouTube,但相同的通用原则也适用于访问任何基于Google的云服务,例如Google +,Google Drive,Docs等。它们创建此类集成的关键推动因素显然是OAuth,因此让我们来看一下该过程有效。

OAuth 2.0处理流程

对于刚开始学习这项技术的新开发人员而言,使用OAuth可能会有些不知所措。 其背后的主要前提是允许用户有选择地确定他们希望外部应用程序可以访问哪些“私有”资源,例如我们正在为本教程开发的资源。 通过使用OAuth,用户可以避免与第三方共享其登录凭据,而可以简单地向该第三方授予访问其某些信息的权限。

为了实现此功能,需要将用户导航到其私人数据所在的源(在本例中为YouTube)。 然后,他们可以允许或拒绝访问请求。 如果他们允许,则私有数据(YouTube)的源将一次性授权代码返回给第三方应用程序。 由于每次需要访问权限时用户都必须授予访问权限是一件很麻烦的事情,因此可以玩一个额外的通话,它将“以旧换新”他们的单次使用授权更长的时间。 我们为本教程开发的Web应用程序的总体流程如下所示。

OAuth流程

App Engine中的Google Services身份验证,第2部分

进行的第一步是确定用户是否已经使用其gmail或Google Domain帐户登录到Google。 尽管不直接与OAuth流程绑定,但使用户能够使用其Google帐户登录非常方便,而不是要求用户登录您的网站。 这是对Google的第一个标注。 然后,一旦登录,应用程序将确定用户是否具有授予OAuth权限的本地帐户设置。 如果他们是第一次登录,则不会。 在这种情况下,将启动OAuth流程。

该过程的第一步是向OAuth提供程序(在本例中为Google YouTube)指定请求访问的“范围”。 由于Google提供了很多服务,因此它们具有很多范围。 您可以使用其OAuth 2.0沙箱轻松确定。

当您启动OAuth流程时,会向他们提供您想要访问的范围,以及Google为您提供的OAuth客户端凭据(这些步骤实际上是支持OAuth的任何提供程序所通用的)。 为了我们的目的,我们正在寻求访问该用户的YouTube帐户的权限,因此Google提供的范围是:https://gdata.youtube.com/。

如果最终用户授予对由范围标识的资源的访问权限,则Google会将授权码发回给应用程序。 这是在servlet中捕获的。 由于返回的代码只是“一次性”代码,因此将其交换为运行时间更长的访问令牌(和相关的刷新令牌)。 上面的步骤由标题为“请求访问和刷新令牌”的活动/框表示。

一旦配备了访问令牌,应用程序便可以通过将API调用与令牌一起放置来访问用户的私有数据。 如果一切顺利,API将返回结果。

这不是一个可怕的复杂过程,它只涉及几个步骤。 让我们看一些具体的实现细节,首先从servlet过滤器开始,该过滤器确定用户是否已经登录Google和/或已授予OAuth访问权限。

授权过滤器

让我们看一下AuthorizationFilter的前几行(要了解如何将其配置为过滤器,请参阅web.xml文件)。

 public void doFilter(ServletRequest req, ServletResponse res,
    FilterChain chain) throws IOException, ServletException {
 
  HttpServletRequest request = (HttpServletRequest) req;
 
  HttpServletResponse response = (HttpServletResponse) res;
 
  HttpSession session = request.getSession();
 
   if not present, add credential store to servlet context
  if (session.getServletContext().getAttribute(Constant.GOOG_CREDENTIAL_STORE) == null) {
   LOGGER.fine('Adding credential store to context ' + credentialStore);
   session.getServletContext().setAttribute(Constant.GOOG_CREDENTIAL_STORE, credentialStore);
  }
 
   if google user isn't in session, add it
  if (session.getAttribute(Constant.AUTH_USER_ID) == null) {
 
   LOGGER.fine('Add user to session');
 
   UserService userService = UserServiceFactory.getUserService();
 
   User user = userService.getCurrentUser();
 
   session.setAttribute(Constant.AUTH_USER_ID, user.getUserId());
   session.setAttribute(Constant.AUTH_USER_NICKNAME, user.getNickname());
 
    if not running on app engine prod, hard-code my email address for testing
   if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Production) {
    session.setAttribute(Constant.AUTH_USER_EMAIL, user.getEmail());
   } else {
    session.setAttribute(Constant.AUTH_USER_EMAIL, '[email protected]');
   }
 
  }

前几行简单地将通用servlet请求和响应转换为它们对应的Http等效项-这是必需的,因为我们要访问HTTP会话。 下一步是确定Servlet上下文中是否存在CredentialStore。 正如我们将看到的,它用于存储用户的凭据,因此在后续的servlet中随时可以使用它很方便。 当我们使用以下命令检查用户是否已经在会话中时,事情就开始了:

if (session.getAttribute(Constant.AUTH_USER_ID) == null) {

如果没有,我们将使用Google的UserService类获取其Google登录凭据。 这是GAE用户可用来获取用户的Google用户ID,电子邮件和昵称的帮助程序类。 从UserService获得此信息后,我们将在会话中存储一些用户的详细信息。

目前,我们还没有对OAuth做任何事情,但是在接下来的代码行系列中会有所改变:
尝试{
Utils.getActiveCredential(request,credentialStore);
} catch(NoRefreshTokenException e1){
//如果输入了该catch块,则需要执行oauth流程
LOGGER.info('未找到用户–授权URL为:'+
e1.getAuthorizationUrl());
response.sendRedirect(e1.getAuthorizationUrl());
}

大多数OAuth处理都使用一个称为Utils的帮助程序类。 在这种情况下,我们将调用静态方法getActiveCredential()。 稍后我们将看到,如果以前没有为用户捕获OAuth凭据,则此方法将返回NoRefreshTokenException。 作为自定义例外,它将返回URL值,该URL值用于将用户重定向到Google以寻求OAuth批准。

让我们更详细地了解getActiveCredential()方法,因为这是管理许多OAuth处理的地方。

 public static Credential getActiveCredential(HttpServletRequest request, CredentialStore credentialStore) throws NoRefreshTokenException {
 
  String userId = (String) request.getSession().getAttribute(Constant.AUTH_USER_ID);
  Credential credential = null;
 
  try {
   if (userId != null) {
    credential = getStoredCredential(userId, credentialStore);
   }
 
   if ((credential == null || credential.getRefreshToken() == null) && request.getParameter('code') != null) {
    credential = exchangeCode(request.getParameter('code'));
 
    LOGGER.fine('Credential access token is: ' + credential.getAccessToken());
    if (credential != null) {
     if (credential.getRefreshToken() != null) {
      credentialStore.store(userId, credential);
     }
    }
   }
 
   if (credential == null || credential.getRefreshToken() == null) {
    String email = (String) request.getSession().getAttribute(Constant.AUTH_USER_EMAIL);
    String authorizationUrl = getAuthorizationUrl(email, request);
    throw new NoRefreshTokenException(authorizationUrl);
   }
 
  } catch (CodeExchangeException e) {
   e.printStackTrace();
  } 
 
  return credential;
 }

我们要做的第一件事是从会话中获取Google userId(如果没有填充,则无法做到这一点)。 接下来,我们尝试使用Utils静态方法getStoredCredential()从CredentialStore获取用户的OAuth凭据(以相同名称存储在Google类中)。 如果找不到该用户的凭据,则调用名为getAuthorizationUrl()的Utils方法。 如下所示,此方法用于构造浏览器重定向到的URL,该URL用于提示用户授权访问其私有数据(该URL由Google提供,因为它将询问用户批准)。

  private static String getAuthorizationUrl(String emailAddress, HttpServletRequest request) {
  GoogleAuthorizationCodeRequestUrl urlBuilder = null;
   try {
    urlBuilder = new GoogleAuthorizationCodeRequestUrl(
     getClientCredential().getWeb().getClientId(),
     Constant.OATH_CALLBACK,
     Constant.SCOPES)
     .setAccessType('offline')
     .setApprovalPrompt('force');
   } catch (IOException e) {
     TODO Auto-generated catch block
    e.printStackTrace();
   }
   urlBuilder.set('state', request.getRequestURI());
 
  if (emailAddress != null) {
    urlBuilder.set('user_id', emailAddress);
  }
 
  return urlBuilder.build();
   }

如您所见,此方法正在使用称为GoogleAuthorizationCodeRequestUrl的类(来自Google)。 它使用您注册使用OAuth时由Google提供的OAuth客户端凭据来构造HTTP调用(巧合的是,这些凭据存储在一个名为client_secrets.json的文件中。其他参数包括OAuth请求的范围和URL如果获得用户的批准,该用户将被重定向回该URL。该URL是您在注册Google的OAuth访问权限时指定的URL:

App Engine中的Google Services身份验证,第2部分

现在,如果用户已经授予OAuth访问权限,则getActiveCredential()方法将改为从CredentialStore获取凭据。

回到接收OAuth凭据结果的URL(在本例中为http:// localhost:8888 / authSub),您可能想知道Google如何发布到该内部专用地址? 好吧,实际上是用户的浏览器发回了结果,因此在这种情况下,本地主机就可以很好地解决问题。 让我们看一下用于处理此回调的名为OAuth2Callback的servlet(有关如何完成authSub的servlet映射的信息,请参见web.xml)。

 public class OAuth2Callback extends HttpServlet {
 
   private static final long serialVersionUID = 1L;
   private final static Logger LOGGER = Logger.getLogger(OAuth2Callback.class.getName());
 
   public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
    
     StringBuffer fullUrlBuf = request.getRequestURL();
     Credential credential = null;
     
     if (request.getQueryString() != null) {
       fullUrlBuf.append('?').append(request.getQueryString());
     }
     
     LOGGER.info('requestURL is: ' + fullUrlBuf);
     
     AuthorizationCodeResponseUrl authResponse = new AuthorizationCodeResponseUrl(fullUrlBuf.toString());
     
      check for user-denied error
     if (authResponse.getError() != null) {
       LOGGER.info('User-denied access');
     } else {
       LOGGER.info('User granted oauth access');
       
       String authCode = authResponse.getCode();
       
       request.getSession().setAttribute('code', authCode);
       
       response.sendRedirect(authResponse.getState());
       
     }
   }
 }

该课程最重要的收获是:
AuthorizationCodeResponseUrl authResponse =新的AuthorizationCodeResponseUrl(fullUrlBuf.toString()); Google提供了AuthorizationCodeResponseUrl类,以方便分析OAuth请求的结果。 如果该类的getError()方法不为null,则意味着用户拒绝了该请求。 如果它为null(表示用户已批准该请求),则使用方法调用getCode()来检索一次性授权码。 此代码值放置在用户的会话中,并且在重定向到用户的目标URL(通过过滤器)后调用Utils.getActiveCredential()时,它将将该授权代码交换为长期访问权限并使用电话:
凭证= exchangeCode((String)request.getSession()。getAttribute('code'));
接下来显示Utils.exchangeCode()方法:

 public static Credential exchangeCode(String authorizationCode)
   throws CodeExchangeException {
    try {
  GoogleTokenResponse response = new GoogleAuthorizationCodeTokenRequest(
  new NetHttpTransport(), Constant.JSON_FACTORY, Utils
   .getClientCredential().getWeb().getClientId(), Utils
   .getClientCredential().getWeb().getClientSecret(),
   authorizationCode, Constant.OATH_CALLBACK).execute();
  return Utils.buildEmptyCredential().setFromTokenResponse(response);
     } catch (IOException e) {
  e.printStackTrace();
  throw new CodeExchangeException();
     }
 }

此方法还使用称为GoogleAuthorizationCodeTokenRequest的Google类,该类用于调用Google以将一次性OAuth授权代码交换为较长时间的访问令牌。
现在,我们已经(最终)获得了YouTube API所需的访问令牌,我们准备向用户显示其视频收藏夹中的10个。


调用YouTube API服务

有了访问令牌,我们现在可以继续向用户显示其收藏夹列表。 为此,调用了一个名为“ FavoritesServlet”的servlet。 它将调用YouTube API,通过Jackson将生成的JSON-C格式解析为一些本地Java类,然后将结果发送到JSP页面进行处理。 这是servlet:

 public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 
  LOGGER.fine('Running FavoritesServlet');
 
  Credential credential = Utils.getStoredCredential((String) request.getSession().getAttribute(Constant.AUTH_USER_ID), 
    (CredentialStore) request.getSession().getServletContext().getAttribute(Constant.GOOG_CREDENTIAL_STORE));
 
  VideoFeed feed = null;
 
   if the request fails, it's likely because access token is expired - we'll refresh
  try {
   LOGGER.fine('Using access token: ' + credential.getAccessToken());
   feed = YouTube.fetchFavs(credential.getAccessToken());
 
  } catch (Exception e) {
 
   LOGGER.fine('Refreshing credentials');
   credential.refreshToken();
   credential = Utils.refreshToken(request, credential);
 
   GoogleCredential googleCredential = Utils.refreshCredentials(credential);
   LOGGER.fine('Using refreshed access token: ' + credential.getAccessToken());
    retry 
   feed = YouTube.fetchFavs(credential.getAccessToken());
  }  
 
  LOGGER.fine('Video feed results are: ' + feed);
 
  request.setAttribute(Constant.VIDEO_FAVS, feed);
 
  RequestDispatcher dispatcher = getServletContext().getRequestDispatcher('htmllistVids.jsp');
 
 dispatcher.forward(request, response);  
 
 }

由于这篇文章主要是关于OAuth流程的,因此我不会过多介绍API调用的放置方式,但是最重要的代码行是:feed = YouTube.fetchFavs(credential.getAccessToken()); feed是VideoFeed的实例。 如您所见,另一个名为YouTube的帮助程序类用于执行繁重的工作。 为了总结一下,我将展示fetchFavs()方法。

   public static VideoFeed fetchFavs(String accessToken) throws IOException, HttpResponseException {
    HttpTransport transport = new NetHttpTransport();
     final JsonFactory jsonFactory = new JacksonFactory();
     HttpRequestFactory factory = transport.createRequestFactory(new HttpRequestInitializer() {
 
       @Override
       public void initialize(HttpRequest request) {
          set the parser
         JsonCParser parser = new JsonCParser(jsonFactory);
         request.addParser(parser);
          set up the Google headers
         GoogleHeaders headers = new GoogleHeaders();
         headers.setApplicationName('YouTube Favorites1.0');
         headers.gdataVersion = '2';
         request.setHeaders(headers);
       }
     });
     
      build the YouTube URL
     YouTubeUrl url = new YouTubeUrl(Constant.GOOGLE_YOUTUBE_FEED);
     url.maxResults = 10;
     url.access_token = accessToken;
      build the HTTP GET request
     HttpRequest request = factory.buildGetRequest(url);
     
     HttpResponse response = request.execute();
      execute the request and the parse video feed
     VideoFeed feed = response.parseAs(VideoFeed.class);
     
     return feed;
   }

它使用称为HttpRequestFactory的Google类构造对YouTube的出站HTTP API调用。 由于我们使用的是GAE,因此我们只能使用哪些类来发出此类请求。 注意代码行:
url.access_token = accessToken;
那就是我们使用通过OAuth流程获取的访问令牌的地方。

因此,虽然需要花费大量代码才能使OAuth内容正常运行,但是一旦到位,您就可以准备调用各种Google API服务来进行滚动!

参考:我们的JCG合作伙伴 Jeff Davis在Jeff's SOA Ruminations博客上为Google Services进行了认证,第2部分

翻译自: https://www.javacodegeeks.com/2012/06/google-services-authentication-in-app_20.html