مقدمه
در ادامه مطالب مربوط به اینترنت و اتصال برنامههای اندروید، میرسیم به مبحث بسیار مهم امنیت برنامهها. بسیاری از برنامههای موبایل که این روزها استفاده میکنیم، نیاز دارند به شناسایی کاربر. کاربر باید بتواند به این برنامهها «وارد» شود و برخی کارها را که «مجاز» است انجام دهد. در برنامهنویسی به عمل شناسایی یک کاربر Authentication یا احراز هویت گفته میشود. هدف از احراز هویت این است که مشخص کند آیا شخصی که میخواهد از برنامه استفاده کند همان کسی است که ادعا میکند یا نه؟ یکی از معمولترین شیوههای احراز هویت استفاده از «نام کاربری» و «رمز عبور» است. اگر کاربر همان فردی که ادعا میکند بود، نوبت این است که چک کنیم آیا برای کاری که میخواهد انجام دهد «اجازه» دارد یا نه؟ این عملیات را در برنامهنویسی «Authorization» یا «احراز صلاحیت» یا «اجازه» مینامند.
در این مطلب میخواهیم ببینیم چطور میتوانیم برنامههای اندروید خود را از لحاظ امنیتی به دو سلاح «احراز هویت» و «احراز صلاحیت» مجهز کنیم.
احراز هویت با استفاده از نام کاربری و رمز عبور
سادهترین شکل احراز هویت این است که از کاربر در زمان استفاده از برنامه نام کاربری و رمز عبور بخواهیم. سپس این اطلاعات را با سرور چک کنیم. اگر نام کاربری و رمز عبور هر دو صحیح بودند و سرور آنها را تأیید کرد، بعد از آن باید در هر فراخوانی سرویسهای سرور این نام کاربری و رمز عبور را ارسال کنیم.
OkHttp
کتابخانه OkHttp برای این کار از کلاس Authenticator استفاده میکند. با اضافه کردن یک نمونه از کلاس Authenticator به کلاینت OkHttp، هر زمان که درخواستهای ارسالی به سمت سرور با پاسخ «۴۰۱ Not Authorized» مواجه شود، درخواست جدیدی میسازد و اطلاعات مربوط به کاربر (نام کاربری و رمز عبور) را به این درخواست جدید اضافه میکند و این درخواست جدید را به سرور میفرستد.
برای ساخت یک نمونه از کلاس Authenticator به شکل زیر عمل میکنیم:
Authenticator authenticator = new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { // ۱ String credential = Credentials.basic("my_username", "my_secret_pass"); // ۲ return response.request().newBuilder() .header("Authorization", credential) .build(); } };
توضیحات:
۱- با استفاده از کلاس Credential نام کاربری و رمز عبور کاربر را به شیوه مناسب کد میکنیم. توجه کنید که این کدگذاری به معنای «امن» بودن نیست. فقط نام کاربری و رمز عبور به شیوهی مورد پذیرش پروتکل HTTP نوشته میشوند. درباره امنیت در ادامه صحبت خواهیم کرد.
۲- یک درخواست جدید میسازیم که همه چیزش همان درخواست قبلی است ولی این بار اطلاعات کاربر هم به آن اضافه شده است. همانطور که میبینید به درخواست جدید یک سرآیند یا header اضافه است با نام Authorization که مقدار آن توسط کلاس Credential ساخته میشود.
حالا با استفاده از این نمونه از کلاس Authenticator یک کلاینت OkHttp میسازیم:
final OkHttpClient client = new OkHttpClient.Builder() // ۱ .authenticator(authenticator) .build();
۱- همه چیزی که باید به OkHttp اضافه کرد، فراخوانی تابع authenticate و ارسال نمونه ساخته شده از کلاس Authenticator به این متد است.
مشکلی که ممکن است در این حالت پیش بیاید این است که اگر نام کاربری و رمز عبور اشتباه باشد، دوباره سرور خطای «۴۰۱ Not Authorized» را برمیگرداند و باز متد authenticate فراخوانی میشود و برنامه اصطلاحاً در لوپ (loop) میافتد. برای جلوگیری از این مشکل تغییر کوچکی در متد authenticate میدهیم:
Authenticator authenticator = new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { String credential = Credentials.basic("my_username", "my_secret_pass"); // ۱ if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry. } return response.request().newBuilder() .header("Authorization", credential) .build(); } };
۱- با این if چک میکنیم که این credential جدید با credential درخواست قبلی یکی هست یا نه؟ اگر یکی بود، معنیاش این است که ما یک بار این credential را آزمودهایم و نیازی نیست دوباره درخواست را به سرور بفرستیم. بنابراین null برمیگردانیم.
Retrofit
برای احراز هویت با نام کاربری و رمز عبور در Retrofit میتوان از چند روش استفاده کرد:
۱- استفاده از کلاس Authenticator که در کتابخانه OkHttp وجود دارد و در بخش قبلی دیدیم. از آنجایی که Retrofit برای کارهای اتصال به سرور و ارسال و دریافت اطلاعات از OkHttp استفاده میکند، میتوان در کلاس ServiceGenerator و در زمان ساخت نمونه کلاینت OkHttp همین مراحلی که برای OkHttp گفتیم برای Retrofit هم تکرار کنیم:
public class ServiceGenerator { public static final String API_BASE_URL = "http://your.api-base.url"; Authenticator authenticator = new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { String credential = Credentials.basic("my_username", "my_secret_pass"); if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry. } return response.request().newBuilder() .header("Authorization", credential) .build(); } }; private static OkHttpClient httpClient = new OkHttpClient.Builder() .authenticator(authenticator) .build(); private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_BASE_URL) .addConverterFactory(GsonConverterFactory.create()); public static <S> S createService(Class<S> serviceClass) { Retrofit retrofit = builder.client(httpClient).build(); return retrofit.create(serviceClass); } }
۲- استفاده از حائل یا Interceptor. حائل یا Interceptor کدهایی هستند که بعد از ارسال درخواست و قبل از رسیدن درخواست به سرور اجرا شده و تغییراتی در درخواست ارسالی اعمال میکنند. میتوان یک حائل تعریف کرد که اطلاعات کاربر و سرآیند یا header مربوط یه احراز هویت را به درخواست اضافه کند و سپس درخواست را به سرور ارسال کند. برای این کار ابتدا یک حائل درست میکنیم:
Interceptor basicAuthenticationInterceptor = new Interceptor() { @Override public Response intercept(Interceptor.Chain chain) throws IOException { String credential = Credentials.basic("my_username", "my_secret_pass"); Request original = chain.request(); Request.Builder requestBuilder = original.newBuilder() .header("Authorization", credential) .header("Accept", "application/json") .method(original.method(), original.body()); Request request = requestBuilder.build(); return chain.proceed(request); } };
حالا کلاس ServiceGenerator را به شکل زیر اصلاح میکنیم:
public class ServiceGenerator { public static final String API_BASE_URL = "https://your.api-base.url"; private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); private static Retrofit.Builder builder = new Retrofit.Builder() .baseUrl(API_BASE_URL) .addConverterFactory(GsonConverterFactory.create()); public static <S> S createService(Class<S> serviceClass) { return createService(serviceClass, null, null); } public static <S> S createService(Class<S> serviceClass, String username, String password) { httpClient.addInterceptor(basicAuthenticationInterceptor); OkHttpClient client = httpClient.build(); Retrofit retrofit = builder.client(client).build(); return retrofit.create(serviceClass); } }
۳- استفاده از @Header. همانطور که میبینید، احراز هویت ساده که با استفاده از نام کاربری و رمز عبور انجام میشود، تنها کاری که میکند این است که یک header یا سرآیند به درخواست اضافه میکند به اسم Authorization که مقدار آن یک زشته است که به فرمت Base64 کد شده است. اگر در سرویسی که نیاز به احراز هویت دارد با استفاده از حاشیهنوشت @Header یک سرآیند یا header با این نام و مقدار اضافه کنیم، به طور خودکار به درخواست اضافه شده و بعد به سرور ارسال میشود:
public interface StoreClient { @GET("/store/{category}") Call<Category> categoryProducts( @Path("category") String category, @Header("Authorization" String credentials); @GET("/store/products/{product-id}") Call<Product> productDetails( @Path("product-id") long productId, @Header("Authorization" String credentials); }
حالا هر زمان که قرار باشد این سرویسها را فراخوانی کنیم، باید مقدار سرآیند Authorization را نیز به همراه سایر پارامترها با این متدها ارسال کنیم.
احراز هویت با استفاده از توکن
یکی از معایب اصلی احراز هویت ساده با نام کاربری و رمز عبور این است که در هر درخواستی باید نام کاربری و رمز عبور کاربر را به سرور بفرستیم. اگر مسیر ارتباطی ایمن نباشد (و پیشفرض این است که همه مسیرها ناامن هستند!) یک هکر به سادگی میتواند به نام کاربری و رمز عبور کاربران دسترسی پیدا کند. برای حل این مشکل بسیاری از سایتها احراز هویت ساده با نام کاربری و رمز عبور را کنار گذاشته و به جای آن از ساز و کار دیگری استفاده میکنند. در این روش، بعد از اولین ورود کاربر به سایت یا اپ موبایل، سرور یک رشته منحصر به فرد تولید میکند به اسم توکن و این توکن را در پاسخ به لاگین موفق به کلاینت میفرستد. از این به بعد کلاینت در درخواستهای خودش از سرور به جای ارسال نام کاربری و رمز عبور، فقط کافی است این توکن را به سرور بفرستد. مزیت این روش یکی امنیت بیشتر آن نسبت به روش احراز هویت ساده است و دیگری این که سرور میتواند به صورت دورهای این توکنها را نامعتبر کند تا کاربرها محبور به لاگین دوباره شوند. یا در مواقعی که مشکل امنیتی برای سایت پیش میآید با نامعتبرکردن همه توکنها، جلوی سوءاستفاده هکرها از اطلاعات کاربران را بگیرد. یکی از وظایف برنامههای موبایل این است که از این توکن به خوبی محافظت کنند.
با این که این روش احراز هویت از فلسفه متفاوتی استفاده میکند ولی ساز و کار فنی آن فرق چندانی با روش احراز هویت ساده ندارد. باز هم باید header یا سرآیند Authorization را به درخواست اضافه کنیم و به جای مقدار آن، از توکنی که پیش از این از سرور گرفتهایم استفاده میکنیم.
در مطلب بعدی شیوه استفاده از حساب کاربری گوگل برای لاگین کردن به برنامه را آموزش خواهم داد.
با سلام و تشکر از مطالب خوبتون
چه اشکالی داره اگه به جای استفاده از Authenticator برای احراز هویت، از JSON استفاده بشه و پوزر پسورد به شکل یک رشته JSON به سرور ارسال بشه و در اونجا مطابقت داده بشه و اگه درست بود، یک پاسخ به شکل JSON مبنی بر جواز ورود به کلاینت ارسال بشه؟
بستگی بسار زیادی به پیادهسازی ساز و کار احراز هویت در سمت سرور دارد. اگر از یک سازکار شخصی شده استفاده میکنید، میتوانید تمام اجزای آن را خودتان به دلخواه تنظیم کنید. البته اکیداً توصیه میکنم این کار را نکنید چون کتابخانههای موجود استانداردها و نکات امنیتی را با بیشترین دقت پیادهسازی میکنند و من بعید میدانم راهکار شخصی شده شما به اندازه آنها امن باشد.
با سلام و خسته نباشید، ادامه مطلب رو نمیخواهید بزارید؟
ارادتمند.
سلام
چند روزی است که به شدت سرم شلوغه. انشاالله به زودی ادامه مطالب رو مینویسم.
فصل های ۲۰، ۲۱ و…. چطور میشه بشون دست پیدا کرد ؟ ! ؟
آموزش اندروید فصل ۲۰ : Intent – مقدمه
آموزش اندروید فصل ۲۱ : Intent – پیاده سازی
سلام جناب بهزادیان
آقا ما هرچی گشتیم آدرس ایمیل شما رو توی سایت پیدا کنیم موفق نشدیم! اگه ممکنه آدرس ایمیل رو محبت کنید تا یه موضوعی رو باهاتون مطرح کنم.
خیلی سپاس
سلام
ali.behzadian در جیمیل 🙂
تشکر عالی توضیح دادین.
با سلام و سپاس از آموزشهای مفید و پر محتوای شما
من هنوز نتونستم مطلبی راجع به کوکیها در اندروید پیدا کنم. میخواستم ببینم آیا توی اندروید هم میشه کوکی رو دریافت یا ذخیره کرد. اگه بشه این کار رو کرد فکر کنم برنامه نویسی برای احراز هویت توسط دریافت از کوکی خیلی راحتتر باشه البته مطمئن نیستم. حالا آیا واقعا میشه تو اندروید هم کوکی ها رو دریافت یا ذخیره کرد و بعد هنگام اتصال اندروید به سرور کوکی که از اندروید ارسال میشه رو توی سرور دریافت و با دیتابیس مطابقت داد؟ (همان طور که در برنامه نویسی تحت وب مثل php و غیره از این روش استفاده میشه)
اگر بخواهیم بعد از ورود کاربر، صفحه لاگین را کاملا غیر فعال کنیم که دوباره کاربر با زدن دکمه بازگشت، به صفحه لاگین وارد نشه باید چه کار کنیم. البته میشه با استفاده از متدهایی در صفحه لاگین متوجه بازگشت کاربر شد و قبل از نمایش محتوای صفحه، کاربر رو به طور خودکار دوباره به صفحه اصلی هدایت کرد، ولی میخوام طوری باشه که اصلا کاربر نتونه دوباره صفحه لاگین رو فراخوانی کنه. آیا امکان پذیره؟
ممنون عالی بود
بسیار ممنون از نشر علم تون…