OAuth 2.0 и клиентская библиотека Google OAuth для Java

Обзор

Назначение. В этом документе описаны общие функции OAuth 2.0, которые предлагает клиентская библиотека Google OAuth для Java. Вы можете использовать эти функции для аутентификации и авторизации в любых интернет-сервисах.

Инструкции по использованию GoogleCredential для авторизации OAuth 2.0 в сервисах Google приведены в статье Использование OAuth 2.0 с клиентской библиотекой API Google для Java.

Краткое описание. OAuth 2.0 – это стандартная спецификация, которая позволяет конечным пользователям безопасно авторизовать клиентское приложение для доступа к защищенным ресурсам на стороне сервера. Кроме того, в спецификации токена на предъявителя OAuth 2.0 объясняется, как получить доступ к этим защищенным ресурсам с помощью токена доступа, предоставленного в процессе авторизации конечного пользователя.

Подробную информацию можно найти в документации Javadoc для следующих пакетов:

Регистрация клиента

Прежде чем использовать клиентскую библиотеку Google OAuth для Java, вам, скорее всего, потребуется зарегистрировать приложение на сервере авторизации, чтобы получить идентификатор и секретный код клиента. Общую информацию об этом процессе можно найти в спецификации регистрации клиента.

Учетные данные и хранилище учетных данных

Credential – это потокобезопасный вспомогательный класс OAuth 2.0 для доступа к защищенным ресурсам с помощью токена доступа. При использовании токена обновления Credential также обновляет токен доступа, когда срок его действия истекает. Например, если у вас уже есть токен доступа, вы можете сделать запрос следующим образом:

  public static HttpResponse executeGet(
      HttpTransport transport, JsonFactory jsonFactory, String accessToken, GenericUrl url)
      throws IOException {
    Credential credential =
        new Credential(BearerToken.authorizationHeaderAccessMethod()).setAccessToken(accessToken);
    HttpRequestFactory requestFactory = transport.createRequestFactory(credential);
    return requestFactory.buildGetRequest(url).execute();
  }

В большинстве приложений необходимо сохранять токен доступа и токен обновления, чтобы в дальнейшем не перенаправлять пользователя на страницу авторизации в браузере. Реализация CredentialStore в этой библиотеке устарела и будет удалена в будущих выпусках. В качестве альтернативы можно использовать интерфейсы DataStoreFactory и DataStore с классом StoredCredential, которые предоставляются клиентской библиотекой Google HTTP-клиента для Java.

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

  • JdoDataStoreFactory сохраняет учетные данные с помощью JDO.
  • AppEngineDataStoreFactory сохраняет учетные данные с помощью Google App Engine Data Store API.
  • MemoryDataStoreFactory сохраняет учетные данные в памяти, что полезно только для краткосрочного хранения в течение времени жизни процесса.
  • FileDataStoreFactory сохраняет учетные данные в файле.

Для пользователей Google App Engine:

AppEngineCredentialStore больше не поддерживается и будет удален.

Рекомендуем использовать AppEngineDataStoreFactory с StoredCredential. Если у вас есть учетные данные, сохраненные старым способом, вы можете использовать добавленные вспомогательные методы migrateTo(AppEngineDataStoreFactory) или migrateTo(DataStore) для переноса.

Используйте DataStoreCredentialRefreshListener и задайте его для учетных данных с помощью GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener).

Обработка кода авторизации

Используйте обработку кода авторизации, чтобы конечный пользователь мог предоставить вашему приложению доступ к своим защищенным данным. Протокол для этого процесса описан в спецификации предоставления кода авторизации.

Этот процесс реализуется с помощью AuthorizationCodeFlow. Выполните следующие действия:

  • Конечный пользователь входит в ваше приложение. Вам нужно связать этого пользователя с идентификатором пользователя, уникальным для вашего приложения.
  • Вызовите метод AuthorizationCodeFlow.loadCredential(String), основанный на идентификаторе пользователя, чтобы проверить, известны ли уже учетные данные пользователя. Если да, то все готово.
  • Если нет, вызовите AuthorizationCodeFlow.newAuthorizationUrl() и перенаправьте браузер конечного пользователя на страницу авторизации, где он сможет предоставить вашему приложению доступ к своим защищенным данным.
  • Затем браузер перенаправляет пользователя на URL переадресации с параметром запроса code, который можно использовать для запроса токена доступа с помощью AuthorizationCodeFlow.newTokenRequest(String).
  • Используйте AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String), чтобы сохранить и получить учетные данные для доступа к защищенным ресурсам.

