استخدام OAuth 2.0 مع مكتبة برامج Google API للغة Java

نظرة عامة

الغرض: يوضّح هذا المستند كيفية استخدام فئة الأداة المساعدة GoogleCredential لإجراء تفويض OAuth 2.0 مع خدمات Google. للحصول على معلومات حول وظائف OAuth 2.0 العامة التي نوفّرها، راجِع بروتوكول OAuth 2.0 ومكتبة عميل Google OAuth للغة Java.

الملخّص: للوصول إلى البيانات المحمية المخزّنة على خدمات Google، استخدِم بروتوكول OAuth 2.0 للتفويض. تتيح Google APIs عمليات OAuth 2.0 لأنواع مختلفة من تطبيقات العميل. في جميع مسارات العمل هذه، يطلب تطبيق العميل رمز دخول مرتبطًا فقط بتطبيق العميل ومالك البيانات المحمية التي يتم الوصول إليها. يرتبط رمز الدخول أيضًا بنطاق محدود يحدّد نوع البيانات التي يمكن لتطبيق العميل الوصول إليها (على سبيل المثال، "إدارة مهامك"). من الأهداف المهمة لبروتوكول OAuth 2.0 توفير وصول آمن وسهل إلى البيانات المحمية، مع تقليل التأثير المحتمل في حال سرقة رمز مميز للوصول.

تم إنشاء حِزم OAuth 2.0 في "مكتبة برامج Google API للغة Java" استنادًا إلى مكتبة برامج Google OAuth 2.0 للغة Java ذات الأغراض العامة.

للحصول على التفاصيل، يُرجى الاطّلاع على مستندات Javadoc للحِزم التالية:

وحدة تحكّم Google API

قبل أن تتمكّن من الوصول إلى واجهات Google API، عليك إعداد مشروع على وحدة تحكّم Google API لأغراض المصادقة والفوترة، سواء كان عميلك تطبيقًا مثبّتًا أو تطبيقًا على الأجهزة الجوّالة أو خادم ويب أو عميلاً يعمل في المتصفّح.

للحصول على تعليمات حول كيفية إعداد بيانات الاعتماد بشكل صحيح، يُرجى الاطّلاع على مساعدة "وحدة تحكّم واجهة برمجة التطبيقات".

بيانات الاعتماد

GoogleCredential

GoogleCredential هي فئة مساعدة آمنة للاستخدام المتزامن لبروتوكول OAuth 2.0، وتتيح الوصول إلى الموارد المحمية باستخدام رمز دخول. على سبيل المثال، إذا كان لديك رمز مميّز للدخول، يمكنك إرسال طلب بالطريقة التالية:

GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Plus plus = new Plus.builder(new NetHttpTransport(),
                             GsonFactory.getDefaultInstance(),
                             credential)
    .setApplicationName("Google-PlusSample/1.0")
    .build();

هوية Google App Engine

تستند بيانات الاعتماد البديلة هذه إلى Google App Engine App Identity Java API. على عكس بيانات الاعتماد التي يطلب تطبيق العميل الوصول إليها من بيانات المستخدم النهائي، تتيح واجهة برمجة التطبيقات App Identity API الوصول إلى بيانات تطبيق العميل نفسه.

استخدِم AppIdentityCredential (من google-api-client-appengine). تكون بيانات الاعتماد هذه أبسط بكثير لأنّ Google App Engine يتولّى جميع التفاصيل. عليك تحديد نطاق OAuth 2.0 الذي تحتاج إليه فقط.

مثال على الرمز مأخوذ من urlshortener-robots-appengine-sample:

static Urlshortener newUrlshortener() {
  AppIdentityCredential credential =
      new AppIdentityCredential(
          Collections.singletonList(UrlshortenerScopes.URLSHORTENER));
  return new Urlshortener.Builder(new UrlFetchTransport(),
                                  GsonFactory.getDefaultInstance(),
                                  credential)
      .build();
}

مخزن البيانات

