OAuth 2.0 ومكتبة برامج Google OAuth للغة Java

نظرة عامة

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

للحصول على تعليمات عن استخدام GoogleCredential لإجراء تفويض OAuth 2.0 مع خدمات Google، يُرجى الاطّلاع على استخدام OAuth 2.0 مع مكتبة برامج Google API للغة Java.

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

للاطلاع على التفاصيل، راجع وثائق JavaDoc حول الحزم التالية:

تسجيل العملاء

قبل استخدام مكتبة عملاء Google OAuth للغة Java، قد تحتاج إلى تسجيل تطبيقك باستخدام خادم تفويض لتلقّي معرِّف العميل وسر العميل. (للحصول على معلومات عامة بشأن هذه العملية، راجع مواصفات تسجيل العميل.)

متجر بيانات الاعتماد وبيانات الاعتماد

بيانات الاعتماد هي فئة مساعد 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:

تم إيقاف AppEngineCredentialStore نهائيًا وجارٍ إزالته.

نقترح عليك استخدام AppEngineDataStoreFactory مع StoredCredential. إذا كانت لديك بيانات اعتماد مخزَّنة بالطريقة القديمة، يمكنك استخدام طرق المساعد المُضافة MigrateTo(AppEngineDataStorefactor) أو MigrateTo(DataStore) لنقل البيانات.

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

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

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

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

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

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

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

توفر هذه المكتبة فئات مساعدة servlet لتبسيط تدفق كود التفويض لحالات الاستخدام الأساسية بشكل كبير. ما عليك سوى توفير فئتَين فرعيتَين ملموسة من 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 مطابق تقريبًا لتدفق رمز التفويض في servlet، إلا أنه يمكننا الاستفادة من واجهة برمجة تطبيقات Java للمستخدمين في Google App Engine. على المستخدم تسجيل الدخول لتفعيل واجهة برمجة تطبيقات Java للمستخدمين. وللحصول على معلومات حول إعادة توجيه المستخدمين إلى صفحة تسجيل دخول إذا لم يسبق لهم تسجيل الدخول، يمكنك الاطّلاع على صفحة الأمان والمصادقة (في web.xml).

يكمن الاختلاف الأساسي عن حالة servlet في أنّك توفر فئتين فرعيتين ملموستين من AbstractAppEngineAuthorizationCodeServlet وAbstractAppEngineAuthorizationCodeCallbackServlet (من google-oauth-client-appengine). وتعمل هذه البرامج على توسيع فئات servlet المجردة وتنفيذ طريقة getUserId بالنيابة عنك باستخدام واجهة برمجة التطبيقات Users Java API. AppEngineDataStoreFactory (من Google HTTP Client Library for 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. وهذه هي الاستراتيجية المستخدمة تلقائيًا في بيانات الاعتماد.

ويتوفّر خيار آخر، ألا وهو الحصول على رمز دخول جديد قبل كل طلب، ولكن هذا الأمر يتطلب طلب HTTP إضافيًا إلى خادم الرمز المميّز في كل مرة، لذلك من المحتمل أن يكون الخيار سيئًا من حيث السرعة واستخدام الشبكة. بشكل مثالي، يمكنك تخزين رمز الدخول في مساحة تخزين آمنة ودائمة لتقليل طلبات التطبيق للرموز المميزة الجديدة للوصول. (ولكن بالنسبة إلى التطبيقات المثبتة، يمثل التخزين الآمن مشكلة صعبة).

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