المصادقة على خدمات OAuth2

رسم توضيحي لمنطق
    الرمز المميز للمصادقة
الشكل 1. إجراء للحصول على رمز مصادقة صالح من مدير حساب Android.

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

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

يُعد استخدام OAuth2 مفيدًا في ما يلي:

  • الحصول على إذن من المستخدم للوصول إلى خدمة عبر الإنترنت باستخدام حسابه.
  • المصادقة على خدمة على الإنترنت بالنيابة عن المستخدم
  • التعامل مع أخطاء المصادقة.

جمع المعلومات

لبدء استخدام OAuth2، عليك معرفة بعض الأمور الخاصة بواجهة برمجة التطبيقات حول الخدمة التي تحاول الوصول إليها:

  • عنوان URL للخدمة التي تريد الوصول إليها.
  • نطاق المصادقة، وهو سلسلة تحدد نوع الوصول المحدد الذي يطلبه تطبيقك. على سبيل المثال، نطاق المصادقة للوصول للقراءة فقط إلى "مهام Google" هو View your tasks، في حين أنّ نطاق المصادقة للإذن بالقراءة والكتابة في "مهام Google" هو Manage your tasks.
  • معرّف العميل وسر العميل، وهما سلاسل تحدد تطبيقك للخدمة. تحتاج إلى الحصول على هذه السلاسل مباشرة من مالك الخدمة. تمتلك Google نظام الخدمة الذاتية للحصول على معرّفات وأسرار العملاء.

طلب إذن الاتصال بالإنترنت

بالنسبة إلى التطبيقات التي تستهدف Android 6.0 (المستوى 23 لواجهة برمجة التطبيقات) والإصدارات الأحدث، لا تتطلّب طريقة getAuthToken() ذاتها أي أذونات. ومع ذلك، لتنفيذ عمليات على الرمز المميّز، عليك إضافة إذن INTERNET إلى ملف البيان، كما هو موضّح في مقتطف الرمز التالي:

<manifest ... >
    <uses-permission android:name="android.permission.INTERNET" />
    ...
</manifest>

طلب رمز مميز للمصادقة

للحصول على الرمز المميز، يمكنك الاتصال بـ AccountManager.getAuthToken().

تحذير: نظرًا لأن بعض عمليات الحساب قد تتضمن الاتصال بالشبكة، تكون معظم طرق AccountManager غير متزامنة. وهذا يعني أنه بدلاً من القيام بجميع أعمال المصادقة في دالة واحدة، يجب تنفيذها كسلسلة من عمليات الاسترداد.

يوضِّح المقتطف التالي كيفية التعامل مع سلسلة من عمليات معاودة الاتصال للحصول على الرمز المميّز:

Kotlin

val am: AccountManager = AccountManager.get(this)
val options = Bundle()

am.getAuthToken(
        myAccount_,                     // Account retrieved using getAccountsByType()
        "Manage your tasks",            // Auth scope
        options,                        // Authenticator-specific options
        this,                           // Your activity
        OnTokenAcquired(),              // Callback called when a token is successfully acquired
        Handler(OnError())              // Callback called if an error occurs
)

Java

AccountManager am = AccountManager.get(this);
Bundle options = new Bundle();

am.getAuthToken(
    myAccount_,                     // Account retrieved using getAccountsByType()
    "Manage your tasks",            // Auth scope
    options,                        // Authenticator-specific options
    this,                           // Your activity
    new OnTokenAcquired(),          // Callback called when a token is successfully acquired
    new Handler(new OnError()));    // Callback called if an error occurs

في هذا المثال، OnTokenAcquired عبارة عن فئة تنفّذ AccountManagerCallback. اتصال AccountManager بـ run() على OnTokenAcquired مع AccountManagerFuture يحتوي على Bundle. إذا نجحت المكالمة، يكون الرمز المميّز داخل Bundle.

إليك كيفية الحصول على الرمز المميز من Bundle:

Kotlin

private class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        // Get the result of the operation from the AccountManagerFuture.
        val bundle: Bundle = result.getResult()

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        val token: String = bundle.getString(AccountManager.KEY_AUTHTOKEN)
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        // Get the result of the operation from the AccountManagerFuture.
        Bundle bundle = result.getResult();

        // The token is a named value in the bundle. The name of the value
        // is stored in the constant AccountManager.KEY_AUTHTOKEN.
        String token = bundle.getString(AccountManager.KEY_AUTHTOKEN);
        ...
    }
}