عادةً ما يكون لرمز الدخول تاريخ انتهاء صلاحية يبلغ ساعة واحدة، وبعد ذلك سيظهر لك خطأ إذا حاولت استخدامه. تتولّى الفئة GoogleCredential تلقائيًا عملية "إعادة تحميل" الرمز المميّز، ما يعني ببساطة الحصول على رمز مميّز جديد للوصول. يتم ذلك من خلال رمز مميّز طويل الأمد لإعادة التحميل، والذي يتم تلقّيه عادةً مع الرمز المميّز للوصول إذا كنت تستخدم المَعلمة access_type=offline أثناء مسار رمز التفويض (راجِع GoogleAuthorizationCodeFlow.Builder.setAccessType(String)).

ستحتاج معظم التطبيقات إلى الاحتفاظ برمز الدخول و/أو رمز التحديث الخاص ببيانات الاعتماد. للحفاظ على رموز الدخول و/أو رموز التحديث الخاصة ببيانات الاعتماد، يمكنك تقديم تنفيذك الخاص لواجهة DataStoreFactory مع StoredCredential، أو يمكنك استخدام أحد عمليات التنفيذ التالية التي توفّرها المكتبة:

  • AppEngineDataStoreFactory: يحتفظ ببيانات الاعتماد باستخدام واجهة برمجة التطبيقات Google App Engine Data Store.
  • MemoryDataStoreFactory: "تحتفظ" ببيانات الاعتماد في الذاكرة، وهو أمر مفيد فقط كتخزين قصير الأجل طوال مدة العملية.
  • FileDataStoreFactory: يحتفظ ببيانات الاعتماد في ملف.

مستخدمو AppEngine: تم إيقاف AppEngineCredentialStore نهائيًا وستتم إزالتها قريبًا. ننصحك باستخدام AppEngineDataStoreFactory مع StoredCredential. إذا كانت لديك بيانات اعتماد مخزّنة بالطريقة القديمة، يمكنك استخدام طريقتَي المساعدة المضافتَين migrateTo(AppEngineDataStoreFactory) أو migrateTo(DataStore) لإجراء عملية النقل.

يمكنك استخدام DataStoreCredentialRefreshListener وإعدادها لبيانات الاعتماد باستخدام GoogleCredential.Builder.addRefreshListener(CredentialRefreshListener)).

مسار رمز التفويض

استخدِم عملية تدفّق رمز التفويض للسماح للمستخدم النهائي بمنح تطبيقك إذنًا بالوصول إلى بياناته المحمية على Google APIs. يتم تحديد البروتوكول الخاص بهذا المسار في منح رمز التفويض.

يتم تنفيذ هذا المسار باستخدام GoogleAuthorizationCodeFlow. الخطوات كالآتي:

  • يسجّل المستخدم النهائي الدخول إلى تطبيقك. عليك ربط هذا المستخدم برقم تعريف مستخدم فريد لتطبيقك.
  • استدعِ AuthorizationCodeFlow.loadCredential(String)) استنادًا إلى رقم تعريف المستخدم للتحقّق مما إذا كانت بيانات اعتماد المستخدِم النهائي معروفة من قبل. إذا كان الأمر كذلك، نكون قد انتهينا.
  • إذا لم يكن كذلك، اتّصِل بالدالة AuthorizationCodeFlow.newAuthorizationUrl() وأعِد توجيه متصفّح المستخدم النهائي إلى صفحة تفويض لمنح تطبيقك إذن الوصول إلى بياناته المحمية.
  • بعد ذلك، سيعيد خادم التفويض من Google توجيه المتصفّح إلى عنوان URL لإعادة التوجيه الذي يحدّده تطبيقك، بالإضافة إلى معلَمة طلب البحث code. استخدِم المَعلمة code لطلب رمز دخول باستخدام AuthorizationCodeFlow.newTokenRequest(String)).
  • استخدِم AuthorizationCodeFlow.createAndStoreCredential(TokenResponse, String)) لتخزين بيانات اعتماد والحصول عليها من أجل الوصول إلى الموارد المحمية.

بدلاً من ذلك، إذا كنت لا تستخدم GoogleAuthorizationCodeFlow، يمكنك استخدام الفئات ذات المستوى الأدنى:

