در متدهای keydown و keypress فوق، اگر متد ()e.preventDefault فراخوانی شود، دکمهی فشرده شده نمایش داده نخواهد شد. در همینجا بازهی key دریافتی را بررسی کنید. مثلا 97 مساوی a است تا 122 که z است. اگر خارج از آن بود متد ()e.preventDefault را فراخوانی کنید.
نظرات مطالب
نرمال سازی (قسمت دوم: Second Normal Form)
بله همینطوره.
سایت ویکی توضیحات خوبی راجب معایب و مزایای استفاده از surrogate key داده.
یه مرور بسیار جزئی که به معایب و مزایا داشتم متوجه شدم که در پست قبلیم از معایبش به normalization و از مزایاش به performance اش اشاره ای داشتم.
سایت ویکی توضیحات خوبی راجب معایب و مزایای استفاده از surrogate key داده.
یه مرور بسیار جزئی که به معایب و مزایا داشتم متوجه شدم که در پست قبلیم از معایبش به normalization و از مزایاش به performance اش اشاره ای داشتم.
نظرات مطالب
NOSQL قسمت سوم
سلام تشکر از مطلب بسیار مفیدتون .
می خواستم بدونم که کدام یک از روشها بیشتر امتحان خودش رو توی دادههای زیاد پس داده .و بشه راحتر باهاش کار کرد .
روش
Document store
Key value
روش هایی دیگری رو هم توی سایت دیدم اگر امکان داره مزایا و معایب هر کدوم رو توضیح دهید ممنون.
نظرات مطالب
EF Code First #7
قبل از ویرایش این سطر را اضافه کنید
همچنین اگر itemهای دریافتی primary key هستند میتونید از متد attach بجای مراجعه به بانک اطلاعاتی، استفاده کنید:
if(user.Roles != null && user.Roles.Any()) user.Roles.Clear();
ctx.Roles.Attach(new Role { Id = item });
پاسخ به بازخوردهای پروژهها
SettingService
سلام ؛ خواهش میکنم.
بیشتر هدف نگهداری تنظیمات سیستم خواهد بود ، که بتوان در یک جدول با فیلدهای key و Value آنها را مدیریت کرد. در مقاله ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF روش استفاده و پیاده سازی آن را توضیح داده ام.
پاسخ به بازخوردهای پروژهها
خطا هنگام اجرا
فایل کانفیگ برنامه به نام ExplorerPCal.exe.Config یک چنین محتوایی را باید داشته باشد:
همچنین برنامه نیز باید دسترسی رایت آنرا داشته باشد (بتواند مثلا true را false کند).
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="RunOnStartup" value="True" /> </appSettings> </configuration>
در قسمت قبل با مفهوم مایکرو سرویسها آشنا شدیم. سرویسهای کوچک و مجزایی که بصورت مستقل، قابلیت توسعه و استقرار دارند و در راستای انجام یک قابلیت کسب و کار در اختیار دیگران قرار میگیرند.
برای روشن شدن موضوع فرض کنید میخواهیم در بخشی از سیستم، performance را افزایش دهیم. برای این امر میتوانیم از هر تکنولوژی که به ما کمک میکند، استفاده کنیم. برای نمونه در یک سیستم شبکه اجتماعی، میتوانیم اطلاعات مربوط به کاربران و ارتباطات آنها را در یک دیتابیس مبتنی بر گراف و اطلاعات مربوط به پستها و نظرات کاربران را در یک دیتابیس سندگرا ذخیره کنیم. به این ترتیب مشاهده میکنیم که وابستهی به تکنولوژی خاصی نمیباشیم و میتوانیم بر اساس نیاز خود، از تکنولوژی مورد نظر بهره ببریم.
مایکرو سرویسها به ما این اجازه را میدهند تا در انتخاب تکنولوژیها نهایت دقت را انجام دهیم و متوجه شویم که تکنولوژیهای جدید چگونه میتوانند به ما کمک کنند.
یکی از بزرگترین موانع در استفاده و انتخاب یک تکنولوژی جدید، ایجاد وابستگی سیستم به آن میباشد. در یک سیستم یکپارچه چنانچه قصد تغییر زبان مورد استفاده یا دیتابیس یا فریمورک مورد استفاده را داشته باشیم، سیستم هزینهی سنگینی را متحمل میشود. در حالیکه در مایکرو سرویسها میتوانیم این تغییرات را با کمترین هزینه انجام دهیم. البته باید توجه داشت که استفاده از تکنولوژی جدید چالشها و سربارهای خودش را دارد و انتخاب یک تکنولوژی جدید نیازمند بررسی کارشناسانه و دقیق میباشد.
Organizational Alignment
ویژگیهای یک مایکرو سرویس چیست؟
بعد از آشنایی با معماری مایکرو سرویسها میخواهیم با ویژگیهای آن آشنا شویم. البته باید به این نکته توجه داشت که همهی معماریهای مایکروسرویسها این ویژگیها را ندارند؛ ولی میتوان انتظار داشت اکثر آنها این ویژگیها را از خود به نمایش بگذارند.
Componentization via Services
تعریف ما از component، واحدی از نرم افزار میباشد که به تنهایی قابل جایگزینی و بهروز رسانی میباشد.
همانطور که گفته شد، مایکرو سرویسها یک نرم افزار را به سرویسهای (business component) کوچکتری تقسیم میکنند که به تنهایی قابل توسعه و استقرار میباشند. ما کتابخانهها را به عنوان component در نظر میگیریم که به نرم افزار ما پیوند خوردهاند و به وسیله فراخوانی توابع در پروسه نرم افزار قابل استفاده میباشند. در حالیکه سرویسها componentهایی هستند که خارج از پروسه نرم افزار ما میباشند و به وسیله مکانیزمهای ارتباطی مانند Web Service Request و Remote Procedure Call قابل دسترسی هستند.
در سیستم های یکپارچه که از چندین component تشکیل شدهاند و این componentها در جریان یک پروسه با هم در ارتباط هستند، اگر بخواهیم در یک component تغییر ایجاد کنیم، این تغییر نیازمند استقرار مجدد کل سیستم میباشد. در حالیکه در معماری مایکرسرویس، کافی است شما component (سرویس) ای را که تغییر کردهاست، دوباره مستقر کنید و سایر بخشهای سیستم از این تغییر تاثیر نمیپذیرند.
Technology Heterogeneity
یک سیستم را در نظر بگیرید که از همکاری چندین سرویس تشکیل شدهاست. ما میتوانیم تصمیم بگیریم که برای هر کدام از سرویسها از تکنولوژی متفاوتی استفاده کنیم. این امر به ما اجازه میدهد برای حل هر مشکل، از بهترین ابزار و تکنولوژی استفاده کنیم. این امر بر خلاف سیستمهایی که تنها باید از تکنولوژی بکار رفته در سیستم استفاده کنند، انعطاف پذیری سیستم را بسیار بالا میبرد.برای روشن شدن موضوع فرض کنید میخواهیم در بخشی از سیستم، performance را افزایش دهیم. برای این امر میتوانیم از هر تکنولوژی که به ما کمک میکند، استفاده کنیم. برای نمونه در یک سیستم شبکه اجتماعی، میتوانیم اطلاعات مربوط به کاربران و ارتباطات آنها را در یک دیتابیس مبتنی بر گراف و اطلاعات مربوط به پستها و نظرات کاربران را در یک دیتابیس سندگرا ذخیره کنیم. به این ترتیب مشاهده میکنیم که وابستهی به تکنولوژی خاصی نمیباشیم و میتوانیم بر اساس نیاز خود، از تکنولوژی مورد نظر بهره ببریم.
مایکرو سرویسها به ما این اجازه را میدهند تا در انتخاب تکنولوژیها نهایت دقت را انجام دهیم و متوجه شویم که تکنولوژیهای جدید چگونه میتوانند به ما کمک کنند.
یکی از بزرگترین موانع در استفاده و انتخاب یک تکنولوژی جدید، ایجاد وابستگی سیستم به آن میباشد. در یک سیستم یکپارچه چنانچه قصد تغییر زبان مورد استفاده یا دیتابیس یا فریمورک مورد استفاده را داشته باشیم، سیستم هزینهی سنگینی را متحمل میشود. در حالیکه در مایکرو سرویسها میتوانیم این تغییرات را با کمترین هزینه انجام دهیم. البته باید توجه داشت که استفاده از تکنولوژی جدید چالشها و سربارهای خودش را دارد و انتخاب یک تکنولوژی جدید نیازمند بررسی کارشناسانه و دقیق میباشد.
Resilience
چنانچه یکی ازسرویسها دچار اشکال شود و این مسئله بصورت زنجیروار برای تمام سرویسها رخ ندهد، با جدا شدن آن سرویس، درروند کار سیستم خللی بوجود نمیآید و سیستم بدون کمترین مشکلی به ادامه کار خود میپردازد. یعنی محدوده یک سرویس، دیوار حائلی میشود تا سایر بخشها تاثیر نپذیرند. در سیستمهای یکپارچه چنانچه یک سرویس دچار اشکال شود، سایر بخشهای سیستم نیز غیر قابل استفاده میشوند. البته میتوان با نصب نسخههای متفاوتی بر روی ماشینهای متفاوت (Scale out)، تاثیر اینگونه اختلالات را تا حدودی کاهش داد. اما بوسیله مایکرو سرویسها میتوانیم سیستمهایی را بسازیم که قادرند با خطاهای بوجود آمده در سرویسها بهدرستی رفتار کنند؛ تا خللی در کار سیستم ایجاد نشود.Scaling
فرض کنید در بخشی ازیک سیستم، دغدغه Performance بوجود میآید. چنانچه ما یک معماری یکپارچه را داشته باشیم، مجبوریم کل آن را scale کنیم. درحالیکه این مشکل تنها در بخش کوچکی از نرم افزار ایجاد شدهاست. اما اگرمعماری ما مایکرو سرویس باشد، فقط همان سرویس مربوطه را که بر روی آن دغدغه داریم، scale میکنیم و سایر بخشهای نرم افزار بدون نیاز به تغییر و بزرگ شدن، به کار خود ادامه میدهند.
Gilt یک خرده فروش آنلاین در صنعت مد میباشد که بخاطر مسائل Performance به معماری مایکروسرویسها روی آورد. آنها در سال 2007 با یک نرم افزار یکپارچه شروع به کار کردند. اما بعد از دوسال سیستم آنها قادر به مقابله با ترافیک سایت نبود. آنها با تقسیم بندی قسمتهای اصلی سیستم خود به مایکرو سرویسها، توانستند خیلی بهتر و موثرتر با ترافیک رسیده مقابله کنند و قابلیت دسترسی پذیری سیستم را افزایش دهند. در حال حاضر سیستم آنها از حدود 450 مایکرو سرویس تشکیل شده که هر کدام روی چندین ماشین مختلف در حال اجرا میباشند.
Ease of Deployment
تغییر حتی یک خط کد، در برنامهای که از چندین میلیون خط تشکیل شده است، ما را ملزم به استقرار مجدد کل سیستم و انتشار نسخه جدید میکند. این استقرار میتواند تاثیر و ریسک بالایی را داشته باشد و ما را مجبور به تغییرات بیشتر و انتشار نسخههای دیگر کند که این حجم زیاد تغییرات ممکن است به محصول ما ضربه بزند.
اما در مایکرو سرویسها یک تغییر کوچک، تمام سیستم را تحت تاثیر قرار نمیدهد. بلکه تنها تغییرات مربوط به سرویس مورد نظر میباشد که به صورت مستقل قابلیت استقرار را دارد و چنانچه سیستم دچار مشکل شود، منشاء تغییرات کاملا مشخصی میباشد و میتوان سریعتر سیستم را به وضعیت قابل اطمینان بازگرداند. این ویژگی یکی از مهمترین دلایلی میباشد که شرکتهایی مانند Netflix و Amazon به پیاده سازی این معماری روی آوردهاند.
Organizational Alignment
بسیاری از ما مشکلات مربوط به پروژههای بزرگ و تیمهای بزرگ را تجربه کردهایم و این مشکلات زمانیکه اعضای تیم بهصورت متمرکز در کنار هم نباشند، افزایش پیدا میکند. ما میدانیم که اگر یک تیم کوچک، بر روی قطعه کد کوچکتری کار کند، میتواند بسیار موثرتر باشد تا زمانیکه یک تیم بزرگ، درگیر یک کد بزرگ شود.
مایکرو سرویسها این امکان را به ما میدهند تا معماری خود را با ساختار سازمانی خود هم راستا کنیم و به ما کمک میکنند تا با تقسیم ساختار نرم افزار به بخشهای کوچکتر وایجاد تیمهای کوچکتر مختص هر بخش، بازدهی و تاثیر تیمها را در سازمان، افزایش دهیم.
Composability
یکی از ویژگیهای کلیدی که در سیستمهای توزیع شده و سرویس گرا به آن وعده داده میشود، قابلیت استفاده مجدد از functionalityهای سیستم میباشد. مایکرو سرویسها این امکان را برای ما فراهم میکنند تا functionality های سیستم، به روشهای مختلف و با اهداف گوناگونی مورد استفاده قرار گیرند. اینکه استفاده کنندگان نرمافزار ما چگونه از آن استفاده میکنند، برای ما مهم است. در حال حاضر ما باید درباره راههای مختلفی که میتوانند قابلیتهای سیستم را باهم ترکیب کنند تا در برنامههای وب، موبایل و یا ابزارهای پوشیدنی مورد استفاده قرار گیرند، فکر کنیم. در مایکرو سرویس تفکر ما این است که بخشهای مختلف سیستم خود را از هم جدا کنیم و این بخشها توسط استفاده کنندههای بیرونی قابل دسترسی باشند و با تغییر شرایط بتوانیم بخشهای گوناگونی را بسازیم.
Optimizing for Replaceability
اگر شما در یک سازمان متوسط و یا بزرگ، مشغول به کار باشید، ممکن است با یک سیستم بزرگ و قدیمی مواجه شده باشید که در گوشهای از سازمان در حال استفاده میباشد و هیچ کسی رغبت نزدیک شدن به آن و دست بردن در آن را ندارد و اگر کسی از شما بپرسد «چرا نمیتوان آن را تغییر داد؟» خواهید گفت این کار، بسیار پرریسک و پردردسر است.
با استفاده از معماری مایکروسرویس، هزینه این تغییرات به مراتب کمتر و تغییرات با ضریب اطمینان بالاتری انجام میشوند.
ASP.NET Core از مکانیزم «Data protection» برای تولید کلیدهای رمزنگاری اطلاعات موقتی خود استفاده میکند. این روش در دو حالت هاست برنامهها توسط IIS و یا عدم تنظیمات ذخیره سازی آنها به صورت دائمی، اطلاعات خود را در حافظه نگهداری میکند و با ریاستارت شدن سرور و یا IIS، این کلیدها از دست رفته و مجددا تولید میشوند. به این ترتیب کاربران شاهد این مشکلات خواهند بود:
الف) چون کوکیها و یا توکنهای آنها دیگر قابل رمزگشایی نیستند (به علت باز تولید کلیدهای رمزنگاری و رمزگشایی اطلاعات)، مجبور به لاگین مجدد خواهند شد (تا کوکیهای جدیدی برای آنها تولید شوند). همچنین آنتیفورجری توکنهای آنها نیز مجددا باید تولید شوند.
ب) تمام اطلاعات محافظت شدهی توسط Data protection API قابل رمزگشایی نخواهند بود.
تنظیم Data protection API مخصوص برنامههای هاست شدهی توسط IIS
برای اینکه کلیدهای رمزنگاری اطلاعات برنامههای وب به صورت دائمی ذخیره شوند و با ریاستارت سرور از دست نروند، یکی از سه روش ذیل را میتوان بکار گرفت:
1) اسکریپت پاور شل ذیل را اجرا کنید:
نحوهی اجرای آن نیز به صورت ذیل است و پس از آن، نام Application pool مخصوص برنامه ذکر میشود:
در این حالت کلیدهای رمزنگاری اطلاعات به صورت دائمی به رجیستری ویندوز اضافه میشوند. این کلیدها به صورت خودکار توسط مکانیزم DPAPI ویندوز، رمزنگاری میشوند.
2) به تنظیمات پیشرفتهی Application pool برنامه در IIS مراجعه کرده و خاصیت Load user profile آنرا true کنید.
در این حالت کلیدها به صورت دائمی در پوشهی پروفایل کاربر مخصوص Application pool برنامه، به صورت رمزنگاری شدهی توسط مکانیزم DPAPI ویندوز، ذخیره خواهند شد.
3) یک SSL Certificate معتبر را تهیه کنید و یا اگر از یک self signed certificate استفاده میکنید باید آنرا در Trusted Root store ویندوز قرار دهید. سپس از روش PersistKeysToFileSystem استفاده کنید.
اگر از یک web farm استفاده میکنید، روش سوم ذکر شده، تنها روشی است که از آن میتوانید استفاده کنید. یک پوشهی اشتراکی قابل دسترسی بین سرورها را ایجاد کنید که دربرگیرندهی X509 certificate شما باشد. سپس این پوشه و مجوز موجود در آنرا توسط روش فوق به برنامه معرفی کنید.
الف) چون کوکیها و یا توکنهای آنها دیگر قابل رمزگشایی نیستند (به علت باز تولید کلیدهای رمزنگاری و رمزگشایی اطلاعات)، مجبور به لاگین مجدد خواهند شد (تا کوکیهای جدیدی برای آنها تولید شوند). همچنین آنتیفورجری توکنهای آنها نیز مجددا باید تولید شوند.
ب) تمام اطلاعات محافظت شدهی توسط Data protection API قابل رمزگشایی نخواهند بود.
تنظیم Data protection API مخصوص برنامههای هاست شدهی توسط IIS
برای اینکه کلیدهای رمزنگاری اطلاعات برنامههای وب به صورت دائمی ذخیره شوند و با ریاستارت سرور از دست نروند، یکی از سه روش ذیل را میتوان بکار گرفت:
1) اسکریپت پاور شل ذیل را اجرا کنید:
نحوهی اجرای آن نیز به صورت ذیل است و پس از آن، نام Application pool مخصوص برنامه ذکر میشود:
.\Provision-AutoGenKeys.ps1 DefaultAppPool
2) به تنظیمات پیشرفتهی Application pool برنامه در IIS مراجعه کرده و خاصیت Load user profile آنرا true کنید.
در این حالت کلیدها به صورت دائمی در پوشهی پروفایل کاربر مخصوص Application pool برنامه، به صورت رمزنگاری شدهی توسط مکانیزم DPAPI ویندوز، ذخیره خواهند شد.
3) یک SSL Certificate معتبر را تهیه کنید و یا اگر از یک self signed certificate استفاده میکنید باید آنرا در Trusted Root store ویندوز قرار دهید. سپس از روش PersistKeysToFileSystem استفاده کنید.
public void ConfigureServices(IServiceCollection services) { services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\")) .ProtectKeysWithCertificate("thumbprint"); }
اگر از یک web farm استفاده میکنید، روش سوم ذکر شده، تنها روشی است که از آن میتوانید استفاده کنید. یک پوشهی اشتراکی قابل دسترسی بین سرورها را ایجاد کنید که دربرگیرندهی X509 certificate شما باشد. سپس این پوشه و مجوز موجود در آنرا توسط روش فوق به برنامه معرفی کنید.
در قسمتهای Blazor Server مثال این سری، با روش کار با سرویسهای سمت سرور برنامه، آشنا شدیم. در این نوع برنامهها، فقط کافی است اصل سرویس مدنظر را مستقیما در کامپوننتهای Razor تزریق کرد و سپس میتوان به نحو متداولی با آنها کار کرد؛ اما در برنامههای Blazor WASM خیر! به این نوع برنامههای سمت کلاینت باید همانند برنامههای React ، Angular ، Vue و یا حتی برنامههای مبتنی بر jQuery نگاه کرد. در تمام فناوریهای سمت کلاینت، این درخواستهای Ajax ای هستند که با سرویسهای یک Web API سمت سرور، ارتباط برقرار کرده، اطلاعاتی را به آنها ارسال و یا دریافت میکنند. در برنامههای Blazor WASM نیز باید به همین ترتیب عمل کرد و در اینجا HttpClient دات نت، جایگزین برای مثال jQuery Ajax ، Fetch API و یا XMLHttpRequest استاندارد میشود (البته jQuery Ajax در اصل یک محصور کنندهی استاندارد XMLHttpRequest است که برای اولین بار توسط مایکروسافت در برنامهی Outlook web access معرفی شد).
ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاقها از Web API
در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاقها و یا یک اتاق مشخص به کلاینت است. اکنون میخواهیم در ادامهی قسمت قبل، اگر کاربری بر روی دکمهی Go صفحهی اول رزرو اتاقی کلیک کرد، لیست تمام اتاقهای تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
این سرویس را در پوشهی Services پروژهی BlazorWasm.Client ایجاد کردهایم که HotelRoomDTO خود را از پروژهی BlazorServer.Models دریافت میکند. به این ترتیب میتوان مدلی را بین یک Web API سمت سرور و یک سرویس سمت کلاینت، به اشتراک گذاشت. بنابراین پروژهی کلاینت، باید ارجاعی را به پروژهی BlazorServer.Models.csproj نیز داشته باشد.
در ادامه اینترفیس فوق را به صورت زیر پیاده سازی میکنیم:
توضیحات:
- HttpClient یکی از سرویسهای تنظیم شدهی در فایل Program.cs پروژههای سمت کلاینت است. بنابراین میتوان آنرا از طریق تزریق به سازندهی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شدهاست. این مورد جزو تازههای NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده میکنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده میکنید:
- در اینجا توسط متد AddParameter، کار افزودن کوئری استرینگها به یک Url از پیش مشخص، انجام میشود. کار encoding نهایی به صورت خودکار توسط HttpUtility استاندارد دات نت، انجام خواهد شد.
- تاریخهای ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالبهای پذیرفته شدهاست.
- جهت سهولت استفادهی از سرویس فوق و همچنین مدلهای برنامه، فضای نام آنها را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامهی سمت کلاینت، قابل دسترسی شوند:
- در آخر این سرویس جدید را باید به لیست سرویسهای برنامه معرفی کرد تا قابلیت تزریق در کامپوننتها را پیدا کند:
چند اصلاح جزئی در کنترلرها و سرویسهای سمت سرور
در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
و همچنین سرویس سمت سرور IHotelRoomService نیز اضافه میکنیم:
البته فعلا پیاده سازی خاصی ندارند و آنها را در قسمتهای بعد مورد استفاده قرار خواهیم داد.
تنظیمات ویژهی HttpClient برنامهی سمت کلاینت
سرویس ClientHotelRoomService فوق، از HttpClient تزریق شدهی در سازندهی خود استفاده میکند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت میکند. در اینجا علاقمندیم تا بجای این تنظیم پیشفرض، فایل جدید appsettings.json را به پوشهی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامههای سمت کلاینت، داخل پوشهی wwwroot است و نه در داخل پوشهی ریشهی اصلی پروژه):
و از این تنظیم جدید به عنوان BaseAddress برنامهی Web API استفاده کنیم که روش آنرا در کدهای ذیل مشاهده میکنید:
تکمیل کامپوننت دریافت لیست تمام اتاقها
در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمهی Go صفحهی اول، به این کامپوننت هدایت میشوند. اکنون میخواهیم، لیست تمام اتاقها را در این کامپوننت، از Web API برنامه دریافت کنیم:
در اینجا در ابتدا سعی میشود تا HomeModel، از Local Storage که در قسمت قبل آنرا تنظیم کردیم، خوانده شود. سپس با استفاده از متد GetHotelRoomsAsync، لیست اتاقها را از Web API دریافت میکنیم. تمام این عملیات آغازین نیز باید در روال رویدادگران OnInitializedAsync صورت گیرند.
روش اجرای پروژههای Blazor WASM
تا اینجا اگر برنامهی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحهی خالی نمایش لیست اتاقها ظاهر میشود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژهی Web API را اجرا کرد و سپس پروژهی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده میکنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینهی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینهی «multiple startup projects» را انتخاب کرده و از لیست پروژههای موجود، دو پروژهی Web API و WASM را انتخاب کنید و Action مقابل آنها را به Start تنظیم کنید. در اینجا حتی میتوان ترتیب اجرای این پروژهها را هم تغییر داد. در این حالت زمانیکه بر روی دکمهی Run، در ویژوال استودیو کلیک میکنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.
نکتهی مهم! در این حالت هم برنامهی کلاینت نمیتواند با برنامهی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شدهی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژهی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
و یا اگر میخواهید پروژه را از طریق NET Core CLI. با اجرای دستور dotnet watch run اجرا کنید ... به صورت پیشفرض نمیشود! چون برای اینکار باید به پوشهی ریشهی پروژههای Web API و WASM وارد شد و دوبار دستور یاد شده را به صورت مجزا اجرا کرد. در این حالت، هر دو پروژه، بر روی پورت پیشفرض 5001 اجرا میشوند. روش تغییر این پورت، مراجعه به فایل Properties\launchSettings.json این پروژهها است. برای مثال همان پورت پیشفرض 5001 را که در فایل appsettings.json انتخاب کردیم، ثابت نگه میداریم. یعنی فایل launchSettings.json پروژهی Web API را ویرایش نمیکنیم. اما این پورت را در پروژهی کلاینت برای مثال به عدد 5002 تغییر میدهیم تا برنامهی کلاینت، بر روی پورت پیشفرض برنامهی Web API اجرا نشود:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-27.zip
ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاقها از Web API
در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاقها و یا یک اتاق مشخص به کلاینت است. اکنون میخواهیم در ادامهی قسمت قبل، اگر کاربری بر روی دکمهی Go صفحهی اول رزرو اتاقی کلیک کرد، لیست تمام اتاقهای تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
namespace BlazorWasm.Client.Services { public interface IClientHotelRoomService { public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate); public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate); } }
در ادامه اینترفیس فوق را به صورت زیر پیاده سازی میکنیم:
namespace BlazorWasm.Client.Services { public class ClientHotelRoomService : IClientHotelRoomService { private readonly HttpClient _httpClient; public ClientHotelRoomService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate) { throw new NotImplementedException(); } public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, "/api/hotelroom")) .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}") .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}") .Uri; return _httpClient.GetFromJsonAsync<IEnumerable<HotelRoomDTO>>(uri); } } }
- HttpClient یکی از سرویسهای تنظیم شدهی در فایل Program.cs پروژههای سمت کلاینت است. بنابراین میتوان آنرا از طریق تزریق به سازندهی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شدهاست. این مورد جزو تازههای NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده میکنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده میکنید:
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; using BlazorServer.Models; using BlazorWasm.Client.Utils; using System; using System.Collections.Specialized; using System.Web; namespace BlazorWasm.Client.Utils { public class UriBuilderExt { private readonly NameValueCollection _collection; private readonly UriBuilder _builder; public UriBuilderExt(Uri uri) { _builder = new UriBuilder(uri); _collection = HttpUtility.ParseQueryString(string.Empty); } public UriBuilderExt AddParameter(string key, string value) { _collection.Add(key, value); return this; } public Uri Uri { get { _builder.Query = _collection.ToString(); return _builder.Uri; } } } }
- تاریخهای ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالبهای پذیرفته شدهاست.
- جهت سهولت استفادهی از سرویس فوق و همچنین مدلهای برنامه، فضای نام آنها را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامهی سمت کلاینت، قابل دسترسی شوند:
@using BlazorWasm.Client.Services @using BlazorServer.Models
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientHotelRoomService, ClientHotelRoomService>(); // ... } } }
چند اصلاح جزئی در کنترلرها و سرویسهای سمت سرور
در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]")] [ApiController] public class HotelRoomController : ControllerBase { // ... [HttpGet] public async Task<IActionResult> GetHotelRooms(DateTime? checkInDate, DateTime? checkOutDate) { // ... } [HttpGet("{roomId}")] public async Task<IActionResult> GetHotelRoom(int? roomId, DateTime? checkInDate, DateTime? checkOutDate) { // ... } } }
namespace BlazorServer.Services { public interface IHotelRoomService : IDisposable { Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDate, DateTime? checkOutDate); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate); // ... } }
تنظیمات ویژهی HttpClient برنامهی سمت کلاینت
سرویس ClientHotelRoomService فوق، از HttpClient تزریق شدهی در سازندهی خود استفاده میکند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت میکند. در اینجا علاقمندیم تا بجای این تنظیم پیشفرض، فایل جدید appsettings.json را به پوشهی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامههای سمت کلاینت، داخل پوشهی wwwroot است و نه در داخل پوشهی ریشهی اصلی پروژه):
{ "BaseAPIUrl": "https://localhost:5001/" }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl")) }); // ... } } }
تکمیل کامپوننت دریافت لیست تمام اتاقها
در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمهی Go صفحهی اول، به این کامپوننت هدایت میشوند. اکنون میخواهیم، لیست تمام اتاقها را در این کامپوننت، از Web API برنامه دریافت کنیم:
@page "/hotel/rooms" @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime @inject IClientHotelRoomService HotelRoomService <h3>HotelRooms</h3> @code { HomeVM HomeModel = new HomeVM(); IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { try { var model = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking); if (model is not null) { HomeModel = model; } else { HomeModel.NoOfNights = 1; } await LoadRooms(); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } private async Task LoadRooms() { Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate); } }
روش اجرای پروژههای Blazor WASM
تا اینجا اگر برنامهی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحهی خالی نمایش لیست اتاقها ظاهر میشود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژهی Web API را اجرا کرد و سپس پروژهی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده میکنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینهی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینهی «multiple startup projects» را انتخاب کرده و از لیست پروژههای موجود، دو پروژهی Web API و WASM را انتخاب کنید و Action مقابل آنها را به Start تنظیم کنید. در اینجا حتی میتوان ترتیب اجرای این پروژهها را هم تغییر داد. در این حالت زمانیکه بر روی دکمهی Run، در ویژوال استودیو کلیک میکنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.
نکتهی مهم! در این حالت هم برنامهی کلاینت نمیتواند با برنامهی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شدهی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژهی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
{ "iisSettings": { "iisExpress": { "applicationUrl": "http://localhost:62930", "sslPort": 44314 } } }
{ "BlazorWasm.Client": { "applicationUrl": "https://localhost:5002;http://localhost:5003", } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-27.zip
اگر به فایل مجوز استفاده از برنامهای مانند EF Profiler دقت کنید، یک فایل XML به ظاهر ساده بیشتر نیست:
در این فایل ساده متنی، نوع مجوز استفاده از برنامه، Trial ذکر شده است. شاید عنوان کنید که خوب ... نوع مجوز را به Standard تغییر میدهیم و فایل را ذخیره کرده و نهایتا استفاده میکنیم. اما ... نتیجه حاصل کار نخواهد کرد! حتی اگر یک نقطه به اطلاعات این فایل اضافه شود، دیگر قابل استفاده نخواهد بود. علت آن هم به قسمت Signature فایل XML فوق بر میگردد.
در ادامه به نحوه تولید و استفاده از یک چنین مجوزهای امضاء شدهای در برنامههای دات نتی خواهیم پرداخت.
تولید کلیدهای RSA
برای تهیه امضای دیجیتال یک فایل XML نیاز است از الگوریتم RSA استفاده شود.
برای تولید فایل XML امضاء شده، از کلید خصوصی استفاده خواهد شد. برای خواندن اطلاعات مجوز (فایل XML امضاء شده)، از کلیدهای عمومی که در برنامه قرار میگیرند کمک خواهیم گرفت (برای نمونه برنامه EF Prof این کلیدها را در قسمت Resourceهای خود قرار داده است).
استفاده کننده تنها زمانی میتواند مجوز معتبری را تولید کند که دسترسی به کلیدهای خصوصی تولید شده را داشته باشد.
امکان تولید کلیدهای اتفاقی مورد استفاده در الگوریتم RSA، در دات نت پیش بینی شده است. خروجی متد فوق یک فایل XML است که به همین نحو در صورت نیاز توسط متد provider.FromXmlString مورد استفاده قرار خواهد گرفت.
تهیه ساختار مجوز
در ادامه یک enum که بیانگر انواع مجوزهای برنامه ما است را مشاهده میکنید:
به همراه کلاسی که اطلاعات مجوز تولیدی را دربر خواهد گرفت:
خواص این کلاس یا عناصر enum یاد شده کاملا دلخواه هستند و نقشی را در ادامه بحث نخواهند داشت؛ از این جهت که از مباحث XmlSerializer برای تبدیل وهلهای از شیء مجوز به معادل XML آن استفاده میشود. بنابراین المانهای آنرا مطابق نیاز خود میتوانید تغییر دهید. همچنین ذکر ویژگی XmlAttribute نیز اختیاری است. در اینجا صرفا جهت شبیه سازی معادل مثالی که در ابتدای بحث مطرح شد، از آن استفاده شده است. این ویژگی راهنمایی است برای کلاس XmlSerializer تا خواص مزین شده با آنرا به شکل یک Attribute در فایل نهایی ثبت کند.
تولید و خواندن مجوز دارای امضای دیجیتال
کدهای کامل کلاس تولید و خواندن یک مجوز دارای امضای دیجیتال را در اینجا مشاهده میکنید:
توضیحات:
در حین کار با متد CreateLicense، پارامتر licensePrivateKey همان اطلاعاتی است که به کمک متد CreateRSAKeyPair قابل تولید است. توسط پارامتر licenseData، اطلاعات مجوز در حال تولید اخذ میشود. در این متد به کمک provider.FromXmlString، اطلاعات کلیدهای RSA دریافت خواهند شد. سپس توسط متد createXmlDocument، محتوای licenseData دریافتی به یک فایل XML نگاشت میگردد (بنابراین اهمیتی ندارد که حتما از ساختار کلاس مجوز یاد شده استفاده کنید). در ادامه متد getXmlDigitalSignature با در اختیار داشتن معادل XML شیء مجوز و کلیدهای لازم، امضای دیجیتال متناظری را تولید میکند. با استفاده از متد appendDigitalSignature، این امضاء را به فایل XML اولیه اضافه میکنیم. از این امضاء جهت بررسی اعتبار مجوز برنامه در متد ReadLicense استفاده خواهد شد.
برای خواندن یک فایل مجوز امضاء شده در برنامه خود میتوان از متد ReadLicense استفاده کرد. توسط آرگومان licensePublicKey، اطلاعات کلید عمومی دریافت میشود. این کلید دربرنامه، ذخیره و توزیع میگردد. پارامتر xmlFileContent معادل محتوای فایل XML مجوزی است که قرار است مورد ارزیابی قرار گیرد.
مثالی در مورد نحوه استفاده از کلاس تولید مجوز
در ادامه نحوه استفاده از متدهای CreateLicense و ReadLicense را ملاحظه میکنید؛ به همراه آشنایی با نمونه کلیدهایی که باید به همراه برنامه منتشر شوند:
ابتدا توسط متد CreateRSAKeyPair کلیدهای لازم را تهیه و ذخیره کنید. این کار یکبار باید صورت گیرد.
همانطور که مشاهده میکنید، اطلاعات کامل یک نمونه از آن، در متد writeLicense مورد نیاز است. اما در متد readLicense تنها به قسمت عمومی آن یعنی Modulus و Exponent نیاز خواهد بود (موارد قابل انتشار به همراه برنامه).
سؤال: امنیت این روش تا چه اندازه است؟
پاسخ: تا زمانیکه کاربر نهایی به کلیدهای خصوصی شما دسترسی پیدا نکند، امکان تولید معادل آنها تقریبا در حد صفر است و به طول عمر او قد نخواهد داد!
اما ... مهاجم میتواند کلیدهای عمومی و خصوصی خودش را تولید کند. مجوز دلخواهی را بر این اساس تهیه کرده و سپس کلید عمومی جدید را در برنامه، بجای کلیدهای عمومی شما درج (patch) کند! بنابراین روش بررسی اینکه آیا برنامه جاری patch شده است یا خیر را فراموش نکنید. یا عموما مطابق معمول قسمتی از برنامه که در آن مقایسهای بین اطلاعات دریافتی و اطلاعات مورد انتظار وجود دارد، وصله میشوند؛ این مورد عمومی است و منحصر به روش جاری نمیباشد.
دریافت نسخه جنریک این مثال:
SignedXmlSample.zip
<?xml version="1.0" encoding="utf-8"?> <license id="17d46246-a6cb-4196-98a0-ff6fc08cb67f" expiration="2012-06-12T00:00:00.0000000" type="Trial" prof="EFProf"> <name>MyName</name> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI=""> <Transforms> <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> <DigestValue>b8N0bDE4gTakfdGKtzDflmmyyXI=</DigestValue> </Reference> </SignedInfo> <SignatureValue>IPELgc9BbkD8smXSe0sGqp5vS57CtZo9ME2ZfXSq/thVu...=</SignatureValue> </Signature> </license>
در ادامه به نحوه تولید و استفاده از یک چنین مجوزهای امضاء شدهای در برنامههای دات نتی خواهیم پرداخت.
تولید کلیدهای RSA
برای تهیه امضای دیجیتال یک فایل XML نیاز است از الگوریتم RSA استفاده شود.
برای تولید فایل XML امضاء شده، از کلید خصوصی استفاده خواهد شد. برای خواندن اطلاعات مجوز (فایل XML امضاء شده)، از کلیدهای عمومی که در برنامه قرار میگیرند کمک خواهیم گرفت (برای نمونه برنامه EF Prof این کلیدها را در قسمت Resourceهای خود قرار داده است).
استفاده کننده تنها زمانی میتواند مجوز معتبری را تولید کند که دسترسی به کلیدهای خصوصی تولید شده را داشته باشد.
public static string CreateRSAKeyPair(int dwKeySize = 1024) { using (var provider = new RSACryptoServiceProvider(dwKeySize)) { return provider.ToXmlString(includePrivateParameters: true); } }
تهیه ساختار مجوز
در ادامه یک enum که بیانگر انواع مجوزهای برنامه ما است را مشاهده میکنید:
namespace SignedXmlSample { public enum LicenseType { None, Trial, Standard, Personal } }
using System; using System.Xml.Serialization; namespace SignedXmlSample { public class License { [XmlAttribute] public Guid Id { set; get; } public string Domain { set; get; } [XmlAttribute] public string IssuedTo { set; get; } [XmlAttribute] public DateTime Expiration { set; get; } [XmlAttribute] public LicenseType Type { set; get; } } }
تولید و خواندن مجوز دارای امضای دیجیتال
کدهای کامل کلاس تولید و خواندن یک مجوز دارای امضای دیجیتال را در اینجا مشاهده میکنید:
using System; using System.IO; using System.Security.Cryptography; // needs a ref. to `System.Security.dll` asm. using System.Security.Cryptography.Xml; using System.Text; using System.Xml; using System.Xml.Serialization; namespace SignedXmlSample { public static class LicenseGenerator { public static string CreateLicense(string licensePrivateKey, License licenseData) { using (var provider = new RSACryptoServiceProvider()) { provider.FromXmlString(licensePrivateKey); var xmlDocument = createXmlDocument(licenseData); var xmlDigitalSignature = getXmlDigitalSignature(xmlDocument, provider); appendDigitalSignature(xmlDocument, xmlDigitalSignature); return xmlDocumentToString(xmlDocument); } } public static string CreateRSAKeyPair(int dwKeySize = 1024) { using (var provider = new RSACryptoServiceProvider(dwKeySize)) { return provider.ToXmlString(includePrivateParameters: true); } } public static License ReadLicense(string licensePublicKey, string xmlFileContent) { var doc = new XmlDocument(); doc.LoadXml(xmlFileContent); using (var provider = new RSACryptoServiceProvider()) { provider.FromXmlString(licensePublicKey); var nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("sig", "http://www.w3.org/2000/09/xmldsig#"); var xml = new SignedXml(doc); var signatureNode = (XmlElement)doc.SelectSingleNode("//sig:Signature", nsmgr); if (signatureNode == null) throw new InvalidOperationException("This license file is not signed."); xml.LoadXml(signatureNode); if (!xml.CheckSignature(provider)) throw new InvalidOperationException("This license file is not valid."); var ourXml = xml.GetXml(); if (ourXml.OwnerDocument == null || ourXml.OwnerDocument.DocumentElement == null) throw new InvalidOperationException("This license file is coruppted."); using (var reader = new XmlNodeReader(ourXml.OwnerDocument.DocumentElement)) { var xmlSerializer = new XmlSerializer(typeof(License)); return (License)xmlSerializer.Deserialize(reader); } } } private static void appendDigitalSignature(XmlDocument xmlDocument, XmlNode xmlDigitalSignature) { xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(xmlDigitalSignature, true)); } private static XmlDocument createXmlDocument(License licenseData) { var serializer = new XmlSerializer(licenseData.GetType()); var sb = new StringBuilder(); using (var writer = new StringWriter(sb)) { var ns = new XmlSerializerNamespaces(); ns.Add("", ""); serializer.Serialize(writer, licenseData, ns); var doc = new XmlDocument(); doc.LoadXml(sb.ToString()); return doc; } } private static XmlElement getXmlDigitalSignature(XmlDocument xmlDocument, AsymmetricAlgorithm key) { var xml = new SignedXml(xmlDocument) { SigningKey = key }; var reference = new Reference { Uri = "" }; reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); xml.AddReference(reference); xml.ComputeSignature(); return xml.GetXml(); } private static string xmlDocumentToString(XmlDocument xmlDocument) { using (var ms = new MemoryStream()) { var settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 }; var xmlWriter = XmlWriter.Create(ms, settings); xmlDocument.Save(xmlWriter); ms.Position = 0; return new StreamReader(ms).ReadToEnd(); } } } }
در حین کار با متد CreateLicense، پارامتر licensePrivateKey همان اطلاعاتی است که به کمک متد CreateRSAKeyPair قابل تولید است. توسط پارامتر licenseData، اطلاعات مجوز در حال تولید اخذ میشود. در این متد به کمک provider.FromXmlString، اطلاعات کلیدهای RSA دریافت خواهند شد. سپس توسط متد createXmlDocument، محتوای licenseData دریافتی به یک فایل XML نگاشت میگردد (بنابراین اهمیتی ندارد که حتما از ساختار کلاس مجوز یاد شده استفاده کنید). در ادامه متد getXmlDigitalSignature با در اختیار داشتن معادل XML شیء مجوز و کلیدهای لازم، امضای دیجیتال متناظری را تولید میکند. با استفاده از متد appendDigitalSignature، این امضاء را به فایل XML اولیه اضافه میکنیم. از این امضاء جهت بررسی اعتبار مجوز برنامه در متد ReadLicense استفاده خواهد شد.
برای خواندن یک فایل مجوز امضاء شده در برنامه خود میتوان از متد ReadLicense استفاده کرد. توسط آرگومان licensePublicKey، اطلاعات کلید عمومی دریافت میشود. این کلید دربرنامه، ذخیره و توزیع میگردد. پارامتر xmlFileContent معادل محتوای فایل XML مجوزی است که قرار است مورد ارزیابی قرار گیرد.
مثالی در مورد نحوه استفاده از کلاس تولید مجوز
در ادامه نحوه استفاده از متدهای CreateLicense و ReadLicense را ملاحظه میکنید؛ به همراه آشنایی با نمونه کلیدهایی که باید به همراه برنامه منتشر شوند:
using System; using System.IO; namespace SignedXmlSample { class Program { static void Main(string[] args) { //Console.WriteLine(LicenseGenerator.CreateRSAKeyPair()); writeLicense(); readLicense(); Console.WriteLine("Press a key..."); Console.ReadKey(); } private static void readLicense() { var xml = File.ReadAllText("License.xml"); const string publicKey = @"<RSAKeyValue> <Modulus> mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc= </Modulus> <Exponent> AQAB </Exponent> </RSAKeyValue>"; var result = LicenseGenerator.ReadLicense(publicKey, xml); Console.WriteLine(result.Domain); Console.WriteLine(result.IssuedTo); } private static void writeLicense() { const string rsaData = @"<RSAKeyValue> <Modulus> mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc= </Modulus> <Exponent> AQAB </Exponent> <P> xwPKN77EcolMTD2O2Csv6k9Y4aen8UBVYjeQ4PtrNGz0Zx6I1MxLEFzRpiKC/Ney3xKg0Icwj0ebAQ04d5+HAQ== </P> <Q> w568t0Xe6OBUfCyAuo7tTv4eLgczHntVLpjjcxdUksdVw7NJtlnOLApJVJ+U6/85Z7Ji+eVhuN91yn04pQkAtw== </Q> <DP> svkEjRdA4WP5uoKNiHdmMshCvUQh8wKRBq/D2aAgq9fj/yxlj0FdrAxc+ZQFyk5MbPH6ry00jVWu3sY95s4PAQ== </DP> <DQ> WcRsIUYk5oSbAGiDohiYeZlPTBvtr101V669IUFhhAGJL8cEWnOXksodoIGimzGBrD5GARrr3yRcL1GLPuCEvQ== </DQ> <InverseQ> wIbuKBZSCioG6MHdT1jxlv6U1+Y3TX9sHED9PqGzWWpVGA+xFJmQUxoFf/SvHzwbBlXnG0DLqUvxEv+BkEid2w== </InverseQ> <D> Yk21yWdT1BfXqlw30NyN7qNWNuM/Uvh2eaRkCrhvFTckSucxs7st6qig2/RPIwwfr6yIc/bE/TRO3huQicTpC2W3aXsBI9822OOX4BdWCec2txXpSkbZW24moXu+OSHfAdYoOEN6ocR7tAGykIqENshRO7HvONJsOE5+1kF2GAE= </D> </RSAKeyValue>"; string data = LicenseGenerator.CreateLicense( rsaData, new License { Id = Guid.NewGuid(), Domain = "dotnettips.info", Expiration = DateTime.Now.AddYears(2), IssuedTo = "VahidN", Type = LicenseType.Standard }); File.WriteAllText("License.xml", data); } } }
همانطور که مشاهده میکنید، اطلاعات کامل یک نمونه از آن، در متد writeLicense مورد نیاز است. اما در متد readLicense تنها به قسمت عمومی آن یعنی Modulus و Exponent نیاز خواهد بود (موارد قابل انتشار به همراه برنامه).
سؤال: امنیت این روش تا چه اندازه است؟
پاسخ: تا زمانیکه کاربر نهایی به کلیدهای خصوصی شما دسترسی پیدا نکند، امکان تولید معادل آنها تقریبا در حد صفر است و به طول عمر او قد نخواهد داد!
اما ... مهاجم میتواند کلیدهای عمومی و خصوصی خودش را تولید کند. مجوز دلخواهی را بر این اساس تهیه کرده و سپس کلید عمومی جدید را در برنامه، بجای کلیدهای عمومی شما درج (patch) کند! بنابراین روش بررسی اینکه آیا برنامه جاری patch شده است یا خیر را فراموش نکنید. یا عموما مطابق معمول قسمتی از برنامه که در آن مقایسهای بین اطلاعات دریافتی و اطلاعات مورد انتظار وجود دارد، وصله میشوند؛ این مورد عمومی است و منحصر به روش جاری نمیباشد.
دریافت نسخه جنریک این مثال:
SignedXmlSample.zip