Статьи

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

В этом посте будет показано, как создать простое Java-приложение Google App Engine (GAE), которое аутентифицируется в Google, а также использует OAuth от Google для авторизации доступа к API-сервисам Google, таким как Google Docs. Кроме того, опираясь на некоторые примеры, уже предоставленные Google, он также покажет, как сохранить данные с помощью App Engine Datastore и Objectify .

Исходный код проекта

Мотивация этого поста состоит в том, что я изо всех сил пытался найти какие-либо примеры, которые действительно связывают эти технологии. Тем не менее, эти технологии действительно представляют собой строительные блоки для многих веб-приложений, которые хотят использовать широкий спектр сервисов API Google.
Для простоты демонстрация просто позволит пользователю войти через домен Google; авторизовать доступ к сервисам Google Docs пользователя; и отобразить список документов Google Docs Word и электронных таблиц пользователя. В этом уроке я делаю несколько предположений об опыте читателя, например, довольно глубокое знакомство с Java.


Обзор потока

Прежде чем мы перейдем прямо к уроку / демонстрации, давайте кратко рассмотрим процесс навигации.

Хотя это может показаться довольно сложным, основной поток можно резюмировать следующим образом:

  1. Пользователь запрашивает доступ к listFiles.jsp (фактически может использоваться любая из страниц JSP).
  2. Проверка, чтобы видеть, вошел ли пользователь в Google. Если нет, они перенаправляются на страницу входа в Google — после входа они возвращаются обратно.
  3. Затем выполняется проверка, чтобы определить, хранится ли пользователь в локальном хранилище данных. Если нет, пользователь добавляется вместе с адресом электронной почты домена Google пользователя.
  4. Затем мы проверяем, предоставил ли пользователь учетные данные OAuth службе API Google Docs. Если нет, инициируется процесс аутентификации OAuth. Как только учетные данные OAuth предоставлены, они сохраняются в локальной таблице пользователя (поэтому нам не нужно спрашивать каждый раз, когда пользователь пытается получить доступ к службам).
  5. Наконец, отображается список таблиц Google Docs или Word.

Этот же подход можно использовать для доступа к другим службам Google, таким как YouTube (например, вы можете отобразить список любимых видео пользователя).

Настройка среды

Для этого урока я использую следующее:

  • Eclipse Indigo Service Release 2 вместе с плагином Google для Eclipse (см. Инструкции по настройке ).
  • Плагин Google GData Java SDK Eclipse версии 1.47.1 (см. Инструкции по настройке ).
  • Google App Engine версия 1.6.5. Некоторые проблемы существуют с более ранними версиями, поэтому я рекомендую убедиться, что вы используете его. Он должен быть установлен автоматически как часть плагина Google для Eclipse.
  • Объективировать версию 3.1. Необходимая библиотека уже установлена ​​в каталог проекта / WEB-INF / lib.

После того, как вы импортировали проект в Eclipse, ваш путь сборки должен выглядеть примерно так:

Настройки App Engine должны выглядеть примерно так:

Вам нужно будет настроить собственное приложение GAE, а также указать свой собственный идентификатор приложения (см. Документы разработчика Google GAE ).
Лучший учебник, который я видел, который описывает, как использовать OAuth для доступа к сервисам API Google, можно найти здесь . Один из наиболее запутанных аспектов, которые я обнаружил, заключался в том, как получить необходимые ключ потребителя и значения секрета потребителя, которые требуются при размещении запроса OAuth. Способ, которым я достиг этого, был:

  1. Создайте приложение GAE с помощью консоли администратора GAE . Вам нужно будет создать свой собственный идентификатор приложения (просто имя для вашего веб-приложения). Получив его, вы обновите свой идентификатор приложения на панели настроек Eclipse App Engine, как показано выше.
  2. Создайте новый домен для приложения. Например, поскольку мой идентификатор приложения был указан выше как tennis-coachrx, я настроил префикс пути целевого URL-адреса следующим образом: http://tennis-coachrx.appspot.com/authSub. Вы увидите, как мы вскоре настроим этот сервлет для получения учетных данных.
  3. Для завершения регистрации домена Google предоставит вам HTML-файл, который вы можете загрузить. Включите этот файл в корневой каталог в каталоге / src / war и загрузите приложение в GAE. Таким образом, когда Google запускает свою проверку, файл будет присутствовать, и он сгенерирует необходимые учетные данные потребителя. Вот скриншот того, как выглядит установка после ее завершения:

Получив ключ потребителя OAuth и секретный ключ OAuth , вы замените следующие значения в файле com.zazarie.shared.Constant:
final static String CONSUMER_KEY = »; final static String CONSUMER_SECRET = »;
Уфф, это казалось большой работой! Однако это разовая сделка, и вам не придется снова с ней суетиться.

Код прохождение

Теперь, когда мы получили эту конфигурацию / настройку OAuth, мы можем углубиться в код. Давайте начнем с рассмотрения структуры каталога war, в которой находятся ваши веб-ресурсы:

ListFiles.jsp — это страница JSP по умолчанию, которая отображается при первом входе в веб-приложение. Давайте теперь посмотрим на файл 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
<?xml version='1.0' encoding='UTF-8'?>
<web-app xmlns:xsi='http:www.w3.org2001XMLSchema-instance'
 xsi:schemaLocation='http:java.sun.comxmlnsjavaee
              http:java.sun.comxmlnsjavaeeweb-app_2_5.xsd'
 version='2.5' xmlns='http:java.sun.comxmlnsjavaee'>
 
 <!-- Filters -->
 <filter>
  <filter-name>AuthorizationFilter<filter-name>
  <filter-class>com.zazarie.server.servlet.filter.AuthorizationFilter<filter-class>
 <filter>
 
 <filter-mapping>
    <filter-name>AuthorizationFilter<filter-name>
  <url-pattern>html*<url-pattern>
 <filter-mapping>
 
 <!-- Servlets -->
 
 <servlet>
  <servlet-name>Step2<servlet-name>
  <servlet-class>com.zazarie.server.servlet.RequestTokenCallbackServlet<servlet-class>
 <servlet>
 
 <servlet-mapping>
  <servlet-name>Step2<servlet-name>
  <url-pattern>authSub<url-pattern>
 <servlet-mapping>
 
 <!-- Default page to serve -->
 <welcome-file-list>
  <welcome-file>htmllistFiles.jsp<welcome-file>
 <welcome-file-list>
 
<web-app>

Фильтр сервлетов с именем AuthorizationFilter вызывается всякий раз, когда запрашивается файл JSP, расположенный в каталоге html. Фильтр, как мы сейчас рассмотрим, отвечает за то, чтобы пользователь вошел в Google, и если это так, то гарантирует, что для этого пользователя были предоставлены учетные данные OAuth (т. Е. Он будет запускать OAuth. процесс удостоверения личности, если требуется).

Имя сервлета шага 2 представляет сервлет, который вызывается Google при предоставлении учетных данных OAuth — воспринимайте его как обратный вызов. Мы рассмотрим это подробнее.

Давайте более подробно рассмотрим AuthorizationFilter.

AuthorizationFilter Deep Dive

В методе doFilter работа выполняется в фильтре сервлетов. Вот реализация:

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
36
37
38
39
40
41
42
@Override
public void doFilter(ServletRequest req, ServletResponse res,
  FilterChain chain) throws IOException, ServletException {
 
 HttpServletRequest request = (HttpServletRequest) req;
 
 HttpServletResponse response = (HttpServletResponse) res;
 
 HttpSession session = request.getSession();
 
 LOGGER.info('Invoking Authorization Filter');
 
 LOGGER.info('Destination URL is: ' + request.getRequestURI());
 
 if (filterConfig == null)
  return;
 
  get the Google user
 AppUser appUser = LoginService.login(request, response);
 
 if (appUser != null) {
  session.setAttribute(Constant.AUTH_USER, appUser);
 }
 
  identify if user has an OAuth accessToken - it not, will set in motion
  oauth procedure
 if (appUser.getCredentials() == null) {
 
   need to save the target URI in session so we can forward to it when
   oauth is completed
  session.setAttribute(Constant.TARGET_URI, request.getRequestURI());
 
  OAuthRequestService.requestOAuth(request, response, session);
  return;
 } else
   store DocService in the session so it can be reused
  session.setAttribute(Constant.DOC_SESSION_ID,
    LoginService.docServiceFactory(appUser));
 
 chain.doFilter(request, response);
 
}