عند إعداد مشروعك في وحدة تحكّم Google API، يمكنك الاختيار من بين بيانات اعتماد مختلفة، وذلك حسب مسار العمل الذي تستخدمه. لمزيد من التفاصيل، يُرجى الاطّلاع على إعداد OAuth 2.0 وسيناريوهات OAuth 2.0. في ما يلي مقتطفات الرموز لكل مسار من المسارات.

تطبيقات خادم الويب

يتم توضيح البروتوكول الخاص بهذا المسار في مقالة استخدام بروتوكول OAuth 2.0 لتطبيقات خادم الويب.

توفّر هذه المكتبة فئات مساعدة خاصة ببرامج servlet لتبسيط عملية الحصول على رمز التفويض بشكل كبير في حالات الاستخدام الأساسية. ما عليك سوى توفير فئات فرعية ملموسة من AbstractAuthorizationCodeServlet وAbstractAuthorizationCodeCallbackServlet (من google-oauth-client-servlet) وإضافتها إلى ملف web.xml. يُرجى العِلم أنّه لا يزال عليك الاهتمام بتسجيل دخول المستخدمين إلى تطبيق الويب واستخراج رقم تعريف المستخدم.

public class CalendarServletSample 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 GoogleAuthorizationCodeFlow.Builder(
        new NetHttpTransport(), GsonFactory.getDefaultInstance(),
        "[[ENTER YOUR CLIENT ID]]", "[[ENTER YOUR CLIENT SECRET]]",
        Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }

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

public class CalendarServletCallbackSample 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 GoogleAuthorizationCodeFlow.Builder(
        new NetHttpTransport(), GsonFactory.getDefaultInstance()
        "[[ENTER YOUR CLIENT ID]]", "[[ENTER YOUR CLIENT SECRET]]",
        Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }

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

تطبيقات Google App Engine

إنّ عملية الحصول على رمز التفويض في App Engine مطابقة تقريبًا لعملية الحصول على رمز التفويض في servlet، باستثناء أنّه يمكننا الاستفادة من واجهة برمجة التطبيقات Users Java API في Google App Engine. يجب أن يكون المستخدم مسجّلاً الدخول كي يتم تفعيل واجهة برمجة تطبيقات Java الخاصة بالمستخدمين. للحصول على معلومات حول إعادة توجيه المستخدمين إلى صفحة تسجيل الدخول إذا لم يكونوا مسجّلين الدخول، راجِع الأمان والمصادقة (في web.xml).

يتمثل الاختلاف الأساسي عن حالة servlet في أنّك توفّر فئات فرعية ملموسة من AbstractAppEngineAuthorizationCodeServlet وAbstractAppEngineAuthorizationCodeCallbackServlet (من google-oauth-client-appengine. وهي توسّع فئات servlet المجردة وتنفّذ الطريقة getUserId نيابةً عنك باستخدام واجهة برمجة تطبيقات Users Java. AppEngineDataStoreFactory (من google-http-client-appengine) هو خيار جيد للاحتفاظ ببيانات الاعتماد باستخدام واجهة برمجة التطبيقات Google App Engine Data Store.

تم أخذ المثال (مع تعديل بسيط) من calendar-appengine-sample:

public class CalendarAppEngineSample extends AbstractAppEngineAuthorizationCodeServlet {

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

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    return Utils.getRedirectUri(req);
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return Utils.newFlow();
  }
}

class Utils {
  static String getRedirectUri(HttpServletRequest req) {
    GenericUrl url = new GenericUrl(req.getRequestURL().toString());
    url.setRawPath("/oauth2callback");
    return url.build();
  }

  static GoogleAuthorizationCodeFlow newFlow() throws IOException {
    return new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY,
        getClientCredential(), Collections.singleton(CalendarScopes.CALENDAR)).setDataStoreFactory(
        DATA_STORE_FACTORY).setAccessType("offline").build();
  }
}

public class OAuth2Callback extends AbstractAppEngineAuthorizationCodeCallbackServlet {

  private static final long serialVersionUID = 1L;

  @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 {
    String nickname = UserServiceFactory.getUserService().getCurrentUser().getNickname();
    resp.getWriter().print("<h3>" + nickname + ", why don't you want to play with me?</h1>");
    resp.setStatus(200);
    resp.addHeader("Content-Type", "text/html");
  }

