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

نظرة عامة

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

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

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

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

تسجيل العميل

قبل استخدام مكتبة عميل OAuth من Google لـ 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 Client Library for 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، يمكنك استخدام الفئات ذات المستوى الأدنى:

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

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

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

نموذج استخدام تطبيق ويب:

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.