Помимо обычного домашнего хозяйства, основная логика начинается со строки:

AppUser appUser = LoginService.login (запрос, ответ);

Как мы увидим через мгновение, LoginService отвечает за регистрацию пользователя в Google, а также создает пользователя в локальном хранилище данных BigTable. Сохраняя пользователя локально, мы можем затем сохранить учетные данные пользователя OAuth, избавляя пользователя от необходимости предоставлять разрешения каждый раз, когда он получает доступ к ограниченной / отфильтрованной странице.
После того, как LoginService вернул пользователя (объект AppUser), мы сохраняем этот пользовательский объект в сеансе (ПРИМЕЧАНИЕ: чтобы включить сеансы, необходимо включить сеансы в файле appengine-web.xml):
session.setAttribute (Constant.AUTH_USER, appUser);

Затем мы проверяем, связаны ли учетные данные OAuth с этим пользователем:

if (appUser.getCredentials () == null) {

session.setAttribute (Constant.TARGET_URI, request.getRequestURI ());

OAuthRequestService.requestOAuth (запрос, ответ, сеанс);
возвращение;
} еще
session.setAttribute (Constant.DOC_SESSION_ID, LoginService.docServiceFactory (appUser));

Если getCredentials () возвращает ноль, учетные данные OAuth еще не были назначены для пользователя. Это означает, что процесс OAuth должен быть запущен. Поскольку это включает в себя двухэтапный процесс отправки запроса в Google и последующего извлечения результатов с помощью обратного вызова (сервлет Step2, упомянутый выше), нам необходимо сохранить целевой URL, чтобы впоследствии мы могли перенаправить пользователя на него после авторизации. Процесс завершен. Это делается путем сохранения URL-адреса, запрошенного в сеансе, с помощью метода setAttribute.

Затем мы запускаем процесс OAuth, вызывая метод OAuthRequestService.requestOAuth () (подробности обсуждаются ниже).

В случае, если getCredentials () возвращает ненулевое значение, это означает, что у нас уже есть учетные данные пользователя OAuth из их локальной записи AppUser в хранилище данных, и мы просто добавляем их в сеанс, чтобы мы могли использовать их позже.

LoginService Deep Dive

