Статьи

Аутентификация служб Google в App Engine, часть 2

В первой части руководства я описал, как использовать OAuth для доступа / аутентификации для сервисов API Google. К сожалению, как я обнаружил чуть позже, подход, который я использовал, был OAuth 1.0, который, по-видимому, официально признан Google устаревшим в пользу версии 2.0 OAuth. Очевидно, я немного расстроился, обнаружив это, и пообещал, что создам новую запись в блоге с инструкциями по использованию 2.0. Хорошая новость заключается в том, что благодаря поддержке 2.0 Google добавил несколько дополнительных вспомогательных классов, которые облегчают работу, особенно если вы используете Google App Engine, что я и использую для этого урока.

На сайте разработчиков Google есть довольно хорошее описание того, как настроить OAuth 2.0. Тем не менее, оказалось, что создать реальный пример того, как это делается, оказалось непросто, поэтому я решил, что задокументирую то, что узнал.
Сценарий учебника В последнем учебнике созданный мною проект иллюстрировал, как получить доступ к списку файлов Google Docs пользователя. В этом уроке я немного изменил ситуацию и вместо этого использовал API YouTube для отображения списка любимых видео пользователя. Доступ к избранному пользователю требует аутентификации с помощью OAuth, так что это был хороший тест.

Начало работы (проект Eclipse для этого руководства можно найти здесь ).

Первое, что вы должны сделать, это выполнить шаги, описанные в официальных документах Google по использованию OAuth 2.0. Поскольку я создаю веб-приложение, вам нужно следовать разделу в этих документах под названием «Приложения веб-сервера». Кроме того, шаги, о которых я говорил ранее для настройки Google App Engine, все еще актуальны, поэтому я собираюсь перейти прямо к коду и обойти эти шаги настройки.

(ПРИМЕЧАНИЕ: проект Eclipse можно найти здесь — я снова решил не использовать Maven, чтобы упростить процесс для тех, кто не установил его или обладает знаниями в Maven).