  @Override
  protected String getRedirectUri(HttpServletRequest req) throws ServletException, IOException {
    return Utils.getRedirectUri(req);
  }

  @Override
  protected AuthorizationCodeFlow initializeFlow() throws IOException {
    return Utils.newFlow();
  }
}

للاطّلاع على نموذج إضافي، يُرجى الرجوع إلى storage-serviceaccount-appengine-sample.

حسابات الخدمة

تتوافق الفئة GoogleCredential أيضًا مع حسابات الخدمة. وعلى عكس بيانات الاعتماد التي يطلب فيها تطبيق العميل الوصول إلى بيانات المستخدم النهائي، تتيح حسابات الخدمة الوصول إلى بيانات تطبيق العميل نفسه. يوقّع تطبيق العميل طلب رمز الدخول باستخدام مفتاح خاص يتم تنزيله من وحدة تحكّم Google API.

مثال على الاستخدام:

HttpTransport httpTransport = new NetHttpTransport();
JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
...
// Build service account credential.

GoogleCredential credential = GoogleCredential.fromStream(new FileInputStream("MyProject-1234.json"))
    .createScoped(Collections.singleton(PlusScopes.PLUS_ME));
// Set up global Plus instance.
plus = new Plus.Builder(httpTransport, jsonFactory, credential)
    .setApplicationName(APPLICATION_NAME).build();
...

للحصول على نموذج إضافي، راجِع storage-serviceaccount-cmdline-sample.

انتحال الهوية

يمكنك أيضًا استخدام مسار حساب الخدمة لانتحال هوية مستخدم في نطاق تملكه. يشبه هذا الإجراء إلى حد كبير إجراء حساب الخدمة المذكور أعلاه، ولكن عليك أيضًا استدعاء GoogleCredential.Builder.setServiceAccountUser(String).

التطبيقات المثبتة

هذا هو مسار رمز المصادقة في سطر الأوامر الموضّح في مقالة استخدام OAuth 2.0 للتطبيقات المثبَّتة.

مثال على الاستخدام:

public static void main(String[] args) {
  try {
    httpTransport = new NetHttpTransport();
    dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR);
    // authorization
    Credential credential = authorize();
    // set up global Plus instance
    plus = new Plus.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(
        APPLICATION_NAME).build();
   // ...
}

private static Credential authorize() throws Exception {
  // load client secrets
  GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
      new InputStreamReader(PlusSample.class.getResourceAsStream("/client_secrets.json")));
  // set up authorization code flow
  GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
      httpTransport, JSON_FACTORY, clientSecrets,
      Collections.singleton(PlusScopes.PLUS_ME)).setDataStoreFactory(
      dataStoreFactory).build();
  // authorize
  return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
}

التطبيقات من جهة العميل

لاستخدام مسار العميل المستند إلى المتصفّح الموضّح في مقالة استخدام OAuth 2.0 للتطبيقات من جهة العميل، عليك عادةً اتّباع الخطوات التالية:

  1. أعِد توجيه المستخدم النهائي في المتصفّح إلى صفحة منح الإذن باستخدام GoogleBrowserClientRequestUrl لمنح تطبيق المتصفّح إذن الوصول إلى البيانات المحمية للمستخدم النهائي.
  2. استخدِم مكتبة عميل Google API للغة JavaScript لمعالجة رمز الدخول المميز الذي تم العثور عليه في جزء عنوان URL في معرّف الموارد المنتظم لإعادة التوجيه المسجّل في وحدة تحكّم Google API.

مثال على الاستخدام لتطبيق ويب:

public void doGet(HttpServletRequest request, HttpServletResponse response)throws IOException {
  String url = new GoogleBrowserClientRequestUrl("812741506391.apps.googleusercontent.com",
      "https://oauth2.example.com/oauthcallback", Arrays.asList(
          "https://www.googleapis.com/auth/userinfo.email",
          "https://www.googleapis.com/auth/userinfo.profile")).setState("/profile").build();
  response.sendRedirect(url);
}

Android

‎@Beta

المكتبة التي يجب استخدامها مع Android:

