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

Обзор

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

Инструкции по использованию GoogleCredential для авторизации OAuth 2.0 в службах Google см. в разделе Использование OAuth 2.0 с клиентской библиотекой Google API для 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 сохраняет учетные данные с помощью API хранилища данных Google App Engine.
  • MemoryDataStoreFactory «сохраняет» учетные данные в памяти, которая полезна только в качестве краткосрочного хранилища на протяжении всего времени существования процесса.
  • FileDataStoreFactory сохраняет учетные данные в файле.

Пользователи Google App Engine:

AppEngineCredentialStore устарел и удаляется.

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

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

Поток кода авторизации

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

Этот поток реализуется с помощью AuthorizationCodeFlow . Шаги:

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

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

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

Эта библиотека предоставляет вспомогательные классы сервлетов, которые значительно упрощают поток кода авторизации для основных случаев использования. Вы просто предоставляете конкретные подклассы 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 почти идентичен потоку кода авторизации сервлета, за исключением того, что мы можем использовать Java API пользователей Google App Engine. Чтобы включить Users Java API, пользователю необходимо войти в систему; информацию о перенаправлении пользователей на страницу входа, если они еще не вошли в систему, см. в разделе Безопасность и аутентификация (в web.xml).

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

Образец кода:

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);
  ...
}

Клиентский поток на основе браузера

Это типичные шаги клиентского процесса на основе браузера, указанные в спецификации Implicit Grant :

  • Используя 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 .