اشتراکها
کتابخانه استاندارد تبدیل اسامی، به جمع آنها در دات نت، کتابخانهی معروف Humanizer است. بهتر است برای این نوع کارها از آن استفاده کنید چون تهیهی حالت جمع اسامی، استثناء زیاد دارد.
بجای dynamic linq پیوست شده، از این کتابخانه استفاده کنید: «System.Linq.Dynamic». همان نسخهی بهبود یافتهی کتابخانهی اصلی مایکروسافت است.
مطابق Ajax API ترجمه گوگل، برای ترجمه یک متن باید محتویات آدرس زیر را تحلیل کرد:
http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}
بنابراین برای استفاده از آن تنها کافی است این URL را تشکیل داده و سپس محتویات خروجی آنرا آنالیز کرد. فرمت نهایی دریافت شده از نوع JSON است. برای مثال اگر hello world! را به این سرویس ارسال نمائیم، خروجی نهایی JSON دریافت شده به صورت زیر خواهد بود:
//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}
در کتابخانهی System.Web.Extensions.dll دات نت فریم ورک سه و نیم، کلاس JavaScriptSerializer برای این منظور پیش بینی شده است. تنها کافی است به متد Deserialize آن، متن JSON دریافتی را پاس کنیم:
GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);
برای اینکه عملیات نگاشت اطلاعات متنی JSON به کلاسهای دات نتی ما با موفقیت صورت گیرد، میتوان خروجی JSON گوگل را به شکل زیر نمایش داد:
//ResponseData.cs file
public class ResponseData
{
public string translatedText { get; set; }
}
//GoogleAjaxResponse.cs file
using System.Net;
/// <summary>
/// کلاسی جهت نگاشت اطلاعات جی سون دریافتی به آن
/// </summary>
public class GoogleAjaxResponse
{
public ResponseData responseData { get; set; }
public object responseDetails { get; set; }
public HttpStatusCode responseStatus { get; set; }
}
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Web.Script.Serialization;
//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}
public class CGoogleTranslator
{
#region Fields (1)
/// <summary>
/// ارجاع دهنده
/// </summary>
private readonly string _referrer;
#endregion Fields
#region Constructors (1)
/// <summary>
/// مطابق مستندات نیاز به یک ارجاع دهنده اجباری میباشد
/// </summary>
/// <param name="referrer"></param>
public CGoogleTranslator(string referrer)
{
_referrer = referrer;
}
#endregion Constructors
#region Properties (2)
/// <summary>
/// ترجمه از زبان
/// </summary>
public CultureInfo FromLanguage { get; set; }
/// <summary>
/// ترجمه به زبان
/// </summary>
public CultureInfo ToLanguage { get; set; }
#endregion Properties
#region Methods (2)
// Public Methods (1)
/// <summary>
/// ترجمه متن با استفاده از موتور ترجمه گوگل
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string TranslateText(string data)
{
//ساخت و انکدینگ آدرس مورد نظر
string url =
string.Format(
"http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}",
HttpUtility.UrlEncode(data), //needs a ref. to System.Web.dll
FromLanguage.TwoLetterISOLanguageName,
ToLanguage.TwoLetterISOLanguageName
);
//دریافت اطلاعات جی سون از گوگل
string jsonGoogleAjaxResponse = fetchWebPage(url);
//needs a ref. to System.Web.Extensions.dll
//نگاشت اطلاعات جی سون دریافت شده به کلاس مرتبط
GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);
if (result != null && result.responseData != null && result.responseStatus == HttpStatusCode.OK)
{
return result.responseData.translatedText;
}
return string.Empty;
}
// Private Methods (1)
/// <summary>
/// دریافت محتویات جی سون بازگشتی از گوگل
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
string fetchWebPage(string url)
{
try
{
var uri = new Uri(url);
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
var request = WebRequest.Create(uri) as HttpWebRequest;
if (request != null)
{
request.Method = WebRequestMethods.Http.Get;
request.Referer = _referrer;
request.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.0; ; rv:1.8.0.7) Gecko/20060917 Firefox/1.9.0.1";
request.AllowAutoRedirect = true;
request.Timeout = 1000 * 300;
request.KeepAlive = false;
request.ReadWriteTimeout = 1000 * 300;
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
using (var response = request.GetResponse() as HttpWebResponse)
{
if (response != null)
{
using (var reader = new StreamReader(response.GetResponseStream()))
{
return reader.ReadToEnd().Trim();
}
}
}
}
}
return string.Empty;
}
catch (Exception ex)
{
Console.WriteLine(String.Format("fetchWebPage: {0} >> {1}", ex.Message, url), true);
return string.Empty;
}
}
#endregion Methods
}
string res = new CGoogleTranslator("https://www.dntips.ir/")
{
FromLanguage = CultureInfo.GetCultureInfo("en-US"),
ToLanguage = CultureInfo.GetCultureInfo("fa-IR")
}.TranslateText("Hello world!");
اگر جدیدا قصد برنامه نویسی اندروید را کردهاید، یا هنوز روشهای متدوالی را برای
کار با این زبان انتخاب نکردهاید؛ به نظرم این مقاله میتواند کمک خوبی
برای شما باشد. مسائلی که بیان میکنم در واقع از تجربیات شخصی و راه حل
هایی است که برای خودم تعیین کردهام و تعدادی از آنها را در طول مدتی که
در این زمینه فعالیت کردهام، از جاهای مختلف دیده و در یک جا گردآوری
کردهام. برای نامگذاری اشیاء و متغیرها و دیگر موارد، من از این قاعده پیروی میکنم که به نظرم بسیار ایده آل میباشد. الگوی معماری هم که جدیدا مورد استفاده قرار دادهام، الگوی MVP است که نمونهای از آن، در گیت هاب قرار گرفته است. البته این مثال ساده تر نیز وجود دارد. تشریح کامل این معماری را به همراه آزمون واحد آن، میتوانید در این مقاله سه قسمتی ببینید.
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
/** * * <p> * check network is available or not <br/> * internet connection is not matter,for check internet connection refer to IsInternetConnected() Method in this class * </p> * <p> * Required Permission : <b>android.permission.ACCESS_NETWORK_STATE</b> * </p> * @param context * @return returns true if a network is available */ public boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); }
همچنین اگر، مورد خاص دیگری مثل بالا بود، حتما آن را ذکر کنید. میتوانید از تگ گذاری در کامنت ها
نیز استفاده کنید. از ویژگیهای کامنت todo در اندروید استادیو این است
که میتوانید در حین کار با سیستم گیت نیز از آن بهره ببرید و قبل از کامیت
کردن کد، کدهای todo به شما یادآوری شوند و هر پیکربندی را که لازم دارید، روی آن انجام دهید.
دو.
از یک کلاس واحد جهت استفاده از اطلاعات عمومی و یا ثابتها استفاده
نمایید. این اطلاعات میتوانند شامل: مسیرها، آدرسهای وب سرویس، شماره
اختصاصی هر نوتیفیکیشن و .... باشند. برای اینکار میتوان هر کدام از اطلاعات را
داخل یک کلاس قرار داد و همه این کلاسها را به صورت استاتیک تعریف کنید تا
بدین شکل در دسترس قرار بگیرند (از الگوی singleton هم میتوان استفاده
کرد).
public class ProjectSettings { public static NotificationsId=new NotificationsId(); public static UrlAddresss=new UrlAddresss(); public static SdPath=new SdPath(); ...... }
ProjectSettings.NotificationsId.UpdateNotificationId
بدین شکل هم به طور ساده و مفهومی صدا زده میشود و هم اینکه در همه جای
برنامه این ثابتها و مقادیر قابل استفاده هستند. به عنوان مثال به شماره
هر نوتیفیکیشن از همه جا دسترسی دارید و هم اینکه شمارهای تکراری اشتباها
انتخاب نمیشود.
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
یا اینکه برای یک دیالوگ مستقیما و در جا همانجا به کدنویسی مشغول میشوند.
این روشها هیچ مشکلی ندارند ولی در آینده نگهداری کد را مشکل میکنند.
مثلا تصور کنید شما بسیاری از جاهای برنامه، Toast زدید و حالا قصد دارید در
نسخه بعدی برنامه، toastهای دلخواه و یا custom ایی را ایجاد کنید. در این صورت
مجبورید کل برنامه را رصد کرده و هر جا toast هست آن را تغییر دهید. در
اینجا هم اصول DRY را نادیده گرفتهاید و هم زحمت شما زیاد شدهاست و حتی
ممکن است یک یا چندتایی از قلم بیفتند. برای دیالوگها هم بدین صورت خواهد
بود و خیلی از مسائل دیگر. به همین جهت استفاده از اینترفیسها توصیه
میشود و فردا نیز اگر باز یک کلاس دیگر را نوشتید، خیلی راحت آن را با کلاس
فعلی تعویض میکنید.
public interface IMessageUI { void ShowToast(Context context,String message); } public class MessageUI impelement IMessageUI { public void ShowToast(Context context,string message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }
چهار. اگر برای اولین بار است وارد اندروید میشوید، خوب چرخههای یک شیء، چون اکتیویتی یا فراگمنت را یاد بگیرید تا در آینده با مشکلات خاصی روبرو نشوید.
به عنوان مثال درست است که اولین رویداد فراخوانی در onCreate رخ میدهد ولی
همیشه محل مناسبی برای دریافت دیتاها در زمان اولیه نیست. به عنوان مثال
تصور کنید که لیستی در اکتیویتی A دارید و به اکتیویتی B میروید و یک آیتم
به اطلاعات اضافه میشود و موقعی که به اکتیویتی A بر میگردید، زیاد تعجب نکنید که لیست دقیقا به
همان شکل قبلی است و خبری از آیتم جدید نیست.
چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
به طور خلاصه نحوه اجرای رویدادها بدین شکل است که ابتدای رویداد OnCreate اجرا میشود که هنوز هیچ UI ئی در آن پیاده سازی نشدهاست و شما در اینجا موظفید Layout خود را معرفی کنید. رویداد OnStart بعد از آن موقعی که UI آماده شده است، اجرا میگردد. سپس رویداد OnResume اجرا میشود.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
پنج.
سرویس را با تردهای UI ترکیب نکنید. بعضا دیده میشود که کاربران
AsyncTask را داخل سرویس استفاده میکنند ولی این را بدانید که سرویس یک
ترد پردازشی جداگانه است و تضمینی برای ارتباط با UI به شما نمیدهند. هر
چند گوگل جدیدا تمهیداتی را برای آن اندیشیده است که به شما اجازه اینکار را
نمیدهد. ولی اگر باز هم اندروید استادیو به شما خوردهای نگرفت، خودتان این
قانون را اجرا کنید. قرار نیست یک AsyncTask با سرویس ترکیب شود.
شش. اگر برنامه شما قرار است در چندین حالت مختلفی که اتفاق میافتد، یک کار خاصی را انجام دهد، برای برنامهتان یک Receiver بنویسید و در آن کدهای تکراری را نوشته و در محلهای مختلف وقوع آن رویدادها، رسیور را صدا بزنید. برای نمونه برنامه تلگرام یک سرویس پیام رسان پشت صحنه دارد که در دو رویداد قرار است اجرا شوند. یکی موقعی که گوشی بوت خود را تکمیل کرده است و در حال آغاز فرایندهای سیستم عامل است و دیگر زمانی است که برنامه اجرا میشود. در اینجا تلگرام از یک رسیور سیستمی برای آگاهی از بوت شدن و یک رسیور داخل برنامه جهت آگاهی از اجرای برنامه استفاده میکند و هر دو به یک کلاس از جنس BroadcastReceiver متصلند:
<receiver android:name=".AppStartReceiver" android:enabled="true"> <intent-filter> <action android:name="org.telegram.start" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
public class AppStartReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { ApplicationLoader.startPushService(); } }); } }
برنامه تلگرام حتی برای حالتهای پخش هم رسیورها استفاده کرده است که در همین رسیور وضعیت تغییر پلیر مشخص میشود:
<receiver android:name=".MusicPlayerReceiver" > <intent-filter> <action android:name="org.telegram.android.musicplayer.close" /> <action android:name="org.telegram.android.musicplayer.pause" /> <action android:name="org.telegram.android.musicplayer.next" /> <action android:name="org.telegram.android.musicplayer.play" /> <action android:name="org.telegram.android.musicplayer.previous" /> <action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> </intent-filter> </receiver>
اینگونه تلگرام میتواند از همه جا سرویس را کنترل کند. مثلا موقعی که
دانلود یک موزیک تمام شده، سریعا پخش آن موزیک دانلود شده را آغاز کند.
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on("poems.id=bank_poems_groups.poem") .where("BankGroup=?", String.valueOf(groupId)); return query.execute();
@Table(name="poems") public class Poem extends Model { public static String tableName="poems"; public static String codeColumn="code"; public static String titleColumn="title"; public static String bookColumn="book"; ...... @Column(name="code",index = true) public int Code; @Column(name="title") public String Title; @Column(name="book") public Book Book; .....}
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on(Poem.TableName+"."+ Poem.IdColumn+"="+ BankPoemsGroups.TableName+"."+ BankPoemsGroups.PoemColumn) .where(Poem.BankGroupColumn+"=?", String.valueOf(groupId)); return query.execute();
حالا کمی بهتر شد. هم برای تغییر آینده بهتر شد و هم اینکه احتمال خطای
تایپی کاهش یافت. ولی باز هم ایجاد کوئری هنوز سخت است و نوشتن مرتب یک
رابطه جوین و شرطی و چسباندن مداوم رشتهها کار خسته کنندهای است و احتمال
خطای سهوی و انسانی هم در آن بالاست. برای رفع این مشکل بهتر است یک کلاس
جدید برای ساخت این کوئریها داشته باشیم که یک نمونه از آن را در این
پایین میبینید:
public class QueryConcater { public String GetInnerJoinQuery(String table1,String field1,String table2,String field2) { String query=table1 +"." +field1+"="+table2+"."+field2; return query; } ...... }
return new Select() .from(Color.class) .innerJoin(ProductItem.class) .on(queryConcater.GetInnerJoinQuery(ProductItem.TableName, ProductItem.ColorColumn, Color.TableName)) .where(queryConcater.WhereConditionQuery (ProductItem.TableName, ProductItem.ProductColumn), productId) .execute();
در دستورات بالا از این کلاس دو متد برای کوئری جوین و یکی هم برای ساخت
شرط ایجاد شده است و مقادیر به صورت پارامتر داده شدهاند. این الگو کمک
میکند که اگر هم این تکه کد اشتباه باشد، با تغییر یکجا بقیه کدها هم تغییر
میکنند و اگر در آینده هم ORM تغییر یافت، نحوه کوئری نویسیها در این کلاس
تغییر کنند، نه اینکه در طول لایه سرویس پراکنده باشند.
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که میتوانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارشها شامل:
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که میتوانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارشها شامل:
- وضعیت گوشی در حین باز شدن برنامه و در حین خطا چگونه بوده است.
- مشخصات گوشی
- این خطا به چه تعداد رخ داده است و برای چه تعداد کاربر
- گزارش گیری بر اساس اولین تاریخ رخداد خطا و آخرین تاریخ، نسخه سیستم عامل اندروید، ورژن برنامه شما و...
نه. آکرا همانند Elmah نمیتواند خطاهای catch شده را دریافت کند. برای حل این مشکل عبارت زیر را در catchها بنویسید:
ACRA.getErrorReporter().handleException(caughtException);
ده. بر خلاف سیستم دات نت که شما اجباری به استفاده از Try Catchها ندارید. در
جاوا اینگونه نیست و هر متدی که Throw روی آن انجام شده باشد مستلزم استفاده از catch است. به همین دلیل در شماره نه
گفتیم که چگونه باید این مشکل را حل کنیم. ولی در بسیاری از اوقات پیش
میآید که ما داریم از ماژولهای متفاوتی استفاده میکنیم که جدا از ماژول
اصلی برنامه هستند و این مورد باعث میشود که بعضی افراد یا Acra را در همه
ماژولها صدا بزنند یا اینکه بی خیال آن شوند. ولی کار راحتتر این است که
شما هم همانند برنامه نویسان جاوا متد خود را به Throw مزین کنید تا در
هنگام استفاده از آن در برنامه اصلی نیاز به catch شدن باشد. در واقع شما
نباید catchها را داخل یک کتابخانه جدا و مستقل قرار دهید و روش صحیح هم
همین است حالا چه استفاده از آکرا نیاز باشد و چه نباشد.
نمونه اشتباه:
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) { try { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) throws IOException { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); }
از Claims جهت ارائهی اطلاعات مرتبط با هویت هر کاربر و همچنین Authorization استفاده میشود. برای مثال درنگارشهای قبلی ASP.NET، مفاهیمی مانند «نقشها» وجود دارند. در نگارشهای جدیدتر آن، «نقشها» تنها یکی از انواع «User Claims» هستند. در قسمتهای قبل روش تعریف این Claims را در IDP و همچنین تنظیمات مورد نیاز جهت دریافت آنها را در سمت برنامهی Mvc Client بررسی کردیم. در اینجا مطالب تکمیلی کار با User Claims را مرور خواهیم کرد.
بررسی Claims Transformations
میخواهیم Claims بازگشت داده شدهی توسط IDP را به یکسری Claims که کار کردن با آنها در برنامهی MVC Client سادهتر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میانافزار oidc دریافت میشود، ابتدا بررسی میشود که آیا دیکشنری نگاشتها وجود دارد یا خیر؟ اگر بله، کار نگاشتها از یک claim type به claim type دیگر انجام میشود.
برای مثال لیست claims اصلی بازگشت داده شدهی توسط IDP، پس از تبدیلات و نگاشتهای آن در برنامهی کلاینت، یک چنین شکلی را پیدا میکند:
در ادامه میخواهیم نوعهای آنها را سادهتر کنیم و آنها را دقیقا تبدیل به همان claim typeهایی کنیم که در سمت IDP تنظیم شدهاند. برای این منظور به فایل src\MvcClient\ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده و سازندهی آنرا به صورت زیر تغییر میدهیم:
در اینجا جدول نگاشتهای پیشفرض Claims بازگشت داده شدهی توسط IDP به Claims سمت کلاینت، پاک میشود.
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شدهاست:
اکنون این نوعها دقیقا با آنچیزی که IDP ارائه میدهد، تطابق دارند.
کار با مجموعهی User Claims
تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آنها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider میباشد. حذف آنها حجم کوکی نگهداری کنندهی آنها را کاهش میدهد. همچنین میخواهیم تعدادی دیگر را نیز به آنها اضافه کنیم.
علاوه بر اینها میانافزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف میکند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آنها نیازی نداریم. اما میتوان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن میتوان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
در اینجا فراخوانی متد Remove، به معنای حذف فیلتری است که کار حذف کردن خودکار claim ویژهی amr را که بیانگر نوع authentication است، انجام میدهد (متد Remove در اینجا یعنی مطمئن شویم که amr در لیست claims باقی میماند). همچنین فراخوانی متد DeleteClaim، به معنای حذف کامل یک claim موجود است.
اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیشفرض حذف نشدهاست:
افزودن Claim جدید آدرس کاربر
برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
در اینجا در لیست Resources، گزینهی IdentityResources.Address نیز اضافه شدهاست که به Claim آدرس مرتبط است.
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه میکنیم:
در آخر Claim متناظر با Address را نیز به اطلاعات هر کاربر، در متد GetUsers، اضافه میکنیم:
تا اینجا تنظیمات IDP برای افزودن Claim جدید آدرس به پایان میرسد.
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه میکنیم تا درخواست این claim را به لیست scopes میانافزار oidc اضافه کنیم:
همچنین میخواهیم مطمئن شویم که این claim درون claims identity قرار نمیگیرد. به همین جهت DeleteClaim در اینجا فراخوانی شدهاست تا این Claim به کوکی نهایی اضافه نشود تا بتوانیم همواره آخرین مقدار به روز شدهی آنرا از UserInfo Endpoint دریافت کنیم.
یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و میشود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میانافزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت میکند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از اینها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمیخواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.
دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint
همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامهی کلاینت» عنوان شد، میانافزار oidc با UserInfo Endpoint کار میکند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچکتر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر میشود.
درخواستی که به سمت UserInfo Endpoint ارسال میشود، باید یک چنین فرمتی را داشته باشد:
یک درخواست از نوع GET و یا POST است که به سمت آدرس UserInfo Endpoint ارسال میشود. در اینجا ذکر Access token نیز ضروری است. از این جهت که بر اساس scopes ذکر شدهی در آن، لیست claims درخواستی مشخص شده و بازگشت داده میشوند.
اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بستهی نیوگت IdentityModel را به پروژهی Mvc Client اضافه کنیم:
توسط این بسته میتوان به DiscoveryClient دسترسی یافت و به کمک آن آدرس UserInfo Endpoint را استخراج میکنیم:
در اینجا یک اکشن متد جدید را جهت سفارش نگارش قاب شدهی تصاویر گالری اضافه کردهایم. کار این اکشن متد، دریافت آخرین آدرس شخص از IDP و سپس ارسال آن به View متناظر است. برای این منظور ابتدا یک DiscoveryClient را بر اساس آدرس IDP تشکیل دادهایم. حاصل آن متادیتای این IDP است که یکی از مشخصات آن UserInfoEndpoint میباشد. سپس بر این اساس، UserInfoClient را تشکیل داده و Access token جاری را به سمت این endpoint ارسال میکنیم. حاصل آن، آخرین Claims کاربر است که مستقیما از IDP دریافت شده و از کوکی جاری کاربر خوانده نمیشود. سپس از این لیست، مقدار Claim متناظر با address را استخراج کرده و آنرا به View ارسال میکنیم.
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
و View متناظر با این اکشن متد در آدرس Views\Gallery\OrderFrame.cshtml به صورت زیر تعریف شدهاست که آدرس شخص را نمایش میدهد:
سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه میکنیم:
اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:
همانطور که ملاحظه میکنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شدهاست.
بررسی Authorization مبتنی بر نقشها
تا اینجا مرحلهی Authentication را که مشخص میکند کاربر وار شدهی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج میشود. مرحلهی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی میباشد. به این مرحله Authorization میگویند و روشهای مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقشهای کاربر
در این حالت claim ویژهی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص میشوند. برای مثال کاربر وارد شدهی به سیستم میتواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شدهی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آنرا نسبت به حالت الف ترجیح میدهند که آنرا در قسمتهای بعدی بررسی خواهیم کرد.
در اینجا روش «تعیین سطوح دسترسی بر اساس نقشهای کاربر» را بررسی میکنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف میکنیم:
در اینجا دو نقش FreeUser و PayingUser به صورت claimهایی جدید به دو کاربر IDP اضافه شدهاند.
سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
چون roles یکی از scopeهای استاندارد نیست، آنرا به صورت یک IdentityResource سفارشی تعریف کردهایم. زمانیکه scope ای به نام roles درخواست میشود، لیستی از نوع claimTypes بازگشت داده خواهد شد.
همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آنرا به AllowedScopes مشخصات Client نیز اضافه میکنیم:
در ادامه قرار است تنها کاربری که دارای نقش PayingUser است، امکان دسترسی به سفارش نگارش قاب شدهی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی کلاینت مراجعه کرده و درخواست scope نقشهای کاربر را به متد تنظیمات AddOpenIdConnect اضافه میکنیم:
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحهی consent، از کاربر مجوز دسترسی به نقشهای او نیز سؤال پرسیده میشود:
اما اگر به لیست موجود در this.User.Claims در برنامهی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشدهاست:
همانطور که در نکتهای پیشتر نیز ذکر شد، چون role جزو لیست نگاشتهای OpenID Connect Options مایکروسافت نیست، آنرا به صورت خودکار به لیست claims اضافه نمیکند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، میتوان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار لیست this.User.Claims به صورت زیر تغییر کردهاست که شامل role نیز میباشد:
استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده
در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
در این حالت از متد استاندارد User.IsInRole برای بررسی نقش کاربر جاری استفاده شدهاست. اما این متد هنوز نمیداند که نقشها را باید از کجا دریافت کند و اگر آنرا آزمایش کنید، کار نمیکند. برای تکمیل آن به فایل ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده تنظیمات متد AddOpenIdConnect را به صورت زیر تغییر میدهیم:
TokenValidationParameters نحوهی اعتبارسنجی توکن دریافتی از IDP را مشخص میکند. همچنین مشخص میکند که چه نوع claim ای از token دریافتی از IDP به RoleClaimType سمت ASP.NET Core نگاشت شود.
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شدهی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.
البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کردهایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود میکنیم:
خاصیت Roles فیلتر Authorize، میتواند چندین نقش جدا شدهی توسط کاما را نیز دریافت کند.
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:
همانطور که مشاهده میکنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحهی Account/AccessDenied که هنوز در برنامهی کلاینت تعریف نشدهاست، هدایت شدهایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شدهاست.
برای تغییر مقدار پیشفرض صفحهی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد میکنیم:
سپس View آنرا در فایل Views\Authorization\AccessDenied.cshtml به صورت زیر تکمیل خواهیم کرد که لینک به logout نیز در آن وجود دارد:
اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
آدرس جدید Authorization/AccessDenied باید به تنظیمات AddCookie اضافه شود تا توسط سیستم شناخته شده و مورد استفاده قرار گیرد. علت اینجا است که role claims، از کوکی رمزنگاری شدهی برنامه استخراج میشوند. به همین جهت تنظیم آن مرتبط است به AddCookie.
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شدهاست نمایش داده خواهد شد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
بررسی Claims Transformations
میخواهیم Claims بازگشت داده شدهی توسط IDP را به یکسری Claims که کار کردن با آنها در برنامهی MVC Client سادهتر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میانافزار oidc دریافت میشود، ابتدا بررسی میشود که آیا دیکشنری نگاشتها وجود دارد یا خیر؟ اگر بله، کار نگاشتها از یک claim type به claim type دیگر انجام میشود.
برای مثال لیست claims اصلی بازگشت داده شدهی توسط IDP، پس از تبدیلات و نگاشتهای آن در برنامهی کلاینت، یک چنین شکلی را پیدا میکند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1 Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd Claim type: given_name - Claim value: Vahid Claim type: family_name - Claim value: N
namespace ImageGallery.MvcClient.WebApp { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); }
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شدهاست:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb • Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: idp - Claim value: local • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
کار با مجموعهی User Claims
تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آنها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider میباشد. حذف آنها حجم کوکی نگهداری کنندهی آنها را کاهش میدهد. همچنین میخواهیم تعدادی دیگر را نیز به آنها اضافه کنیم.
علاوه بر اینها میانافزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف میکند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آنها نیازی نداریم. اما میتوان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن میتوان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.ClaimActions.Remove("amr"); options.ClaimActions.DeleteClaim("sid"); options.ClaimActions.DeleteClaim("idp"); }); }
اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیشفرض حذف نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
افزودن Claim جدید آدرس کاربر
برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace DNT.IDP { public static class Config { // identity-related resources (scopes) public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Address() }; }
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address },
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Main Road 1") } }, new TestUser { // ... Claims = new List<Claim> { // ... new Claim("address", "Big Street 2") } } }; }
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه میکنیم تا درخواست این claim را به لیست scopes میانافزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... .AddOpenIdConnect("oidc", options => { // ... options.Scope.Add("address"); // … options.ClaimActions.DeleteClaim("address"); }); }
یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و میشود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میانافزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت میکند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از اینها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمیخواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.
دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint
همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامهی کلاینت» عنوان شد، میانافزار oidc با UserInfo Endpoint کار میکند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچکتر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر میشود.
درخواستی که به سمت UserInfo Endpoint ارسال میشود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo Authorization: Bearer R9aty5OPlk
اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بستهی نیوگت IdentityModel را به پروژهی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { public async Task<IActionResult> OrderFrame() { var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]); var metaDataResponse = await discoveryClient.GetAsync(); var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint); var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var response = await userInfoClient.GetAsync(accessToken); if (response.IsError) { throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception); } var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value; return View(new OrderFrameViewModel(address)); }
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels { public class OrderFrameViewModel { public string Address { get; } = string.Empty; public OrderFrameViewModel(string address) { Address = address; } } }
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel <div class="container"> <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div> <div class="text bottomMarginSmall">We've got this address on record for you:</div> <div class="text text-info bottomMarginSmall">@Model.Address</div> <div class="text">If this isn't correct, please contact us.</div> </div>
سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه میکنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:
همانطور که ملاحظه میکنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شدهاست.
بررسی Authorization مبتنی بر نقشها
تا اینجا مرحلهی Authentication را که مشخص میکند کاربر وار شدهی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج میشود. مرحلهی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی میباشد. به این مرحله Authorization میگویند و روشهای مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقشهای کاربر
در این حالت claim ویژهی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص میشوند. برای مثال کاربر وارد شدهی به سیستم میتواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شدهی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آنرا نسبت به حالت الف ترجیح میدهند که آنرا در قسمتهای بعدی بررسی خواهیم کرد.
در اینجا روش «تعیین سطوح دسترسی بر اساس نقشهای کاربر» را بررسی میکنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف میکنیم:
namespace DNT.IDP { public static class Config { public static List<TestUser> GetUsers() { return new List<TestUser> { new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "PayingUser") } }, new TestUser { //... Claims = new List<Claim> { //... new Claim("role", "FreeUser") } } }; }
سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP { public static class Config { public static IEnumerable<IdentityResource> GetIdentityResources() { return new List<IdentityResource> { // ... new IdentityResource( name: "roles", displayName: "Your role(s)", claimTypes: new List<string>() { "role" }) }; }
همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آنرا به AllowedScopes مشخصات Client نیز اضافه میکنیم:
namespace DNT.IDP { public static class Config { public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { // ... AllowedScopes = { // ... "roles" } // ... } }; }
در ادامه قرار است تنها کاربری که دارای نقش PayingUser است، امکان دسترسی به سفارش نگارش قاب شدهی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامهی کلاینت مراجعه کرده و درخواست scope نقشهای کاربر را به متد تنظیمات AddOpenIdConnect اضافه میکنیم:
options.Scope.Add("roles");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار در صفحهی consent، از کاربر مجوز دسترسی به نقشهای او نیز سؤال پرسیده میشود:
اما اگر به لیست موجود در this.User.Claims در برنامهی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشدهاست:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N
همانطور که در نکتهای پیشتر نیز ذکر شد، چون role جزو لیست نگاشتهای OpenID Connect Options مایکروسافت نیست، آنرا به صورت خودکار به لیست claims اضافه نمیکند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، میتوان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7 • Claim type: amr - Claim value: pwd • Claim type: given_name - Claim value: Vahid • Claim type: family_name - Claim value: N • Claim type: role - Claim value: PayingUser
استفاده از نقش تعریف شده جهت محدود کردن دسترسی به سفارش تصاویر قاب شده
در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.GivenName, RoleClaimType = JwtClaimTypes.Role, };
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شدهی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.
البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کردهایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود میکنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public async Task<IActionResult> OrderFrame() {
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:
همانطور که مشاهده میکنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحهی Account/AccessDenied که هنوز در برنامهی کلاینت تعریف نشدهاست، هدایت شدهایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شدهاست.
برای تغییر مقدار پیشفرض صفحهی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد میکنیم:
using Microsoft.AspNetCore.Mvc; public class AuthorizationController : Controller { public IActionResult AccessDenied() { return View(); } }
<div class="container"> <div class="h3">Woops, looks like you're not authorized to view this page.</div> <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div> </div>
اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { // ... }).AddCookie("Cookies", options => { options.AccessDeniedPath = "/Authorization/AccessDenied"; }) // ...
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شدهاست نمایش داده خواهد شد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
برای انتخاب یک کتابخانه صرفا به سرعت آن نباید توجه کرد. این موارد برای انتخاب کتابخانههای ثالث، مهم هستند:
- آیا این کتابخانه محلی برای بحث و رفع اشکال دارد؟
- آیا سورس باز است؟ (غیر الزامی؛ اما یک امتیاز مثبت)
- آیا به همراه مثالهای کاربردی است؟
- آیا مستندات قابل قبولی دارد؟
- آیا در جستجویی که انجام شده، کسی از آن در پروژههای خودش استفاده میکند؟
- آیا هر از چندگاهی به روز میشود؟ آخرین باری که به روز شده چه زمانی بوده؟
- آیا استفاده از آن در انواع و اقسام پروژهها مجاز است؟ مجوز استفاده از آن به چه نحوی است؟
- آیا این کتابخانه محلی برای بحث و رفع اشکال دارد؟
- آیا سورس باز است؟ (غیر الزامی؛ اما یک امتیاز مثبت)
- آیا به همراه مثالهای کاربردی است؟
- آیا مستندات قابل قبولی دارد؟
- آیا در جستجویی که انجام شده، کسی از آن در پروژههای خودش استفاده میکند؟
- آیا هر از چندگاهی به روز میشود؟ آخرین باری که به روز شده چه زمانی بوده؟
- آیا استفاده از آن در انواع و اقسام پروژهها مجاز است؟ مجوز استفاده از آن به چه نحوی است؟
یک نکتهی تکمیلی: پشتیبانی توکار ASP.NET Core 2.0 از Range headers
فرض کنید برای آزمایش قسمت «از سرگیری مجدد» دریافت یک فایل حجیم مطلب جاری، یک چنین کدی را در یک برنامهی ASP.NET Core 2.0 تهیه کردهاید:
در مورد PhysicalFile در مطلب «تغییرات متدهای بازگشت فایلها به سمت کلاینت در ASP.NET Core» بیشتر بحث شدهاست.
اجرای این کد به همراه هدر مخصوص «Accept-Ranges: bytes » که در مطلب جاری در مورد آن بحث شد نیز هست:
یعنی دریافت فایلها در ASP.NET Core 2.0 به صورت توکار از ویژگی «از سرگیری مجدد» پشتیبانی میکند. قابلیتی که در نگارشهای پیشین ASP.NET (تمام نگارشهای آن)، به صورت پیشفرض و توکار وجود نداشت و برای پیاده سازی آن میبایستی مقدار زیادی کد نوشته میشد.
فرض کنید برای آزمایش قسمت «از سرگیری مجدد» دریافت یک فایل حجیم مطلب جاری، یک چنین کدی را در یک برنامهی ASP.NET Core 2.0 تهیه کردهاید:
public class HomeController : Controller { public IActionResult GetFile() { return PhysicalFile(@"C:\path\file.pdf", "application/pdf"); }
اجرای این کد به همراه هدر مخصوص «Accept-Ranges: bytes » که در مطلب جاری در مورد آن بحث شد نیز هست:
یعنی دریافت فایلها در ASP.NET Core 2.0 به صورت توکار از ویژگی «از سرگیری مجدد» پشتیبانی میکند. قابلیتی که در نگارشهای پیشین ASP.NET (تمام نگارشهای آن)، به صورت پیشفرض و توکار وجود نداشت و برای پیاده سازی آن میبایستی مقدار زیادی کد نوشته میشد.
در اینجا return File، FileStreamResult و VirtualFileResult نیز از ویژگی partial range requests پشتیبانی میکنند. همچنین حتی اگر از static files middleware آن نیز استفاده کنید، یک چنین قابلیتی را پیاده سازی کردهاست.
به علاوه تمام متدهای بازگشت فایل، پارامتر enableRangeProcessing را نیز به همراه دارند:
var result = new FileStreamResult(readStream, contentType) { LastModified = lastModified, EntityTag = entityTag, EnableRangeProcessing = true, }; return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag, true); return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag, enableRangeProcessing: true);
یک نکتهی تکمیلی
اگر با استفاده از مطالب فوق قصد داشته باشید در WPF یک PDF Viewer درست کنید، میتوان از متد ذیل استفاده کرد:
بعد برای استفاده از BitmapImageهای حاصل از آن، برای مثال نمایش اولین صفحه در یک کنترل Image استاندارد، میتوان نوشت:
اگر با استفاده از مطالب فوق قصد داشته باشید در WPF یک PDF Viewer درست کنید، میتوان از متد ذیل استفاده کرد:
private async Task<List<System.Windows.Media.Imaging.BitmapImage>> getPdfPageImages() { var results = new List<System.Windows.Media.Imaging.BitmapImage>(); using (var randomAccessStream = File.Open("PieChartPdfReport.pdf", FileMode.Open).AsRandomAccessStream()) { var pdfDocument = await PdfDocument.LoadFromStreamAsync(randomAccessStream); for (uint i = 0; i < pdfDocument.PageCount; i++) { using (var memoryStream = new MemoryStream()) { using (var stream = memoryStream.AsRandomAccessStream()) { using (var page = pdfDocument.GetPage(i)) { // Set render options var renderOptions = new PdfPageRenderOptions { BackgroundColor = Colors.LightGray, DestinationHeight = (uint)(page.Size.Height * 10) }; await page.RenderToStreamAsync(stream); //, renderOptions); await stream.FlushAsync(); var bitmapImage = new System.Windows.Media.Imaging.BitmapImage(); bitmapImage.BeginInit(); //Without this, BitmapImage uses lazy initialization by default and the stream will be closed by then. bitmapImage.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; bitmapImage.StreamSource = memoryStream; bitmapImage.EndInit(); results.Add(bitmapImage); } } } } } return results; }
private async void Button_Click(object sender, RoutedEventArgs e) { var images = await this.getPdfPageImages(); ImagePdf.Source = images.First(); }
روزگاری دریافت مجوزهای SSL، گران و سخت بود. برای رفع این مشکلات مؤسسههایی مانند Let's Encrypt پدیدار شدهاند که مجوزهای SSL رایگانی را برای سایتهای اینترنتی صادر میکنند. دسترسی به سرویس آنها از طریق API ارائه شدهی آن، بسیار ساده بوده، کار با آن رایگان است و نیاز به مجوز خاصی ندارد. فقط باید دقت داشت که گواهینامههای Let's Encrypt دو ماهه هستند و وبسرور سایت شما باید اجازهی دسترسی به محل ویژهای را که جهت تعیین اعتبار دومین درخواستی ایجاد میشود، صادر کند. البته درخواست گواهی مجدد و تمدید آن در هر زمانی، حتی اگر پیش از انقضای آن باشد، مسیر است و از این لحاظ محدودیتی ندارد. در ادامه نحوهی کار با این سرویس را در ویندوزهای سرور بررسی خواهیم کرد.
دریافت برنامهی win-acme
برنامهی win-acme کار دریافت، نصب و تنظیم به روز رسانی خودکار مجوزهای Let’s Encrypt را در ویندوز بسیار ساده کردهاست و تقریبا به برنامهی استاندارد انجام اینکار تبدیل شدهاست. این برنامه، انجام مراحل زیر را خودکار کردهاست:
- اسکن IIS برای یافتن bindings و نام سایتها
- اتصال به Let’s Encrypt certificate authority و دریافت مجوزهای لازم
- درج خودکار مجوزهای دریافتی در Windows certificate store
- ایجاد HTTPS binding خودکار در IIS
- استفاده از Windows Task Scheduler، جهت ایجاد وظیفهی به روز رسانی خودکار مجوزهای درخواست شده
به همین جهت پیش از هر کاری نیاز است این برنامه را دریافت کنید:
https://github.com/PKISharp/win-acme/releases
این برنامه از دات نت نگارش 4.6.2 استفاده میکند. بنابراین نیاز است این نگارش و یا ترجیحا آخرین نگارش دات نت فریم ورک را بر روی سرور نصب کنید.
آماده سازی برنامهی ASP.NET جهت دریافت مجوزهای Let’s Encrypt
را بر روی سرور شما بررسی خواهد کرد (بنابراین سرور شما باید به اینترنت متصل بوده و همچنین مجوز دسترسی به این مسیر را عمومی کند). برنامهی win-acme این id را از سرور Let’s Encrypt به صورت خودکار دریافت کرده و فایلی را در مسیر یاد شده ایجاد میکند. سپس سرور Let’s Encrypt یکبار این مسیر را خواهد خواند. مشکل اینجا است که دسترسی به این فایل بدون پسوند در برنامههای MVC به صورت پیشفرض مسیر نیست و نیازی به تنظیمات خاصی دارد:
روش انجام اینکار در ASP.NET Core به صورت زیر است:
این اکشن متد را در هر کنترلری قرار دهید، تفاوتی نمیکند و کار خواهد کرد؛ چون attribute routing آن مستقل از محل قرارگیری آن است.
در MVC 5x پارامتر env را حذف و بجای آن از Server.MapPath و در آخر از return File استفاده کنید.
اگر این مرحله را تنظیم نکنید، در وسط کار دریافت مجوز توسط برنامهی win-acme، به علت اینکه مشخص نیست آیا شما صاحب دامنه هستید یا خیر، خطایی را دریافت کرده و برنامه متوقف میشود.
آماده سازی IIS برای دریافت مجوزهای Let’s Encrypt
ابتدا به قسمت Edit bindings وب سایتی که قرار است مجوز را دریافت کند مراجعه کرده:
و سپس bindings مناسبی را ایجاد کنید (از نوع HTTP و نه HTTPS):
برای مثال اگر سایت شما قرار است توسط آدرسهای www.dotnettips.info و dotnettips.info در دسترس باشد، نیاز است دو binding را در اینجا ثبت کنید. برای تمام موارد ثبت شده هم این تنظیمات را مدنظر داشته باشید:
برنامهی win-acme بر اساس این HTTP Bindings است که معادلهای متناظر HTTPS آنها را به صورت خودکار ثبت و تنظیم میکند.
اجرای برنامهی win-acme بر روی ویندوز سرور 2008
IISهای 8 به بعد (و یا ویندوز سرور 2012 به بعد) دارای ویژگی هستند به نام Server Name Indication و یا SNI که اجاز میدهند بتوان چندین مجوز SSL را بر روی یک IP تنظیم کرد.
در اینجا به ازای هر Bindings تعریف شدهی در قسمت قبل، یک مجوز Let’s Encrypt دریافت خواهد شد. اما چون ویندوز سرور 2008 به همراه IIS 7.5 است، فاقد ویژگی SNI است. به همین جهت در حالت عادی برای مثال فقط برای www.dotnettips.info مجوزی را دریافت میکنید و اگر کاربر به آدرس dotnettips.info مراجعه کند، دیگر نمیتواند به سایت وارد شود و پیام غیرمعتبر بودن مجوز SSL را مشاهده خواهد کرد.
برنامهی win-acme برای رفع این مشکل، از ویژگی خاصی به نام «SAN certificate» پشتیبانی میکند.
به این ترتیب با ویندوز سرور 2008 هم میتوان دامنهی اصلی و زیر دامنههای تعریف شده را نیز پوشش داد و سایت به این ترتیب بدون مشکل کار خواهد کرد. مراحل تنظیم SAN توسط برنامهی win-acme به این صورت است:
ابتدا که برنامهی win-acme را با دسترسی admin اجرا میکنید، چنین منویی نمایش داده میشود:
گزینهی N یا ایجاد مجوز جدید را انتخاب کنید.
سپس منوی بعدی را نمایش میدهد:
در این حالت برای ویندوز سرور 2008، فقط و فقط گزینهی 2 را انتخاب کنید.
سپس لیست سایتهای نصب شدهی در IIS را نمایش میدهد:
در اینجا برای مثال شمارهی 1 یا هر شمارهی دیگر متناظر با وب سایت مدنظر را انتخاب کنید.
در ادامه منوی زیر را نمایش میدهد:
لیستی را که در اینجا مشاهده میکنید، همان Bindings است که پیشتر ایجاد کردیم. عنوان میکند که برای کدامیک از اینها نیاز است مجوز دریافت و نصب شود. کلید enter را فشار دهید تا برای تمام آنها اینکار صورت گیرد.
و ... همین! پس از آن کار دریافت، نصب و به روز رسانی Bindings در IIS به صورت خودکار انجام خواهد شد. اکنون اگر به قسمت Binding سایت مراجعه کنید، این تنظیمات خودکار جدید را مشاهده خواهید کرد:
اگر به لاگ نصب مجوزها دقت کنید این دو سطر نیز در انتهای آن ذکر میشوند:
علت اینجا است که مجوزهای Let’s Encrypt طول عمر کمی دارند و در صورت به روز نشدن مداوم، کاربران دیگر قادر به مرور سایت نخواهند بود. به همین جهت این برنامه یک Scheduled Task ویندوز را نیز جهت به روز رسانی خودکار این مجوزها ایجاد و تنظیم میکند.
اجرای برنامهی win-acme بر روی ویندوزهای سرور 2012 به بعد
چون IIS ویندوزهای سرور 2012 به بعد، از نصب و فعالسازی بیش از یک مجوز SSL به ازای یک IP به صورت توکار تحت عنوان ویژگی SNI پشتیبانی میکنند، مراحل انجام آن سادهتر هستند.
ابتدا که برنامهی win-acme را با دسترسی admin اجرا میکنید، چنین منویی نمایش داده میشود:
گزینهی N یا ایجاد مجوز جدید را انتخاب کنید.
سپس منوی بعدی را نمایش میدهد:
در این حالت گزینهی 4 را انتخاب کنید (با فرض اینکه از IIS 8.0 به بعد استفاده میکنید).
سپس از شما درخواست میکند که لیست دامنه و زیر دامنههایی را که قرار است برای آنها مجوز SSL صادر شوند، به صورت لیست جدا شدهی توسط کاما، وارد کنید:
در ادامه لیست وب سایتهای ثبت شدهی در IIS را نمایش میدهد:
در اینجا شمارهی سایتی را که میخواهید برای آن مجوز صادر شود، انتخاب کنید.
و ... همین! پس از آن مجوزهای SSL درخواستی، دریافت، نصب و تنظیم خواهند شد. همچنین یک Scheduled Task هم برای به روز رسانی خودکار آن تنظیم میشود.
دریافت برنامهی win-acme
برنامهی win-acme کار دریافت، نصب و تنظیم به روز رسانی خودکار مجوزهای Let’s Encrypt را در ویندوز بسیار ساده کردهاست و تقریبا به برنامهی استاندارد انجام اینکار تبدیل شدهاست. این برنامه، انجام مراحل زیر را خودکار کردهاست:
- اسکن IIS برای یافتن bindings و نام سایتها
- اتصال به Let’s Encrypt certificate authority و دریافت مجوزهای لازم
- درج خودکار مجوزهای دریافتی در Windows certificate store
- ایجاد HTTPS binding خودکار در IIS
- استفاده از Windows Task Scheduler، جهت ایجاد وظیفهی به روز رسانی خودکار مجوزهای درخواست شده
به همین جهت پیش از هر کاری نیاز است این برنامه را دریافت کنید:
https://github.com/PKISharp/win-acme/releases
این برنامه از دات نت نگارش 4.6.2 استفاده میکند. بنابراین نیاز است این نگارش و یا ترجیحا آخرین نگارش دات نت فریم ورک را بر روی سرور نصب کنید.
آماده سازی برنامهی ASP.NET جهت دریافت مجوزهای Let’s Encrypt
سرور Let’s Encrypt، در حین صدور مجوز برای سایت شما نیاز دارد بررسی کند که آیا شما واقعا صاحب همان دومین هستید یا خیر. به همین جهت مسیر
/.well-known/acme-challenge/id
[HttpGet("/.well-known/acme-challenge/{id}")] public IActionResult LetsEncrypt(string id, [FromServices] IHostingEnvironment env) { id = Path.GetFileName(id); // security cleaning var file = Path.Combine(env.ContentRootPath, ".well-known", "acme-challenge", id); return PhysicalFile(file, "text/plain"); }
در MVC 5x پارامتر env را حذف و بجای آن از Server.MapPath و در آخر از return File استفاده کنید.
[Route(".well-known/acme-challenge/{id}")] public ActionResult LetsEncrypt(string id) { id = Path.GetFileName(id); // security cleaning var file = Path.Combine(Server.MapPath("~/.well-known/acme-challenge"), id); return File(file, "text/plain", id); }
آماده سازی IIS برای دریافت مجوزهای Let’s Encrypt
ابتدا به قسمت Edit bindings وب سایتی که قرار است مجوز را دریافت کند مراجعه کرده:
و سپس bindings مناسبی را ایجاد کنید (از نوع HTTP و نه HTTPS):
برای مثال اگر سایت شما قرار است توسط آدرسهای www.dotnettips.info و dotnettips.info در دسترس باشد، نیاز است دو binding را در اینجا ثبت کنید. برای تمام موارد ثبت شده هم این تنظیمات را مدنظر داشته باشید:
Type:http Port:80 IP address:All Unassigned
اجرای برنامهی win-acme بر روی ویندوز سرور 2008
IISهای 8 به بعد (و یا ویندوز سرور 2012 به بعد) دارای ویژگی هستند به نام Server Name Indication و یا SNI که اجاز میدهند بتوان چندین مجوز SSL را بر روی یک IP تنظیم کرد.
در اینجا به ازای هر Bindings تعریف شدهی در قسمت قبل، یک مجوز Let’s Encrypt دریافت خواهد شد. اما چون ویندوز سرور 2008 به همراه IIS 7.5 است، فاقد ویژگی SNI است. به همین جهت در حالت عادی برای مثال فقط برای www.dotnettips.info مجوزی را دریافت میکنید و اگر کاربر به آدرس dotnettips.info مراجعه کند، دیگر نمیتواند به سایت وارد شود و پیام غیرمعتبر بودن مجوز SSL را مشاهده خواهد کرد.
برنامهی win-acme برای رفع این مشکل، از ویژگی خاصی به نام «SAN certificate» پشتیبانی میکند.
به این ترتیب با ویندوز سرور 2008 هم میتوان دامنهی اصلی و زیر دامنههای تعریف شده را نیز پوشش داد و سایت به این ترتیب بدون مشکل کار خواهد کرد. مراحل تنظیم SAN توسط برنامهی win-acme به این صورت است:
ابتدا که برنامهی win-acme را با دسترسی admin اجرا میکنید، چنین منویی نمایش داده میشود:
N: Create new certificate M: Create new certificate with advanced options L: List scheduled renewals R: Renew scheduled S: Renew specific A: Renew *all* V: Revoke certificate C: Cancel scheduled renewal X: Cancel *all* scheduled renewals Q: Quit
سپس منوی بعدی را نمایش میدهد:
[INFO] Running in Simple mode 1: Single binding of an IIS site 2: SAN certificate for all bindings of an IIS site 3: SAN certificate for all bindings of multiple IIS sites 4: Manually input host names C: Cancel
سپس لیست سایتهای نصب شدهی در IIS را نمایش میدهد:
1: Default Web Site C: Cancel Choose site: 1
در ادامه منوی زیر را نمایش میدهد:
* www.dotnettips.info * dotnettips.info Press enter to include all listed hosts, or type a comma-separated lists of exclusions:
و ... همین! پس از آن کار دریافت، نصب و به روز رسانی Bindings در IIS به صورت خودکار انجام خواهد شد. اکنون اگر به قسمت Binding سایت مراجعه کنید، این تنظیمات خودکار جدید را مشاهده خواهید کرد:
اگر به لاگ نصب مجوزها دقت کنید این دو سطر نیز در انتهای آن ذکر میشوند:
[INFO] Adding renewal for Default Web Site [INFO] Next renewal scheduled at 2018/7/21 4:19:20 AM
اجرای برنامهی win-acme بر روی ویندوزهای سرور 2012 به بعد
چون IIS ویندوزهای سرور 2012 به بعد، از نصب و فعالسازی بیش از یک مجوز SSL به ازای یک IP به صورت توکار تحت عنوان ویژگی SNI پشتیبانی میکنند، مراحل انجام آن سادهتر هستند.
ابتدا که برنامهی win-acme را با دسترسی admin اجرا میکنید، چنین منویی نمایش داده میشود:
N: Create new certificate M: Create new certificate with advanced options L: List scheduled renewals R: Renew scheduled S: Renew specific A: Renew *all* V: Revoke certificate C: Cancel scheduled renewal X: Cancel *all* scheduled renewals Q: Quit
سپس منوی بعدی را نمایش میدهد:
[INFO] Running in Simple mode 1: Single binding of an IIS site 2: SAN certificate for all bindings of an IIS site 3: SAN certificate for all bindings of multiple IIS sites 4: Manually input host names C: Cancel
سپس از شما درخواست میکند که لیست دامنه و زیر دامنههایی را که قرار است برای آنها مجوز SSL صادر شوند، به صورت لیست جدا شدهی توسط کاما، وارد کنید:
Enter comma-separated list of host names, starting with the primary one: dotnettips.info, www.dotnettips.info
1: Default Web Site 2: mysiteName Choose site to create new bindings: 1
و ... همین! پس از آن مجوزهای SSL درخواستی، دریافت، نصب و تنظیم خواهند شد. همچنین یک Scheduled Task هم برای به روز رسانی خودکار آن تنظیم میشود.