إذا كنت تطوّر تطبيقات لنظام Android وكانت واجهة Google API التي تريد استخدامها مضمّنة في مكتبة &quot;خدمات Google Play&quot;، استخدِم هذه المكتبة للحصول على أفضل أداء وتجربة. إذا لم تكن واجهة Google API التي تريد استخدامها مع Android جزءًا من مكتبة &quot;خدمات Google Play&quot;، يمكنك استخدام &quot;مكتبة برامج Google API للغة Java&quot; التي تتوافق مع الإصدار 4.0 من نظام التشغيل Android (Ice Cream Sandwich) أو الإصدارات الأحدث، والتي تم توضيحها هنا. تتوفّر ميزة التوافق مع Android في مكتبة برامج Google API للغة Java @Beta.

الخلفية:

بدءًا من الإصدار Eclair (SDK 2.1)، تتم إدارة حسابات المستخدمين على جهاز Android باستخدام "مدير الحسابات". تتولّى حزمة SDK إدارة جميع أذونات تطبيقات Android بشكل مركزي باستخدام AccountManager. عليك تحديد نطاق OAuth 2.0 الذي يحتاج إليه تطبيقك، وسيعرض لك رمزًا مميزًا للوصول يمكنك استخدامه.

يتم تحديد نطاق OAuth 2.0 من خلال المَعلمة authTokenType على النحو التالي: oauth2: بالإضافة إلى النطاق. على سبيل المثال:

oauth2:https://www.googleapis.com/auth/tasks

يحدّد هذا النطاق إذن الوصول للقراءة والكتابة إلى واجهة برمجة تطبيقات "مهام Google". إذا كنت بحاجة إلى نطاقات OAuth 2.0 متعددة، استخدِم قائمة مفصولة بمسافات.

تحتوي بعض واجهات برمجة التطبيقات على مَعلمات authTokenType خاصة تعمل أيضًا. على سبيل المثال، "إدارة مهامك" هو اسم مستعار للمثال authtokenType الموضّح أعلاه.

يجب أيضًا تحديد مفتاح واجهة برمجة التطبيقات من وحدة تحكّم Google API. بخلاف ذلك، لا يوفّر لك الرمز المميّز الذي يمنحه لك AccountManager سوى حصة مجهولة الهوية، وهي عادةً ما تكون منخفضة جدًا. في المقابل، عند تحديد مفتاح API، ستحصل على حصة مجانية أكبر، ويمكنك اختياريًا إعداد الفوترة للاستخدام الذي يتجاوز هذه الحصة.

مقتطف مثال على الرمز مأخوذ من tasks-android-sample:

com.google.api.services.tasks.Tasks service;

@Override
public void onCreate(Bundle savedInstanceState) {
  credential =
      GoogleAccountCredential.usingOAuth2(this, Collections.singleton(TasksScopes.TASKS));
  SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
  credential.setSelectedAccountName(settings.getString(PREF_ACCOUNT_NAME, null));
  service =
      new com.google.api.services.tasks.Tasks.Builder(httpTransport, jsonFactory, credential)
          .setApplicationName("Google-TasksAndroidSample/1.0").build();
}

private void chooseAccount() {
  startActivityForResult(credential.newChooseAccountIntent(), REQUEST_ACCOUNT_PICKER);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  switch (requestCode) {
    case REQUEST_GOOGLE_PLAY_SERVICES:
      if (resultCode == Activity.RESULT_OK) {
        haveGooglePlayServices();
      } else {
        checkGooglePlayServicesAvailable();
      }
      break;
    case REQUEST_AUTHORIZATION:
      if (resultCode == Activity.RESULT_OK) {
        AsyncLoadTasks.run(this);
      } else {
        chooseAccount();
      }
      break;
    case REQUEST_ACCOUNT_PICKER:
      if (resultCode == Activity.RESULT_OK && data != null && data.getExtras() != null) {
        String accountName = data.getExtras().getString(AccountManager.KEY_ACCOUNT_NAME);
        if (accountName != null) {
          credential.setSelectedAccountName(accountName);
          SharedPreferences settings = getPreferences(Context.MODE_PRIVATE);
          SharedPreferences.Editor editor = settings.edit();
          editor.putString(PREF_ACCOUNT_NAME, accountName);
          editor.commit();
          AsyncLoadTasks.run(this);
        }
      }
      break;
  }
}