Класс LoginService имеет один основной метод, называемый login, за которым следует группа вспомогательных методов JPA для сохранения или обновления локального пользователя в хранилище данных. Мы сосредоточимся на login (), так как именно здесь находится большая часть бизнес-логики.

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public static AppUser login(HttpServletRequest req, HttpServletResponse res) {
 
 LOGGER.setLevel(Constant.LOG_LEVEL);
 
 LOGGER.info('Initializing LoginService');
 
 String URI = req.getRequestURI();
 
 UserService userService = UserServiceFactory.getUserService();
 
 User user = userService.getCurrentUser();
 
 if (user != null) {
 
  LOGGER.info('User id is: '' + userService.getCurrentUser().getUserId()
    + ''');
 
  String userEmail = userService.getCurrentUser().getEmail();
 
  AppUser appUser = (AppUser) req.getSession().getAttribute(
    Constant.AUTH_USER);
 
  if (appUser == null) {
 
   LOGGER.info('appUser not found in session');
 
    see if it is a new user
   appUser = findUser(userEmail);
 
   if (appUser == null) {
    LOGGER.info('User not found in datastore...creating');
    appUser = addUser(userEmail);
   } else {
    LOGGER.info('User found in datastore...updating');
    appUser = updateUserTimeStamp(appUser);
   }
  } else {
   appUser = updateUserTimeStamp(appUser);
  }
 
  return appUser;
 
 } else {
 
  LOGGER.info('Redirecting user to login page');
 
  try {
   res.sendRedirect(userService.createLoginURL(URI));
  } catch (IOException e) {
   e.printStackTrace();
  }
 
 }
 
 return null;
}

Первое, что мы делаем, это используем класс Google UserService, чтобы определить, вошел ли пользователь в Google:

UserService userService = UserServiceFactory.getUserService ();

Пользователь user = userService.getCurrentUser ();

Если объект User, возвращаемый вызовом Google, имеет значение null, пользователь не входит в Google и перенаправляется на страницу входа с помощью:

res.sendRedirect (userService.createLoginURL (URI));

Если пользователь вошел в систему (т. Е. Не имеет значения NULL), следующее, что мы делаем, это определяем, существует ли этот пользователь в локальном хранилище данных. Это делается путем поиска пользователя с его авторизованным адресом электронной почты Google с appUser = findUser (userEmail). Поскольку JPA / Objectify не является основным предметом обсуждения для этого урока, я не буду вдаваться в подробности того, как работает этот метод. Тем не менее, на веб-сайте Objectify есть несколько отличных учебных пособий / документации.

Если пользователь не существует локально, объект заполняется адресом электронной почты Google и создается с помощью appUser = addUser (userEmail). Если пользователь существует, мы просто обновляем метку времени входа в систему для целей регистрации.

OAuthRequestService Deep Dive

Как вы, возможно, помните ранее, когда пользователь настроен локально, AuthorizationFilter проверит, предоставлены ли ему учетные данные OAuth. Если нет, то вызывается метод OAuthRequestService.requestOAuth (). Это показано ниже:

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
36
37
38
39
40
41
42
43
44
45
public static void requestOAuth(HttpServletRequest req,
  HttpServletResponse res, HttpSession session) {
 
 LOGGER.setLevel(Constant.LOG_LEVEL);
 
 LOGGER.info('Initializing OAuthRequestService');
 
 GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
 oauthParameters.setOAuthConsumerKey(Constant.CONSUMER_KEY);
 oauthParameters.setOAuthConsumerSecret(Constant.CONSUMER_SECRET);
 
  Set the scope.
 oauthParameters.setScope(Constant.GOOGLE_RESOURCE);
 
  Sets the callback URL.
 oauthParameters.setOAuthCallback(Constant.OATH_CALLBACK);
 
 GoogleOAuthHelper oauthHelper = new GoogleOAuthHelper(
   new OAuthHmacSha1Signer());
 
 try {
   Request is still unauthorized at this point
  oauthHelper.getUnauthorizedRequestToken(oauthParameters);
 
   Generate the authorization URL
  String approvalPageUrl = oauthHelper
    .createUserAuthorizationUrl(oauthParameters);
 
  session.setAttribute(Constant.SESSION_OAUTH_TOKEN,
    oauthParameters.getOAuthTokenSecret());
 
  LOGGER.info('Session attributes are: '
    + session.getAttributeNames().hasMoreElements());
 
  res.getWriter().print(
    '<a href='' + approvalPageUrl
      + ''>Request token for the Google Documents Scope');
 
 } catch (OAuthException e) {
  e.printStackTrace();
 } catch (IOException e) {
  e.printStackTrace();
 }
 
}

Чтобы упростить работу с OAuth, в Google есть набор вспомогательных классов Java, которые мы используем. Первое, что нам нужно сделать, это настроить учетные данные потребителей (их получение обсуждалось ранее):

GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters ();
oauthParameters.setOAuthConsumerKey (Constant.CONSUMER_KEY);
oauthParameters.setOAuthConsumerSecret (Constant.CONSUMER_SECRET);

Затем мы устанавливаем область запроса OAuth, используя:
oauthParameters.setScope (Constant.GOOGLE_RESOURCE);

Где Constant.GOOGLE_RESOURCE разрешается в https://docs.google.com/feeds/. Когда вы делаете запрос OAuth, вы указываете объем ресурсов, к которым вы пытаетесь получить доступ. В этом случае мы пытаемся получить доступ к Документам Google (в API GData для каждой службы указан URL области действия). Затем мы устанавливаем, где мы хотим, чтобы Google возвращал ответ.

oauthParameters.setOAuthCallback (Constant.OATH_CALLBACK);

Это значение изменяется независимо от того, работаем ли мы локально в режиме разработки или развернуто в Google App Engine. Вот как значения определяются в интерфейсе Constant:

// Используем для запуска на GAE
// финальная статическая строка OATH_CALLBACK = ‘http://tennis-coachrx.appspot.com/authSub’;

// Используем для локального тестирования
final static String OATH_CALLBACK = ‘http://127.0.0.1:8888/authSub’;

Когда затем подпишите запрос с помощью помощника Google:

GoogleOAuthHelper oauthHelper = новый GoogleOAuthHelper (новый OAuthHmacSha1Signer ());

Затем мы генерируем URL, по которому пользователь будет переходить, чтобы авторизовать доступ к ресурсу. Это генерируется динамически, используя:

StringopyingPageUrl = oauthHelper.createUserAuthorizationUrl (oauthParameters);

Последний шаг — предоставить пользователю ссылку, чтобы он мог перейти по этому URL-адресу для подтверждения запроса. Это делается путем создания некоторого простого HTML-кода, который выводится с помощью res.getWriter (). Print ().

Как только пользователь предоставил доступ, Google перезванивает сервлету, указанному параметром URL / authSub, который соответствует классу сервлета RequestTokenCallbackServlet. Мы рассмотрим это дальше.

RequestTokenCallbackServlet Deep Dive

Сервлет использует вспомогательные классы Google OAuth для генерации требуемого токена доступа и токенов секретного доступа, которые потребуются при последующих вызовах в службу документации Google API. Вот метод doGet, который получает ответный звонок от Google:

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
36
37
38
39
40
41
42
43
44
45
public void doGet(HttpServletRequest req, HttpServletResponse resp)
  throws ServletException, IOException {
 
  Create an instance of GoogleOAuthParameters
 GoogleOAuthParameters oauthParameters = new GoogleOAuthParameters();
 oauthParameters.setOAuthConsumerKey(Constant.CONSUMER_KEY);
 oauthParameters.setOAuthConsumerSecret(Constant.CONSUMER_SECRET);
 
 GoogleOAuthHelper oauthHelper = new GoogleOAuthHelper(
   new OAuthHmacSha1Signer());
 
 String oauthTokenSecret = (String) req.getSession().getAttribute(
   Constant.SESSION_OAUTH_TOKEN);
 
 AppUser appUser = (AppUser) req.getSession().getAttribute(
   Constant.AUTH_USER);
 
 oauthParameters.setOAuthTokenSecret(oauthTokenSecret);
 
 oauthHelper.getOAuthParametersFromCallback(req.getQueryString(),
   oauthParameters);
 
 try {
 
  String accessToken = oauthHelper.getAccessToken(oauthParameters);
 
  String accessTokenSecret = oauthParameters.getOAuthTokenSecret();
 
  appUser = LoginService.getById(appUser.getId());
 
  appUser = LoginService.updateUserCredentials(appUser,
    new OauthCredentials(accessToken, accessTokenSecret));
 
  req.getSession().setAttribute(Constant.DOC_SESSION_ID,
    LoginService.docServiceFactory(appUser));
 
  RequestDispatcher dispatcher = req.getRequestDispatcher((String) req
    .getSession().getAttribute(Constant.TARGET_URI));
  if (dispatcher != null)
   dispatcher.forward(req, resp);
 
 } catch (OAuthException e) {
  e.printStackTrace();
 }
}

Google GoogleOAuthHelper используется для выполнения служебных задач, необходимых для заполнения двух интересующих нас значений:
String accessToken = oauthHelper.getAccessToken (oauthParameters);
String accessTokenSecret = oauthParameters.getOAuthTokenSecret ();

Получив эти значения, мы запрашиваем объект пользователя из хранилища данных и сохраняем эти значения в подклассе AppUser.OauthCredentials:
appUser = LoginService.getById (appUser.getId ());
appUser = LoginService.updateUserCredentials (appUser,
новые OauthCredentials (accessToken, accessTokenSecret));
req.getSession (). SetAttribute (Constant.DOC_SESSION_ID,
LoginService.docServiceFactory (appUser));

Кроме того, вы увидите, что они также сохраняются в сеансе, поэтому у нас они легко доступны при размещении запроса API к Документам Google.

Теперь, когда у нас есть все необходимое, мы просто перенаправляем пользователя обратно на ресурс, который он первоначально запрашивал:

RequestDispatcher dispatcher = req.getRequestDispatcher ((String) req
. .GetSession () GetAttribute (Constant.TARGET_URI));
dispatcher.forward (req, resp);

Теперь, когда они получают доступ к странице JSP со списком своих документов, все должно работать!

Вот демоверсия финального продукта:

Надеюсь, вам понравился урок и демонстрация — с нетерпением ждем ваших комментариев!

Перейдите ко второй части этого урока.

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