Если вы не используете AuthorizationCodeFlow, то можете использовать классы более низкого уровня:

  • Используйте DataStore.get(String), чтобы загрузить учетные данные из хранилища на основе идентификатора пользователя.
  • Используйте AuthorizationCodeRequestUrl, чтобы перенаправить браузер на страницу авторизации.
  • Используйте AuthorizationCodeResponseUrl, чтобы обработать ответ авторизации и проанализировать код авторизации.
  • Используйте AuthorizationCodeTokenRequest, чтобы запросить токен доступа и, возможно, токен обновления.
  • Создайте новый объект Credential и сохраните его с помощью метода DataStore.set(String, V).
  • Получите доступ к защищенным ресурсам с помощью учетных данных. Токены доступа с истекшим сроком действия автоматически обновляются с помощью токена обновления, если это применимо. Используйте DataStoreCredentialRefreshListener и задайте его для учетных данных с помощью Credential.Builder.addRefreshListener(CredentialRefreshListener).

Обработка кода авторизации сервлета

Эта библиотека содержит вспомогательные классы сервлетов, которые значительно упрощают обработку кода авторизации в базовых сценариях. Вам нужно лишь предоставить конкретные подклассы AbstractAuthorizationCodeServlet и AbstractAuthorizationCodeCallbackServlet (из google-oauth-client-servlet) и добавить их в файл web.xml. Обратите внимание, что вам по-прежнему нужно позаботиться о входе пользователей в веб-приложение и извлечении идентификатора пользователя.

Пример кода:

public class ServletSample extends AbstractAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

public class ServletCallbackSample extends AbstractAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new NetHttpTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialDataStore(
            StoredCredential.getDefaultDataStore(
                new FileDataStoreFactory(new File("datastoredir"))))
        .build();
  }

  @Override
  protected String getUserId(HttpServletRequest req) throws ServletException, IOException {
    // return user ID
  }
}

Обработка кода авторизации Google App Engine

Обработка кода авторизации в App Engine почти идентична обработке кода авторизации в сервлете, за исключением того, что мы можем использовать Users Java API Google App Engine. Чтобы включить Users Java API, пользователь должен войти в систему. О том, как перенаправлять пользователей на страницу входа, если они ещё не вошли в систему, читайте в разделе Безопасность и аутентификация (в web.xml).

Основное отличие от случая с сервлетом заключается в том, что вы предоставляете конкретные подклассы AbstractAppEngineAuthorizationCodeServlet и AbstractAppEngineAuthorizationCodeCallbackServlet (из google-oauth-client-appengine). Они расширяют абстрактные классы сервлетов и реализуют метод getUserId с помощью Users Java API. AppEngineDataStoreFactory (из клиентской библиотеки HTTP Google для Java) – хороший вариант для сохранения учетных данных с помощью Google App Engine Data Store API.

Пример кода:

public class AppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    // do stuff
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

public class AppEngineCallbackSample extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  @Override
  protected void onSuccess(HttpServletRequest req, HttpServletResponse resp, Credential credential)
      throws ServletException, IOException {
    resp.sendRedirect("/");
  }

  @Override
  protected void onError(
      HttpServletRequest req, HttpServletResponse resp, AuthorizationCodeResponseUrl errorResponse)
      throws ServletException, IOException {
    // handle error
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return new AuthorizationCodeFlow.Builder(BearerToken.authorizationHeaderAccessMethod(),
        new UrlFetchTransport(),
        new JacksonFactory(),
        new GenericUrl("https://server.example.com/token"),
        new BasicAuthentication("s6BhdRkqt3", "7Fjfp0ZBr1KtDRbnfVdmIw"),
        "s6BhdRkqt3",
        "https://server.example.com/authorize").setCredentialStore(
            StoredCredential.getDefaultDataStore(AppEngineDataStoreFactory.getDefaultInstance()))
        .build();
  }
}

Обработка кода авторизации в командной строке

Упрощенный пример кода из dailymotion-cmdline-sample:

/** Authorizes the installed application to access user's protected data. */
private static Credential authorize() throws Exception {
  OAuth2ClientCredentials.errorIfNotSpecified();
  // set up authorization code flow
  AuthorizationCodeFlow flow = new AuthorizationCodeFlow.Builder(BearerToken
      .authorizationHeaderAccessMethod(),
      HTTP_TRANSPORT,
      JSON_FACTORY,
      new GenericUrl(TOKEN_SERVER_URL),
      new ClientParametersAuthentication(
          OAuth2ClientCredentials.API_KEY, OAuth2ClientCredentials.API_SECRET),
      OAuth2ClientCredentials.API_KEY,
      AUTHORIZATION_SERVER_URL).setScopes(Arrays.asList(SCOPE))
      .setDataStoreFactory(DATA_STORE_FACTORY).build();
  // authorize
  LocalServerReceiver receiver = new LocalServerReceiver.Builder().setHost(
      OAuth2ClientCredentials.DOMAIN).setPort(OAuth2ClientCredentials.PORT).build();
  return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
}

private static void run(HttpRequestFactory requestFactory) throws IOException {
  DailyMotionUrl url = new DailyMotionUrl("https://api.dailymotion.com/videos/favorites");
  url.setFields("id,tags,title,url");

  HttpRequest request = requestFactory.buildGetRequest(url);
  VideoFeed videoFeed = request.execute().parseAs(VideoFeed.class);
  ...
}

public static void main(String[] args) {
  ...
  DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
  final Credential credential = authorize();
  HttpRequestFactory requestFactory =
      HTTP_TRANSPORT.createRequestFactory(new HttpRequestInitializer() {
        @Override
        public void initialize(HttpRequest request) throws IOException {
          credential.initialize(request);
          request.setParser(new JsonObjectParser(JSON_FACTORY));
        }
      });
  run(requestFactory);
  ...
}

Процесс работы клиента на основе браузера

Ниже описаны стандартные этапы процесса, выполняемого клиентом на основе браузера, в соответствии со спецификацией неявного предоставления.

  • С помощью BrowserClientRequestUrl перенаправьте браузер конечного пользователя на страницу авторизации, где он сможет предоставить вашему приложению доступ к своим защищенным данным.
  • Используйте приложение JavaScript для обработки токена доступа, найденного во фрагменте URL в URI перенаправления, зарегистрированном на сервере авторизации.

Пример использования для веб-приложения:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
  String url = new BrowserClientRequestUrl(
      "https://server.example.com/authorize", "s6BhdRkqt3").setState("xyz")
      .setRedirectUri("https://client.example.com/cb").build();
  response.sendRedirect(url);
}

Как определить, что срок действия токена доступа истек

Согласно спецификации токена-носителя OAuth 2.0, когда сервер вызывается для доступа к защищенному ресурсу с истекшим токеном доступа, сервер обычно отвечает кодом статуса HTTP 401 Unauthorized, например:

   HTTP/1.1 401 Unauthorized
   WWW-Authenticate: Bearer realm="example",
                     error="invalid_token",
                     error_description="The access token expired"

Однако в спецификации, по-видимому, предусмотрена большая гибкость. Дополнительную информацию вы найдете в документации вашего провайдера OAuth 2.0.

Другой способ – проверить параметр expires_in в ответе на запрос токена доступа. Этот параметр определяет срок действия предоставленного токена доступа в секундах (обычно один час). Однако токен доступа может не истечь в конце этого периода, и сервер может продолжать разрешать доступ. Поэтому мы рекомендуем дождаться кода статуса 401 Unauthorized, а не предполагать, что срок действия токена истек, исходя из прошедшего времени. Вы также можете попробовать обновить токен доступа незадолго до истечения срока его действия. Если сервер токенов недоступен, продолжайте использовать токен доступа, пока не получите ошибку 401. Эта стратегия используется по умолчанию в Credential.

Другой вариант – получать новый токен доступа перед каждым запросом. Однако в этом случае каждый раз будет отправляться дополнительный HTTP-запрос к серверу токенов, что может снизить скорость работы и увеличить нагрузку на сеть. В идеале токен доступа следует хранить в безопасном постоянном хранилище, чтобы минимизировать количество запросов приложения на получение новых токенов доступа. (Но для установленных приложений безопасное хранение – сложная задача.)

Обратите внимание, что токен доступа может стать недействительным не только по истечении срока действия, но и по другим причинам, например если пользователь явно отозвал токен. Поэтому убедитесь, что ваш код обработки ошибок надежен. Если вы обнаружили, что токен больше не действителен, например срок его действия истек или он был отозван, вы должны удалить токен доступа из своего хранилища. Например, на устройствах Android необходимо вызвать метод AccountManager.invalidateAuthToken.