Поток приложений очень прост (предполагается, что пользователь впервые):

  1. Когда пользователь получает доступ к веб-приложению (при условии, что вы запускаете его локально по адресу http: // localhost: 8888, используя эмулятор разработчика GAE), он должен сначала войти в Google, используя свою учетную запись gmail или домена Google.
  2. После входа в систему пользователь перенаправляется на простую страницу JSP со ссылкой на свои любимые видео на YouTube.
  3. При нажатии на ссылку сервлет инициирует процесс OAuth, чтобы получить доступ к своей учетной записи YouTube. Первая часть этого процесса перенаправляется на страницу Google, которая запрашивает у них доступ к приложению.
  4. Предполагая, что пользователь отвечает утвердительно, список из 10 избранных будет отображаться со ссылками.
  5. Если они нажмут на ссылку, видео загрузится.

Вот изображение первых трех страниц потока:

И вот последние две страницы (при условии, что пользователь нажимает на данную ссылку):

Хотя этот пример относится только к YouTube, к доступу к любым облачным сервисам на основе Google, таким как Google+, Google Drive, Документы и т. Д., Применяются те же общие принципы, очевидно, что ключевым ключом для создания таких интеграций является OAuth, поэтому давайте посмотрим, как этот процесс работает.

Процесс выполнения OAuth 2.0

Использование OAuth может быть немного сложным для начинающего разработчика, впервые изучающего технологию. Основная предпосылка заключается в том, чтобы позволить пользователям выборочно определять, какие «частные» ресурсы они хотят сделать доступными для внешнего приложения, такого как мы разрабатываем для этого руководства. Используя OAuth, пользователь может избежать необходимости передавать свои учетные данные для входа третьему лицу, но вместо этого может просто предоставить этому третьему лицу доступ к некоторым своим данным.

Чтобы реализовать эту возможность, пользователь переходит к источнику, в котором находятся его личные данные, в данном случае YouTube. Затем они могут разрешить или отклонить запрос на доступ. Если они позволяют это, источник личных данных (YouTube) затем возвращает одноразовый код авторизации в стороннее приложение. Поскольку пользователю довольно утомительно предоставлять доступ каждый раз, когда требуется доступ, существует дополнительный вызов, который можно воспроизвести, чтобы «обменять» его однократное разрешение на более длительный срок. Общий ход веб-приложения, которое мы разрабатываем для этого урока, можно увидеть ниже.

OAuth Flow

Первым делом необходимо определить, вошел ли пользователь в Google, используя свою учетную запись gmail или домен Google. Хотя это и не связано напрямую с процессом OAuth, очень удобно разрешать пользователям входить в систему с использованием своей учетной записи Google, а не требовать от них регистрации на вашем веб-сайте. Это первый выноска, сделанная в Google. Затем, после входа в систему, приложение определяет, имеет ли пользователь локальную настройку учетной записи с предоставленными разрешениями OAuth. Если они входят в первый раз, они не будут. В этом случае инициируется процесс OAuth.

Первый шаг этого процесса — указать провайдеру OAuth, в данном случае Google YouTube, какой «объем» доступа запрашивается. Так как у Google много сервисов, у них много возможностей. Вы можете определить это проще всего, используя их песочницу OAuth 2.0 .

Когда вы запускаете процесс OAuth, вы предоставляете им область (и), к которой вы хотите получить доступ, а также учетные данные клиента OAuth, которые Google предоставил вам (эти шаги на самом деле довольно общие для любого поставщика, который поддерживает OAuth). В наших целях мы стремимся получить доступ к учетной записи пользователя YouTube, поэтому Google предоставляет следующие возможности: https://gdata.youtube.com/.

Если конечный пользователь предоставит доступ к ресурсу, идентифицированному областью, Google отправит обратно код авторизации в приложение. Это захвачено в сервлете. Поскольку возвращаемый код является только одноразовым, он заменяется на более продолжительный токен доступа (и связанный токен обновления). Этот шаг представлен выше действием / блоком под названием «Доступ и запрос на обновление маркера».

Получив токен доступа, приложение может получить доступ к личным данным пользователей, разместив вызов API вместе с токеном. Если все получится, API вернет результаты.

Это не ужасно сложный процесс — он просто включает в себя несколько шагов. Давайте рассмотрим некоторые конкретные детали реализации, начиная с фильтра сервлетов, который определяет, вошел ли пользователь в Google и / или получил ли доступ к OAuth.

AuthorizationFilter

Давайте посмотрим на первые несколько строк AuthorizationFilter (чтобы узнать, как он настроен как фильтр, смотрите файл web.xml).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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]');
  }
 
 }

Первые несколько строк просто приводят общий запрос сервлета и ответ на их соответствующие эквиваленты Http — это необходимо, поскольку мы хотим получить доступ к сеансу HTTP. Следующим шагом является определение наличия CredentialStore в контексте сервлета. Как мы увидим, это используется для хранения учетных данных пользователя, поэтому удобно иметь их в готовом виде в последующих сервлетах. Суть дела начинается, когда мы проверяем, присутствует ли пользователь в сеансе, используя:

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

Если нет, мы получаем их учетные данные для входа в Google с помощью класса Google UserService. Это вспомогательный класс, доступный пользователям GAE для получения идентификатора пользователя Google, адреса электронной почты и псевдонима. Как только мы получим эту информацию от UserService, мы сохраним некоторые детали пользователя в сеансе.

На данный момент мы ничего не сделали с OAuth, но это изменится в следующей серии строк кода:
пытаться {
Utils.getActiveCredential (request, credentialStore);
} catch (NoRefreshTokenException e1) {
// если введен этот блок catch, нам нужно выполнить процесс oauth
LOGGER.info (‘Пользователь не найден — URL авторизации:’ +
e1.getAuthorizationUrl ());
response.sendRedirect (e1.getAuthorizationUrl ());
}

Вспомогательный класс Utils используется для большей части обработки OAuth. В этом случае мы вызываем статический метод getActiveCredential (). Как мы увидим через некоторое время, этот метод будет возвращать исключение NoRefreshTokenException, если для пользователя ранее не были получены учетные данные OAuth. В качестве пользовательского исключения будет возвращено значение URL, которое используется для перенаправления пользователя в Google для получения одобрения OAuth.