إذا سارت الأمور على ما يرام، يحتوي Bundle على رمز مميّز صالح في مفتاح KEY_AUTHTOKEN، وستكون جاهزًا لذلك.

قد يتعذّر تنفيذ طلبك الأول للحصول على رمز مميّز للمصادقة، وذلك لعدة أسباب:

  • حدث خطأ في الجهاز أو الشبكة أدى إلى تعذُّر استخدام "AccountManager".
  • قرّر المستخدم عدم منح تطبيقك إذن الوصول إلى الحساب.
  • بيانات اعتماد الحساب المخزّنة غير كافية للوصول إلى الحساب.
  • انتهت صلاحية الرمز المميز للمصادقة المُخزَّنة مؤقتًا.

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

يتم الإبلاغ عن حالة الفشل الثالثة، وهي عدم وجود بيانات اعتماد غير كافية، عبر Bundle التي تتلقاها في AccountManagerCallback (OnTokenAcquired من المثال السابق). إذا كان Bundle يتضمّن Intent في المفتاح KEY_INTENT، سيخبرك برنامج المصادقة بأنّه يجب التفاعل مع المستخدم مباشرةً قبل أن يمنحك رمزًا مميّزًا صالحًا.

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

Kotlin

private inner class OnTokenAcquired : AccountManagerCallback<Bundle> {

    override fun run(result: AccountManagerFuture<Bundle>) {
        val launch: Intent? = result.getResult().get(AccountManager.KEY_INTENT) as? Intent
        if (launch != null) {
            startActivityForResult(launch, 0)
        }
    }
}

Java

private class OnTokenAcquired implements AccountManagerCallback<Bundle> {
    @Override
    public void run(AccountManagerFuture<Bundle> result) {
        ...
        Intent launch = (Intent) result.getResult().get(AccountManager.KEY_INTENT);
        if (launch != null) {
            startActivityForResult(launch, 0);
            return;
        }
    }
}

لاحظ أن المثال يستخدم startActivityForResult()، بحيث يمكنك التقاط نتيجة Intent من خلال تنفيذ onActivityResult() في نشاطك الخاص. هذا مهم: إذا لم تحصل على النتيجة من رد برنامج المصادقة Intent، من المستحيل معرفة ما إذا كان المستخدم قد أجرى المصادقة بنجاح أم لا.

إذا كانت النتيجة RESULT_OK، يعني ذلك أنّ برنامج المصادقة قد عدَّل بيانات الاعتماد المخزَّنة لكي تكون كافية لمستوى الوصول الذي طلبته، وعليك الاتصال بـ AccountManager.getAuthToken() مرة أخرى لطلب الرمز المميّز الجديد للمصادقة.

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

الاتصال بالخدمة على الإنترنت

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

تتطلب منك واجهات برمجة تطبيقات Google تقديم أربع قيم مع كل طلب، وهي: مفتاح واجهة برمجة التطبيقات، ومعرّف العميل، وسر العميل، ومفتاح المصادقة. وتأتي الأنواع الثلاثة الأولى من موقع وحدة تحكم Google API على الويب. القيمة الأخيرة هي قيمة السلسلة التي حصلت عليها عن طريق استدعاء AccountManager.getAuthToken(). وتقوم بتمريرها إلى خادم Google كجزء من طلب HTTP.

Kotlin

val url = URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=$your_api_key")
val conn = url.openConnection() as HttpURLConnection
conn.apply {
    addRequestProperty("client_id", your client id)
    addRequestProperty("client_secret", your client secret)
    setRequestProperty("Authorization", "OAuth $token")
}

Java

URL url = new URL("https://www.googleapis.com/tasks/v1/users/@me/lists?key=" + your_api_key);
URLConnection conn = (HttpURLConnection) url.openConnection();
conn.addRequestProperty("client_id", your client id);
conn.addRequestProperty("client_secret", your client secret);
conn.setRequestProperty("Authorization", "OAuth " + token);

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

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