Давайте посмотрим на метод getActiveCredential () более подробно, так как именно здесь осуществляется большая часть обработки OAuth.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 из сеанса (они не могут зайти так далеко, пока он не будет заполнен). Затем мы пытаемся получить учетные данные пользователя OAuth (хранящиеся в классе Google с тем же именем) из хранилища CredentialStore, используя статический метод Utils getStoredCredential (). Если для этого пользователя не найдено никаких учетных данных, вызывается метод Utils с именем getAuthorizationUrl (). Этот метод, который показан ниже, используется для создания URL-адреса, на который перенаправлен браузер, на который пользователь запрашивает разрешение на доступ к своим личным данным (URL-адрес обслуживается Google, поскольку он запрашивает у пользователя утверждение).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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();
 }

Как видите, этот метод использует класс (от Google) с именем GoogleAuthorizationCodeRequestUrl. Он создает HTTP-вызов с использованием учетных данных клиента OAuth, предоставленных Google, когда вы регистрируетесь для использования OAuth (эти учетные данные, по совпадению, хранятся в файле client_secrets.json. Другие параметры включают объем запроса OAuth и URL-адрес что пользователь будет перенаправлен обратно в случае одобрения пользователем. Этот URL-адрес является тем, который вы указали при регистрации для доступа Google к OAuth:

Теперь, если пользователь уже предоставил доступ OAuth, метод getActiveCredential () будет вместо этого получать учетные данные из CredentialStore.

Возвращаясь к URL-адресу, который получает результаты учетных данных OAuth, в данном случае http: // localhost: 8888 / authSub, вы, возможно, задаетесь вопросом, как Google может публиковать сообщения на этот внутренний адрес? Ну, это пользовательский браузер, который на самом деле отправляет результаты, так что localhost, в этом случае, решает просто отлично. Давайте посмотрим, что сервлет с именем OAuth2Callback используется для обработки этого обратного вызова (см. Web.xml, чтобы узнать, как выполняется отображение сервлета для authSub).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 = new AuthorizationCodeResponseUrl (fullUrlBuf.toString ()); Google для удобства предоставляет класс AuthorizationCodeResponseUrl для анализа результатов запроса OAuth. Если метод getError () этого класса не равен NULL, это означает, что пользователь отклонил запрос. В случае, если он пуст, что указывает на то, что пользователь утвердил запрос, вызов метода getCode () используется для получения кода одноразовой авторизации. Это значение кода помещается в сеанс пользователя, и когда Utils.getActiveCredential () вызывается после перенаправления на целевой URL пользователя (через фильтр), он будет обменивать этот код авторизации для долгосрочного доступа и обновлять токен, используя звонок:
credential = exchangeCode ((String) request.getSession (). getAttribute (‘code’));
Метод Utils.exchangeCode () показан ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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();
    }
}

В этом методе также используется класс Google с именем GoogleAuthorizationCodeTokenRequest, который используется для вызова Google для обмена одноразовым кодом авторизации OAuth на токен более длительного доступа.
Теперь, когда мы (наконец-то) получили токен доступа, необходимый для API YouTube, мы готовы показать пользователю 10 их избранных видео.


Вызов сервисов API YouTube

Имея токен доступа, мы можем перейти к отображению пользователю списка избранных. Для этого вызывается сервлет FavoritesServlet. Он вызовет API YouTube, проанализирует полученный формат JSON-C в некоторых локальных классах Java через Джексона и затем отправит результаты на страницу JSP для обработки. Вот сервлет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 ().

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
}

Он использует класс Google с именем HttpRequestFactory для создания исходящего HTTP-вызова API на YouTube. Поскольку мы используем GAE, мы ограничены тем, какие классы мы можем использовать для размещения таких запросов. Обратите внимание на строку кода:
url.access_token = accessToken;
Вот где мы используем токен доступа, полученный с помощью процесса OAuth.

Таким образом, несмотря на то, что для правильной работы OAuth потребовалось немало кода, как только он будет готов, вы готовы к рок-н-роллу с вызовом всевозможных сервисов API Google!

Ссылка: аутентификация для служб Google, часть 2 от нашего партнера по JCG Джеффа Дэвиса в блоге Jeff’s SOA Ruminations .