مباحث پایهای اعتبارسنجی کاربران در ASP.NET Core با نگارشهای پیشین ASP.NET MVC، آنچنان تفاوت ساختاری مهمی ندارند و یکی هستند. عمدهی تفاوتها در نحوهی برپایی تنظیمات اولیهی اسکریپتهای آنها و همچنین یک سری Tag Helper جدید معادل با HTML Helperهای متداول اعتبارسنجی هستند.
دریافت وابستگیهای سمت کاربر مباحث اعتبارسنجی
زمانیکه گزینهی ایجاد یک پروژهی جدید ASP.NET Core را در VS.NET انتخاب میکنیم، علاوه بر قالب empty آن، قالب دیگری به نام web application نیز در آن موجود است. با انتخاب این قالب، فایلی را به نام bower.json نیز با این محتوا مشاهده میکنید:
این مداخل تمام پیشنیازهای مباحث اعتبارسنجی کاربران را به صورت خودکار به پروژه اضافه میکنند. بنابراین افزودن فایل جدید bower.json به پروژه با محتوای فوق و سپس مراجعه به solution explorer و کلیک راست بر روی گره dependencies و در آخر انتخاب restore packages، سبب دریافت خودکار این بستهها میشود.
این بستهها را پس از دریافت، در پوشهی bower_components خواهید یافت:
البته باید دقت داشت که استفاده از bower در اینجا الزامی نیست. اگر علاقمند بودید از npm و node.js استفاده کنید.
افزودن وابستگیهای سمت کاربر مباحث اعتبارسنجی و عمومی کردن آنها
پس از دریافت وابستگیهای مورد نیاز توسط bower، به فایل layout برنامه مراجعه کرده و سپس آنها را به ترتیب ذیل اضافه میکنیم:
هرچند این ترتیب درست است، اما ... کار نمیکند. علت را هم در مبحث «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» بررسی کردیم: تمام پوشههای موجود در ریشهی یک برنامهی ASP.NET Core، غیرعمومی هستند (و توسط کاربران قابل درخواست نیستند)، مگر اینکه آنهارا توسط میان افزار static files، عمومی کنیم. برای این منظور به کلاس آغازین برنامه مراجعه کرده و سپس به متد Configure آن، تنظیمات ذیل را اضافه کنید:
به این ترتیب فایلهای موجود در پوشهی bower_components به مسیر bower_components/ نگاشت میشوند. البته انتخاب مقدار RequestPath در اینجا اختیاری است و برای مثال میتوانید مسیر scripts/ را نیز معرفی کنید (نگاشت مسیر فیزیکی و واقعی فایلها به URL خاص دیگری که توسط وب سرور ارائه خواهد شد).
اگر RequestPath را به مسیر دیگری تنظیم کردید، نیاز است ابتدای سه مدخل ذکر شده را بر این اساس اصلاح کنید، تا فایلها توسط وب سرور قابل ارائه شوند.
استفاده از CDN برای توزیع اسکریپتهای اعتبارسنجی مورد نیاز
در مورد environment tag helper در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» پیشتر بحث شد. در اینجا نیز میتوان برای مثال در حال توسعه، از اسکریپتهای محلی
و در حالت نهایی توزیع برنامه، از CDNهای ارائهی اسکریپتها، جهت کاهش بار سرور استفاده کرد:
در اینجا fallback به آدرس محلی دریافت اسکریپتهای مدنظر، در صورت در دسترس نبودن CDN، تنظیم شدهاست.
روش عملکرد fallback هم به این صورت است که بررسی میشود آیا عبارت ذکر شدهی در قسمت asp-fallback-test قابل اجرا است یا خیر؟ اگر خیر، یعنی CDN قابل دسترسی نیست و از نمونهی محلی استفاده میکند.
خلاصهای از Tag helpers اعتبارسنجی
در جدول «راهنمای تبدیل HTML Helpers به Tag Helpers» مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers»، معادلهای HTML Helpers مباحث اعتبارسنجی را نیز ملاحظه کردید. خلاصهی تکمیلی آن به صورت ذیل است:
ValidationSummary.All سبب نمایش خطاهای اعتبارسنجی خواص و همچنین کل مدل میشود:
معادل است با
ValidationSummary.ModelOnly صرفا خطاهای اعتبارسنجی در سطح مدل را نمایش میدهد:
معادل است با
و برای تعیین نمایش خطاهای اعتبارسنجی یک خاصیت از مدل:
معادل است با
دریافت وابستگیهای سمت کاربر مباحث اعتبارسنجی
زمانیکه گزینهی ایجاد یک پروژهی جدید ASP.NET Core را در VS.NET انتخاب میکنیم، علاوه بر قالب empty آن، قالب دیگری به نام web application نیز در آن موجود است. با انتخاب این قالب، فایلی را به نام bower.json نیز با این محتوا مشاهده میکنید:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6" } }
این بستهها را پس از دریافت، در پوشهی bower_components خواهید یافت:
البته باید دقت داشت که استفاده از bower در اینجا الزامی نیست. اگر علاقمند بودید از npm و node.js استفاده کنید.
افزودن وابستگیهای سمت کاربر مباحث اعتبارسنجی و عمومی کردن آنها
پس از دریافت وابستگیهای مورد نیاز توسط bower، به فایل layout برنامه مراجعه کرده و سپس آنها را به ترتیب ذیل اضافه میکنیم:
<script src="~/bower_components/jquery/dist/jquery.min.js"></script> <script src="~/bower_components/jquery-validation/dist/jquery.validate.min.js"></script> <script src="~/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> @RenderSection("scripts", required: false) </body> </html>
// Serve wwwroot as root app.UseFileServer(); // Serve /bower_components as a separate root app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "bower_components")), // Only react to requests that match this path RequestPath = "/bower_components", // Don't expose file system EnableDirectoryBrowsing = false });
اگر RequestPath را به مسیر دیگری تنظیم کردید، نیاز است ابتدای سه مدخل ذکر شده را بر این اساس اصلاح کنید، تا فایلها توسط وب سرور قابل ارائه شوند.
استفاده از CDN برای توزیع اسکریپتهای اعتبارسنجی مورد نیاز
در مورد environment tag helper در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» پیشتر بحث شد. در اینجا نیز میتوان برای مثال در حال توسعه، از اسکریپتهای محلی
<environment name="Development"> <script src="~/bower_components/jquery/dist/jquery.min.js"></script> <script src="~/bower_components/jquery-validation/dist/jquery.validate.min.js"></script> <script src="~/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script> </environment>
<environment names="Staging, Production"> <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.js" asp-fallback-src="/bower_components/jquery/dist/jquery.min.js" asp-fallback-test="window.jQuery"> </script> <script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js" asp-fallback-src="bower_components/jquery-validation/dist/jquery.validate.min.js" asp-fallback-test="window.jQuery && window.jQuery.validator"> </script> <script src="https://ajax.aspnetcdn.com/ajax/mvc/5.2.3/jquery.validate.unobtrusive.min.js" asp-fallback-src="/bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js" asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive"> </script> </environment>
روش عملکرد fallback هم به این صورت است که بررسی میشود آیا عبارت ذکر شدهی در قسمت asp-fallback-test قابل اجرا است یا خیر؟ اگر خیر، یعنی CDN قابل دسترسی نیست و از نمونهی محلی استفاده میکند.
خلاصهای از Tag helpers اعتبارسنجی
در جدول «راهنمای تبدیل HTML Helpers به Tag Helpers» مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers»، معادلهای HTML Helpers مباحث اعتبارسنجی را نیز ملاحظه کردید. خلاصهی تکمیلی آن به صورت ذیل است:
ValidationSummary.All سبب نمایش خطاهای اعتبارسنجی خواص و همچنین کل مدل میشود:
@Html.ValidationSummary(false)
<div asp-validation-summary="All"></div>
ValidationSummary.ModelOnly صرفا خطاهای اعتبارسنجی در سطح مدل را نمایش میدهد:
@Html.ValidationSummary(true)
<div asp-validation-summary="ModelOnly"></div>
و برای تعیین نمایش خطاهای اعتبارسنجی یک خاصیت از مدل:
@Html.ValidationMessageFor(m => m.UserName, "", new { @class = "text-danger" })
<span asp-validation-for="UserName" class="text-danger"></span>
در قسمت آخر این سری، نگاهی خواهیم داشت به نحوهی توزیع برنامههای React و نکات مرتبط با آن.
افزودن متغیرهای محیطی
در برنامهی نمایش لیست فیلمهایی که تا قسمت 29 آنرا بررسی کردیم، از فایل src\config.json برای ذخیره سازی اطلاعات تنظیمات برنامه استفاده شد. هرچند این روش کار میکند اما بر اساس محیطهای مختلف توسعه، متغیر نیست. اغلب برنامهها باید بتوانند حداقل در سه محیط توسعه، آزمایش و تولید، بر اساس متغیرها و تنظیمات خاص هر کدام، کار کنند. برای مثال بر روی سیستمی که کار توسعه در آن انجام میشود، میخواهیم apiUrl متفاوتی را نسبت به حالتیکه برنامه توزیع میشود، داشته باشیم.
برای رفع این مشکل، برنامههایی که توسط create-react-app تولید میشوند، دارای پشتیبانی توکاری از متغیرهای محیطی هستند. برای این منظور نیاز است در ریشهی پروژه (جائیکه فایل package.json قرار دارد) فایل جدید env. را ایجاد کرد. در ویندوز برای ایجاد یک چنین فایلهایی که فقط از یک پسوند تشکیل میشوند، باید نام فایل را به صورت .env. وارد کرد؛ سپس خود ویندوز نقطهی نهایی را حذف میکند. البته اگر از ادیتور VSCode برای ایجاد این فایل استفاده میکنید، نیازی به درج نقطهی انتهایی نیست. در این فایل environment ایجاد شده میتوان تمام متغیرهای محیطی مورد نیاز را با مقادیر پیشفرض آنها درج کرد. همچنین میتوان این مقادیر پیشفرض را بر اساس محیطهای مختلف کاری، بازنویسی کرد. برای مثال میتوان فایل env.development. را اضافه کرد؛ به همراه فایلهای env.test. و env.production.
متغیرهای محیطی به صورت key=value درج میشوند. این کلیدها نیر باید با REACT_APP_ شروع شوند؛ در غیر اینصورت، کار نخواهند کرد. برای مثال در فایل env.، دو متغیر پیشفرض زیر را تعریف میکنیم:
اکنون برای خواندن این متغیرها برای مثال در فایل index.js (و یا هر فایل جاوا اسکریپتی دیگری در برنامه)، سطر زیر را درج میکنیم:
process به معنای پروسهی جاری برنامهاست (و مرتبط است به پروسهی node.js ای که برنامهی React را اجرا میکند) و خاصیت env، به همراه تمام متغیرهای محیطی برنامه میباشد. در این حالت اگر برنامه را اجرا کنیم، در کنسول توسعه دهندگان مرورگر، به یک چنین خروجی خواهیم رسید:
در این خروجی، متغیر "NODE_ENV: "development به صورت خودکار با تولید بستههای مخصوص ارائهی نهایی، به production تنظیم میشود. سایر متغیرهای محیطی تعریف شده را نیز در اینجا ملاحظه میکنید. با توجه به خواص شیء env، برای مثال جهت دسترسی به نام برنامه میتوان از مقدار process.env.REACT_APP_NAME استفاده کرد.
یک نکته: با هر تغییری در مقادیر متغیرهای محیطی، نیاز است یکبار دیگر برنامه را از ابتدا توسط دستور npm start، راه اندازی مجدد کرد؛ چون این فایلها به صورت خودکار ردیابی نمیشوند.
نحوهی پردازش متغیرهای محیطی درج شدهی در برنامه
اگر همان سطر لاگ کردن خروجی process.env را به صورت زیر تغییر دهیم:
و برنامه را مجددا اجرا کنیم، با مراجعهی به برگهی Sources و انتخاب مسیر localhost:3000/static/js/main.chunk.js و سپس جستجوی "My App Name" ای که در اینجا اضافه کردیم (با فشردن دکمههای Ctrl+F)، به خروجی زیر خواهیم رسید:
همانطور که مشاهده میکنید، فراخوانی console.log ما، دیگر به همراه متغیر process.env.REACT_APP_NAME نیست؛ بلکه مقدار اصلی این متغیر در اینجا درج شدهاست. بنابراین اگر در در حین توسعهی برنامه، از متغیرهای محیطی استفاده شود، این متغیرها با مقادیر اصلی آنها در حین پروسهی Build نهایی، جایگزین میشوند.
Build برنامههای React برای محیط تولید
اجرای دستور npm start، سبب ایجاد یک Build مخصوص محیط توسعه میشود که بهینه سازی نشدهاست و به همراه اطلاعات اضافی قابل توجهی جهت دیباگ سادهتر برنامهاست. برای رسیدن به یک خروجی بهینه سازی شدهی مخصوص محیط تولید و ارائهی نهایی باید دستور npm run build را در خط فرمان اجرا کرد. خروجی نهایی این دستور، در پوشهی جدید build واقع در ریشهی پروژه، قرار میگیرد. اکنون میتوان کل محتویات این پوشه را جهت ارائهی نهایی در وب سرور خود، مورد استفاده قرار داد.
پس از پایان اجرای دستور npm run build، پیام «امکان ارائهی آن توسط static server زیر نیز وجود دارد» ظاهر میشود:
اگر علاقمند باشید تا خروجی حالت production تولید شده را نیز به صورت محلی آزمایش کنید، ابتدا باید static server یاد شده را توسط دستور npm install فوق نصب کنید. سپس ریشهی پروژه را در خط فرمان باز کرده و دستور serve -s build را صادر کنید (البته اگر با خط فرمان به پوشهی build وارد شدید، دیگر نیازی به ذکر پوشهی build نخواهد بود). اکنون میتوانید برنامه را در آدرس http://localhost:5000 در مرورگر خود بررسی نمائید.
البته با توجه به اینکه backend سرور برنامههای ما نیز در همین آدرس قرار دارد و در صورت ورود این آدرس، به صورت خودکار به https://localhost:5001/index.html هدایت خواهید شد، میتوان این پورت پیشفرض را با اجرای دستور serve -s build -l 1234 تغییر داد. اکنون میتوان آدرس جدید http://localhost:1234 را در مرورگر آزمایش کرد که ... با خطای زیر کار نمیکند:
روش رفع این مشکل را در قسمت 23 بررسی کردیم و در اینجا جهت بهبود آن میتوان متد WithOrigins فایل Startup.cs را به صورت زیر تکمیل کرد:
یک نکته: زمانیکه از دستور npm start استفاده میشود، متغیرهای محیطی از فایل env.development. خوانده خواهند شد و زمانیکه از دستور npm run build استفاده میشود، این متغیرها از فایل env.production. تامین میشوند. در این حالتها اگر متغیری در این دو فایل درج نشده بود، از مقدار پیشفرض موجود در فایل env. استفاده میگردد. از فایل env.test. با اجرای دستور npm test، به صورت خودکار استفاده میشود.
آماده سازی برنامهی React، برای توزیع نهایی
تا اینجا برنامهی React تهیه شده، اطلاعات apiUrl خودش را از فایل config.json دریافت میکند. اکنون میخواهیم بر اساس حالات مختلف توسعه و تولید، از apiUrlهای متفاوتی استفاده شود. به همین جهت به فایل env.production. مراجعه کرده و تنظیمات ذیل را به آن اضافه میکنیم:
البته فعلا همین متغیرها را به فایل env.development. نیز میتوان اضافه کرد؛ چون backend سرور ما در هر دو حالت، در این مثال، در آدرس فوق قرار دارد.
اکنون به برنامه مراجعه کرده و در هرجائی که ارجاعی به فایل config.json وجود دارد، سطر import آنرا حذف میکنیم. با این تغییر، تمام آدرسهایی مانند:
را به صورت زیر ویرایش میکنیم:
در ادامه برای تامین مقدار apiUrl به صورت خودکار، به فایل src\services\httpService.js مراجعه کرده و در ابتدای آن، یک سطر زیر را اضافه میکنیم:
به این ترتیب تمام درخواستهای ارسالی توسط Axios، دارای baseURL ای خواهند شد که از فایل متغیر محیطی جاری تامین میشود. همانطور که پیشتر نیز عنوان شد، این مقدار در زمان Build، با مقدار ثابتی که از فایل env جاری خوانده میشود، جایگزین خواهد شد.
همچنین adminRoleName مورد نیاز در فایل src\services\authService.js را نیز از همان فایل env جاری تامین میکنیم:
پس از این تغییرات، نیاز است برای حالت توسعه، یکبار دیگر دستور npm start و یا برای حالت تولید، دستور npm run build را اجرا کرد تا اطلاعات درج شدهی در فایلهای env.، پردازش و جایگزین شوند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-34-frontend.zip و sample-34-backend.zip
افزودن متغیرهای محیطی
در برنامهی نمایش لیست فیلمهایی که تا قسمت 29 آنرا بررسی کردیم، از فایل src\config.json برای ذخیره سازی اطلاعات تنظیمات برنامه استفاده شد. هرچند این روش کار میکند اما بر اساس محیطهای مختلف توسعه، متغیر نیست. اغلب برنامهها باید بتوانند حداقل در سه محیط توسعه، آزمایش و تولید، بر اساس متغیرها و تنظیمات خاص هر کدام، کار کنند. برای مثال بر روی سیستمی که کار توسعه در آن انجام میشود، میخواهیم apiUrl متفاوتی را نسبت به حالتیکه برنامه توزیع میشود، داشته باشیم.
برای رفع این مشکل، برنامههایی که توسط create-react-app تولید میشوند، دارای پشتیبانی توکاری از متغیرهای محیطی هستند. برای این منظور نیاز است در ریشهی پروژه (جائیکه فایل package.json قرار دارد) فایل جدید env. را ایجاد کرد. در ویندوز برای ایجاد یک چنین فایلهایی که فقط از یک پسوند تشکیل میشوند، باید نام فایل را به صورت .env. وارد کرد؛ سپس خود ویندوز نقطهی نهایی را حذف میکند. البته اگر از ادیتور VSCode برای ایجاد این فایل استفاده میکنید، نیازی به درج نقطهی انتهایی نیست. در این فایل environment ایجاد شده میتوان تمام متغیرهای محیطی مورد نیاز را با مقادیر پیشفرض آنها درج کرد. همچنین میتوان این مقادیر پیشفرض را بر اساس محیطهای مختلف کاری، بازنویسی کرد. برای مثال میتوان فایل env.development. را اضافه کرد؛ به همراه فایلهای env.test. و env.production.
متغیرهای محیطی به صورت key=value درج میشوند. این کلیدها نیر باید با REACT_APP_ شروع شوند؛ در غیر اینصورت، کار نخواهند کرد. برای مثال در فایل env.، دو متغیر پیشفرض زیر را تعریف میکنیم:
REACT_APP_NAME=My App REACT_APP_VERSION=1
console.log(process.env);
در این خروجی، متغیر "NODE_ENV: "development به صورت خودکار با تولید بستههای مخصوص ارائهی نهایی، به production تنظیم میشود. سایر متغیرهای محیطی تعریف شده را نیز در اینجا ملاحظه میکنید. با توجه به خواص شیء env، برای مثال جهت دسترسی به نام برنامه میتوان از مقدار process.env.REACT_APP_NAME استفاده کرد.
یک نکته: با هر تغییری در مقادیر متغیرهای محیطی، نیاز است یکبار دیگر برنامه را از ابتدا توسط دستور npm start، راه اندازی مجدد کرد؛ چون این فایلها به صورت خودکار ردیابی نمیشوند.
نحوهی پردازش متغیرهای محیطی درج شدهی در برنامه
اگر همان سطر لاگ کردن خروجی process.env را به صورت زیر تغییر دهیم:
console.log("My App Name", process.env.REACT_APP_NAME);
همانطور که مشاهده میکنید، فراخوانی console.log ما، دیگر به همراه متغیر process.env.REACT_APP_NAME نیست؛ بلکه مقدار اصلی این متغیر در اینجا درج شدهاست. بنابراین اگر در در حین توسعهی برنامه، از متغیرهای محیطی استفاده شود، این متغیرها با مقادیر اصلی آنها در حین پروسهی Build نهایی، جایگزین میشوند.
Build برنامههای React برای محیط تولید
اجرای دستور npm start، سبب ایجاد یک Build مخصوص محیط توسعه میشود که بهینه سازی نشدهاست و به همراه اطلاعات اضافی قابل توجهی جهت دیباگ سادهتر برنامهاست. برای رسیدن به یک خروجی بهینه سازی شدهی مخصوص محیط تولید و ارائهی نهایی باید دستور npm run build را در خط فرمان اجرا کرد. خروجی نهایی این دستور، در پوشهی جدید build واقع در ریشهی پروژه، قرار میگیرد. اکنون میتوان کل محتویات این پوشه را جهت ارائهی نهایی در وب سرور خود، مورد استفاده قرار داد.
پس از پایان اجرای دستور npm run build، پیام «امکان ارائهی آن توسط static server زیر نیز وجود دارد» ظاهر میشود:
> npm install -g serve > serve -s build
البته با توجه به اینکه backend سرور برنامههای ما نیز در همین آدرس قرار دارد و در صورت ورود این آدرس، به صورت خودکار به https://localhost:5001/index.html هدایت خواهید شد، میتوان این پورت پیشفرض را با اجرای دستور serve -s build -l 1234 تغییر داد. اکنون میتوان آدرس جدید http://localhost:1234 را در مرورگر آزمایش کرد که ... با خطای زیر کار نمیکند:
Access to XMLHttpRequest at 'https://localhost:5001/api/genres' from origin 'http://localhost:1234' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
WithOrigins("http://localhost:3000", "http://localhost:1234")
یک نکته: زمانیکه از دستور npm start استفاده میشود، متغیرهای محیطی از فایل env.development. خوانده خواهند شد و زمانیکه از دستور npm run build استفاده میشود، این متغیرها از فایل env.production. تامین میشوند. در این حالتها اگر متغیری در این دو فایل درج نشده بود، از مقدار پیشفرض موجود در فایل env. استفاده میگردد. از فایل env.test. با اجرای دستور npm test، به صورت خودکار استفاده میشود.
آماده سازی برنامهی React، برای توزیع نهایی
تا اینجا برنامهی React تهیه شده، اطلاعات apiUrl خودش را از فایل config.json دریافت میکند. اکنون میخواهیم بر اساس حالات مختلف توسعه و تولید، از apiUrlهای متفاوتی استفاده شود. به همین جهت به فایل env.production. مراجعه کرده و تنظیمات ذیل را به آن اضافه میکنیم:
REACT_APP_API_URL=https://localhost:5001/api REACT_APP_ADMIN_ROLE_NAME=Admin
اکنون به برنامه مراجعه کرده و در هرجائی که ارجاعی به فایل config.json وجود دارد، سطر import آنرا حذف میکنیم. با این تغییر، تمام آدرسهایی مانند:
const apiEndpoint = apiUrl + "/users";
const apiEndpoint = "/users";
axios.defaults.baseURL = process.env.REACT_APP_API_URL;
همچنین adminRoleName مورد نیاز در فایل src\services\authService.js را نیز از همان فایل env جاری تامین میکنیم:
const adminRoleName = process.env.REACT_APP_ADMIN_ROLE_NAME;
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-34-frontend.zip و sample-34-backend.zip
البته یک مشکل اساسی در این روش که وجود دارد و آن امنیت پایین در استفاده از Entityهای ماژولهای مختلف است.
در پروژههای بزرگ و ERP هر ماژول باید به یکسری Entityهای مشخصی دسترسی و ارتباط داشته باشد و نباید بصورت نامحدود با هر Entity از هر ماژولی join بزند.
در این روش تقریبا همه Entityها در یک سطح قرار دارند و کپسوله سازی وجود ندارد.
من دو تا برنامه رو با این مخزن کد ایجاد کردم، هشِ رمز عبورِ پروژه اول رو در پایگاه داده دوم کپی کردم و تونستم با رمز عبور پروژه اول در پروژه دوم هم لاگین کنم.
- آیا این یک مشکل است ؟
- اگر مشکل است، برای رفع آن اگر در کلاس PasswordValidator، رمز عبور را با Salt خودم هش کنم مشکل حل میشود ؟
و در نهایت ممنون میشم بگید که محل ذخیره سازی Salt باید در کدهای سی شارپی باشه یا در یک فایل txt ؟
ASP.NET Core به همراه یک فریم ورک توکار ثبت وقایع (Logging) ارائه شدهی توسط تزریق وابستگیها است که به صورت پیش فرض نیز فعال است.
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
متد Configure امضای مشخصی را ندارد و در اینجا به هر تعداد سرویسی که نیاز باشد، میتوان اینترفیسهای آنها را جهت تزریق وابستگیهای متناظر توسط IoC Containser توکار ASP.NET Core، معرفی کرد. در اینجا برای تنظیم ویژگیهای سرویس ثبت وقایع، تزریق وابستگی ILoggerFactory صورت گرفتهاست.
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
در مورد Log Level و یا سطوح ثبت وقایع، در ادامهی مطلب بحث خواهد شد.
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
پس از این تنظیمات، برنامه را اجرا کنید.
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
در این کنترلر، وابستگی اینترفیس ILogger با پارامتری از نوع کنترلر جاری به سازندهی کلاس تزریق شدهاست. علت ذکر این پارامتر جنریک این است که ILoggerFactory بداند چگونه باید متد CreateLogger خود را در پشت صحنه وهله سازی کند.
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
این حداقل مرتبط است با اعدادی که در کنار سطوح فوق ملاحظه میکنید. برای مثال اگر حداقل سطح ثبت وقایع به Information تنظیم شود، چون سطح آن 3 است، دیگر سطوح پایینتر از آن لاگ نخواهند شد. اهمیت این مساله در اینجا است که اگر صرفا نیاز به اطلاعات Critical داشتیم، نیازی نیست تا با انبوهی از اطلاعات لاگ شده سر و کار داشته باشیم و به این ترتیب میتوان حجم اطلاعات نمایش داده شده را کاهش داد.
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویسها و تزریق وابستگیها» مشاهده کردهاید. در اینجا لیست سرویسهایی را مشاهده میکنید که به صورت پیش فرض، ثبت شدهاند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آنها هستند. بنابراین نیازی به فعال سازی آنها نیست؛ اما برای استفادهی از آنها نیاز به انجام یک سری تنظیمات است.
پیاده سازی ثبت وقایع در ASP.NET Core
اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug();
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه میخواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» بیشتر بحث شد) و لاگ کردن ویژهی در کنسول NET Core. را فعال میکند:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگیهای ذیل در فایل project.json برنامه را دارد:
{ "dependencies": { //same as before "Microsoft.Extensions.Logging": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.Extensions.Logging.Debug": "1.0.0" } }
در اینجا میتوانید ریز وقایعی را که توسط ASP.NET Core لاگ شدهاست، مشاهده کنید. برای مثال چه درخواستی صورت گرفتهاست و چقدر اجرای آن زمانبردهاست.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر میخواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشهی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:
دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شدهاست که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز میتوان حالتهای مختلف اطلاعات لاگ شده را مشاهده کرد و تمام اینها مرتبط هستند به ذکر متد AddConsole .
کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگیها
برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمتهای مختلف برنامه، میتوان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")] public class AboutController : Controller { private readonly ILogger<AboutController> _logger; public AboutController(ILogger<AboutController> logger) { _logger = logger; } [Route("")] public ActionResult Hello() { _logger.LogInformation("Running Hello"); return Content("Hello from DNT!"); }
سپس با توجه به اینکه این سرویس جزو سرویسهای از پیش ثبت شدهی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شدهاست و خروجی عبارت لاگ شدهی آنرا در اینجا میتوانید مشاهده کنید:
سطوح مختلف ثبت وقایع
اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آنها به شرح زیر هستند:
Debug (1): ثبت واقعهای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز میباشد. بنابراین نباید در حالت ارائهی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعهی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمتهای مختلف برنامه مورد استفاده قرار میگیرند.
(4) Warning: جهت ثبت واقعهای نامطلوب در سیستم بکار میرود و سبب قطع اجرای برنامه نمیشود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شدهاند را ثبت میکند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شدهاند.
برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو میشود، میتوان حداقل سطح ثبت وقایع را نیز ذکر کرد:
loggerFactory.AddDebug(minLevel: LogLevel.Information);
البته ترتیب واقعی این سطوح را در enum مرتبط با آنها بهتر میتوان مشاهده کرد:
public enum LogLevel { Trace, Debug, Information, Warning, Error, Critical, None, }
یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی میکنید، حداقل سطح ثبت وقایع آن به Information تنظیم شدهاست. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایینتر است از Information). بنابراین اگر میخواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آنرا به LogLevel.Debug تنظیم نمائید.
امکان استفادهی از پروایدرهای ثبت وقایع ثالث
تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمتهای مختلف و یا در بانک اطلاعاتی وجود دارد، میتوان به تامین کنندههای ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
در این مقاله آموزشی قصد داریم به یکی از مهمترین و اساسیترین مفاهیم تعریف شده در پایگاه داده بنام تراکنشها بپردازیم. بعنوان تعریف میتوان اینگونه بیان نمود که تراکنش یک واحد کاری منطقی است که عملی را بر روی پایگاه داده انجام میدهد. عموما تراکنشها دنباله ای از عملیات پایگاه داده هستند که رویه هم رفته انجام یک کار یا وظیفه را بر عهده دارند. نکته مهمی که در مورد تراکنشها مطرح میشود اینست که آنها باید به گونه ای مدیریت شوند که پایگاه داده را از یک وضعیت سازگار و درست (consistent) به وضعیت سازگار دیگری ببرند. به بیان دیگر اگر تراکنش از چند عملیات تشکیل شده باشد، پس از پایان اجرای تمامی عملیات مربوط به تراکنش نباید در دادههای پایگاه داده هیچ تناقضی با قوانین پایگاه داده (integrity rules) بوجود بیاید. مزیت استفاده از تراکنش نیز همین مسئله است که به توسعه دهنده نرم افزار این اطمینان را میدهد که صحت و درستی پایگاه داده در اثر اجرای دستورات او از بین نخواهد رفت. علاوه بر آن اگر در حین اجرای یکی از دستورات خللی ایجاد گردد، پایگاه داده دوباره به وضعیت سازگار قبلی خود باز گردانده خواهد شد. نسلهای اولیه سیستمهای مدیریت پایگاه داده فاقد پیاده سازی تراکنش بودند و بهمین دلیل توسعه دهندگان کار بسیار مشکلی در شبیه سازی این واحدهای یکپارچه منطقی داشتند. خوشبختانه اکثر DBMSهای امروزی این مفهوم مهم را پشتیبانی میکنند و نیازی به نگرانی در مورد پیاده سازی آن نیست. تنها کاری که لازم است انجام گیرد کسب مهارت در زمینه استفاده از آنهاست.
تعریف تراکنشها و مشخص کردن عملیات موجود در آنها اغلب توسط خود توسعه دهنده برنامه صورت میگیرد. اوست که تعیین میکند تراکنشش باید چه عملیاتی را با چه ترتیبی انجام دهد. اما در کنار این قسم از تراکنشها که توسط کاربران تعریف میشود، تراکنشهای دیگری نیز وجود دارند که توسط خود سیستم مدیریت پایگاه داده تعریف میشوند. به این قبیل تراکنشها که واحدهای کاری بسیار کوچک و عموما تجزیه ناپذیری هستند تراکنشهای خودکار یا auto transactions گفته میشود. بعنوان مثال اگر ما تراکنشی را تعریف کرده باشیم که شامل یک عمل خواندن و یک عمل درج باشد، در هنگام اجرا سیستم این تراکنش را به دو تراکنش کوچکتر میشکند که در یکی عمل خواندن و در دیگری عملی نوشتن و درج را انجام میدهد. البته توجه داشته باشید که اگر چه این دو عملیات جدا و مستقل از هم اجرا میشوند اما رابطه منطقی آنها با یکدیگر حفظ میشود و در صورت خللی در یکی از آنها اثر دیگری نیز بازگردانده شده و پایگاه داده دوباره به حالت قبل از جرا برگردانده میشود. به این کار عمل undo شدن تراکنش گفته میشود.
گفتیم که تعریف تراکنش توسط کاربر صورت میپذیرد و مدیریت آن بر عهده پایگاه داده قرار میگیرد. در این میان نکته حائز اهمیتی وجود دارد که در اینجا باید به آن اشاره شود. اندازه تراکنش نقشی بسیار موثر در کارایی پایگاه داده ایفا میکند. توجه داشته باشید که اندازه تراکنشها نباید خیلی بزرگ باشد. چراکه منجر به بزرگ شدن بیرویه فایل مربوط به ثبت وقایع پایگاه داده (log file) میگردد. تمامی علیات تاثیر گذار بر روی پایگاه داده در این فایل ثبت میشوند تا در موقع لزوم بتوان با استفاده از عمل بازیابی و ترمیم پایگاه داده (recovery) را انجام داد. بزرگ بودن این فایل در هنگام ترمیم میتواند بر روی کارایی تاثیر گذار باشد. علاوه بر این موضوع اندازه تراکنشها اثر سوء دیگری نیز میتواند در پی داشته باشد و آن محدود نمودن درجه همروندی است. یعنی اگر اندازه تراکنش بیش از حد معمول باشد ممکن است بر روی تعداد تراکنش هایی که میتوانند بطور موازی و همزمان اجرا شوند تاثیر منفی بگذارد. چرا که معمولا در آغاز تراکنش بر روی منابعی که مورد استفاده تراکنش قرار میگیرد قفل گذاری میشود تا بگونه ای مسئله نواحی بحرانی حل شود. این قفلها زمانی آزاد میشوند که تمامی عملیات داخل تراکنش بطور کامل اجرا شده باشند یا اینکه مشکلی در حین اجرا بوجود آید. در این صورت هرچه تراکنش بزرگتر باشد اجرای آن بیشتر طول خواهد کشید و در نتیجه قفلهای آن نیز دیرتر آزاد میشوند. بدین ترتیب سایر تراکنش هایی که میخواهند از منابع مشترک استفاده کنند باید تا پایان اجرای تراکنش بزرگ ما منتظر بمانند. این مسئله یعنی کاهش درجه اجرای موازی با همروندی که اگر در سیستمهای بزرگ به آن دقت نشود به گلوگاهی تبدیل خواهد شد و کارایی را به نحو قابل توجهی کاهش میدهد.
تعریف تراکنشها :
بدنه اصلی هر تراکنش را چهار کلمه کلیدی تشکیل میدهند که البته ممکن است صریحا در تعریف توسط کاربر لحاظ نشوند اما این چهار کلمه کلیدی باید در تمامی تراکنشها چه بصورت صریح و چه بصورت ضمنی آورده شوند. این کلمات عبارتند از BEGIN TRANSACTION، END TRANSACTION، ROLLBACK و COMMIT. کلمات کلیدی BEGIN TRANSACTION و END TRANSACTION همانطور که از نامشان پیداست آغاز و پایان یک تراکنش را نشان میدهد. اینکه تراکنش از چه نقطه ای آغاز و در چه نقطه ای به پایان رسیده است برای مدیریت آن بسیار مهم و حیاتی است بخصوص در مواقعی که در حین انجام مشکلی پیش بیاید. از کلمه کلیدی ROLLBACK هنگامی استفاده میکنیم که بخواهیم تغییراتی که تا این لحظه بر روی پایگاه داده صورت گرفته است را مجددا بی اثر کنیم و پایگاه داده را به حالت پیش از شروع تراکنش بازگردانیم. توجه داشته باشید که در برخی از مواقع ممکن است این کلمه را خودمان در بدنه تراکنش مستقیما قرار دهیم. بعنوان مثال یک خطای منطقی را در بخشی از روال انجام تراکنش با یک عبارت شرطی تشخیص میدهیم و با استفاده از ROLLBACK به مدیریت پایگاه داده اعلام میکنیم که عملیات بازگردانی را انجام بده. گاهی ممکن است ما صریحا این کلمه را در تراکنش نیاورده باشیم اما درحین انجام تراکنش خطایی رخ دهد، در این صورت خود سیستم مدیریت پایگاه داده خطا را شناسایی کرده و عملیات مربوط به ROLLBACK را انجام میدهد تا صحت و سازگاری پایگاه داده حفظ گردد. کلمه کلیدی COMMIT نیز باید در انتهای تراکنش آورده شود تا به مدیریت پایگاه داده اعلام شود که عملیات کامل شده است و تغییرات باید در پایگاه داده بطور فیزیکی اعمال شوند. توجه داشته باشید که تا زمانی که مدیریت پایگاه داده به دستور COMMIT نرسیده باشد، تغییرات را جهت اعمال بر روی حافظه فیزیکی به واحد مدیریت حافظه نمیدهد و بنابراین این تغییرات تا پیش از COMMIT از چشم سایر کاربران مخفی خواهد ماند.
نکته ای که در اینجا وجود دارد این است که فرمان COMMIT به معنی این نیست که بلافاصله تغییرات بر روی دیسک و حافظه جانبی نوشته میشود. بلکه به این معنی است که تمامی عملیات تراکنش با موفقیت انجام شده است و سیستم مدیریت پایگاه میتواند آنها را برای نوشته شدن در حافظه جانبی به واحد مدیریت حافظه تحویل دهد. در اینجاست که یکی دیگر از پیچیدگیهای طراحی سیستم مدیریت پایگاه داده روشن میشود و آن اینست که این سیستم باید بنحوی این دادهها را در فاصله بین COMMIT و نوشته شدن در حافظه برای سایر کاربران قابل مشاهده نماید.
در ادامه نمونه ای از یک تراکنش را مشاهده میکنید :
BEGIN TRANSACTION; INSERT INTO SP RELATION {S# S#(‘S5’), P# P#(‘P1’), QTY QTY(1000)}}; IF any error occurred THEN GOTO UNDO; END IF; UPDATE P WHERE P# = P#(‘P1’) TOTAL:=TOTAL + QTY(1000); IF any error occurred THEN GOTO UNDO; END IF; COMMIT; GOTO FINISH; UNDO: ROLLBACK; FINISH: RETURN;
همانطور که مشاهده میکنید تراکنش بالا دارای تمامی بخشهای اصلی تراکنش که ذکر شد میباشد. البته این امکان وجود دارد که صراحتا این کلمات را در تعریف بدنه تراکنش نیاوریم. بعنوان مثال میتوان از آوردن COMMIT صرف نظر کرد. در این صورت خود سیستم مدیریت پایگاه داده پس از اجرای آخرین دستور تراکنش در صورتی که هیچ خطایی رخ نداده باشد بطور خودکار عمل COMMIT را انجام میدهد. این امر در مورد ROLLBACK و END نیز صادق است. اما در مورد BEGIN TRANSACTION نکته ای وجود دارد و آن اینست که ما باید به پایگاه داده اعلام کنیم که بطور خودکار در پایان یک تراکنش برای شروع تراکنش بعدی BEGIN TRANSACTION را لحاظ کند. این کار را باید با دستور SET IMPLICIT TRANSACTION ON انجام دهیم.
گفتیم که وقوع خطا میتواند توسط برنامه نویس شناسایی شود و یا توسط سیستم. یک نمونه از تشخیص خطا توسط برنامه نویس را در مثال بالا مشاهده میکنید. عموما دراین قبیل خطاها پس از انجام عمل ROLLBACK تراکنش UNDO شده و اجرای آن متوقف میشود که اصطلاحا میگوییم تراکنش ABORT میشود. اما در مورد خطاهایی که خود سیستم تشخیص میدهد وضع به این منوال نیست. در شرایط خطا، سیستم پس از UNDO کردن تراکنش عموما آن را ABORT نمیکند بلکه مجددا اجرا میکند که به این عمل REDO گفته میشود. در بخشهای بعدی بطور کامل در مورد دو عمل REDO و UNDO بحث خواهیم کرد.
ویژگیهای تراکنشها :
هر تراکنشی که در سیستم اجرا میشود باید دارای چهار ویژگی باشد. در حقیقت این ویژگیها باید به نحوی تامین شوند تا مقصود و هدف کلی تراکنشها که بردن پایگاه داده از یک وضعیت صحیح به وضعیت صحیح دیگری است برآورده شود. در ادامه هر کدام را یک به یک شرح میدهیم :
Atomicity:
اولین ویژگی ای که یک تراکنش باید داشته باشد اینست که اثری که بر روی پایگاه داده ما میگذارد اثری کامل و بدون نقص باشد. به این معنا که اگر قرار است مجموعه از عملیات تغییراتی را اعمال کنند باید تمامی آن تغییرات بر روی جداول اعمال شوند. در صورتی که حتی یکی از عملیات با مشکل مواجه شود باید تاثیرات عملیات قبلی بازگردانده شوند. به بیانی سادهتر در تراکنش یا تمامی عملیات باید بطور کامل انجام شوند و یا هیچ یک از آنها نباید اجرا شده و اثرگذار باشند. به این ویژگی Atomicity گفته میشود.
توجه داشته باشید که در حین اجرای یک تراکنش احتمالا پایگاه داده به وضعیت غیر سازگار و نادرست خواهد رفت. یکی از وظایف سیستم مدیریت پایگاه داده اینست که این وضعیت ناسازگار را از دید سایر تراکنشها مخفی بسازد تا زمانی که تراکنش COMMIT شود.
در مورد Atomicity در برخی مقالات و مطالب آموزشی گفته میشود که این مفهوم یعنی تراکنش نباید قابل شکسته شدن باشد که این تعریف چندان صحیحی از Atomicity نمیباشد. چراکه یک تراکنش در حین اجرا ممکن است بارها و بارها شکسته شود و یا از یک تراکنش بر روی تراکنش دیگری سوئیچ شود. بنابراین مراد از Atomicity همان واحد کاری کامل است نه واحد کاری غیر قابل شکسته شدن.
Consistency:
تراکنش باید تغییرات را به گونه ای اعمال کند که پایگاه داده را از وضعیت صحیح به وضعیت صحیح دیگری ببرد.از آنجا که صحت پایگاه داده را قوانین جامعیت پایگاه داده (integrity rules) تضمین میکنند بنابراین تراکنش باید تغییرات را بگونه ای اعمال کند که این قوانین نقض نشوند. به این خاصیت از تراکنشها Consistency گفته میشود.
Isolation:
عموما برنامههای مبتنی بر پایگاه در دنیای واقعی برنامه هایی چند کاربره هستند که در برخی از آنها ممکن است میلیونها تراکنش بطور همزمان با یکدیگر در حال اجرا باشند. در چنین حجم بالایی یکی از مسائلی که مطرح میشود اینست که تراکنشهای موازی تاثیر سوئی بر روی یکدیگر نداشته باشند. بعنوان مثال یکی از مشکلاتی که در اجرای همروند و موازی تراکنشها ممکن است رخ دهد مشکل lost update میباشد. بر همین اساس یکی دیگر از ویژگی هایی که یک تراکنش باید داشته باشد که اینست که اثر سوئی بر روی تراکنشهای همروند دیگر نداشته باشد. به این ویژگی Isolation گفته میشود.
در مورد ایزولاسیون (isolation) تراکنشها باید گفت که ایزولاسیون سطوح و درجه بندی هایی دارد که هر کدام از این سطوح مشخص میکنند که تراکنشها تا چه حدی اجازه دارند بر روی هم تاثیر گذار باشند. در واقع این سطوح، میزان عایق بندی تراکنشها را نسبت به یکدیگر مشخص میکنند. هرچه درجه ایزولاسیون بالاتر باشند به این معنی است که تراکنشها تاثیر کمتری بر روی یکدیگر خواهند داشت. خوب در ظاهر ممکن است این قضیه بسیار خوب در نظر بیاید چرا که به ما اطمینان می دهد که اثر ناخواسته ای بر روی یکدیگر نخواهند داشت. اما باید این نکته را نیز در نظر بگیریم که هر چه درجه ایزولاسیون بالاتر باشد درجه همروندی (concurrency) پایین میآید و این به معنای کاهش امکان پردازش موازی تراکنشها میباشد. این مسئله در مورد پایگاههای داده بسیار بزرگ که میلیونها تراکنش همزمان در خواست اجرا داده میشوند به یک مسئله بحرانی و یک گلوگاه میتواند تبدیل شود. بنابراین تعیین درجه ایزولاسیون بسیار مهم است و باید با درنظر گرفته شرایط پروژه انجام گیرد.
اینکه پایگاه داده ما در چه سطحی از ایزولاسیون باید عمل نماید توسط کاربر تعیین میشود. البته بحث در مورد ارجای موازی تراکنشها و ایزولاسیون آنها بسیار مفصل است و انشاالله در مطلبی دیگر به آن خواهیم پرداخت.
Durability:
تغییراتی که تراکنشها بر روی پایگاه داده میگذارند باید بعد از COMMIT شدن آن پایدار و قابل مشاهده باشند. به این خاصیت durability گفته میشود.
وضعیتهای یک تراکنش :
تراکنشها در سیستم همانند یک موجودیت (entity) فعال است هستند. همانطور که میدانید سادهترین موجودیت فعال در سیستم فرآیندها (process) میباشند که cpu را بعنوان یک ابزار در اختیار گرفته و وظایفی را انجام میدهند. تراکنش نیز یک موجودیت فعال میباشد و همانند سایر موجودیتهای فعال دارای وضعیت هایی (state) میباشند که در ادامه هریک شرح داده شده اند :
• فعال (Active) : تراکنشی که در حالت اجرا است در وضعیت فعال میباشد.
• کامیت جزئی (Partially Committed): پس از اجرای آخرین دستور تراکنش به وضعیت کامیت جزئی میرود.
• شکست (Failed): در این وضعیت، در روند اجرا خطایی رخ داده و اجرای ادامه تراکنش امکان پذیر نمیباشد.
• خاتمه (Aborted): پس از تشخیص خطا تراکنش میتواند به وضعیت Aborted که در انجا اجرا متوفق شده و تغییرات ROLLBACK میشوند.
• Committed: در این وضعیت اجرای تراکنش با موفقیت انجام شده و تراکنش پایان میپذیرد.
در ادامه نمودار حالت تراکنشها نشاد داده شده است :
نکته ای که در اینجا لازم به ذکر است اینست که در حالت پس از حالت شکست به دو شکل امکان ادامه کار وجود دارد. در صورتی که خطای منطقی در تراکنش دیده شود که عموما توسط کاربر تشخیص داده میشود تراکش پس از شکست به حالت خاتمه برده میشود و کار تمام است. اما در برخی از شرایط خطایی سیستم توسط خود سیستم رخ میدهد. که در چنین حالاتی پس از شکست تراکنش مجددا تراکنش ممکن است به حالت فعال برگردانده شود و اجرای ان دوباره از ابتدای تراکنش شروع شود. به این وضعیت اصطلاحا REDO شدن تراکنش گفته میشود که در بخش RECOVERY و ترمیم پایگاه داده باید به آن پرداخته شود.
اعمال زمان COMMIT:
در زمان COMMIT (بصورت صریح و یا ضمنی) باید اعمالی انجام شود که در اینجا به آن میپردازیم. اولین کاری که صورت میگیرد اینست که سیگنالی به DBMS ارسال میشود مبنی بر اینکه تراکنش با موفقیت به پایان رسیده است. پس از اینکار سیستم مدیریت پایگاه داده شروع به آزاد کردن قفل هایی میکند که در طول اجرای تراکنش بر روی منابع مختلف پایگاه داده زده شده است تا از تاثیر سوء تراکنشها بر روی یکدیگر جلوگیری به عمل آید. علاوه بر کار ذکر شده تغییراتی که توسط تراکنش داده شده است باید پایدار و قابل رویت توسط سایر تراکنشها گردد.
همانطور که در بخش ابتدایی این مطلب آموزشی اشاره کردیم COMMIT به معنی نوشته شدن تغییرات بر روی دیسک سخت نیست. سیستم مدیریت پایگاه داده تنها درخواست نوشتن دادهها را به سیستم مدیریت حافظه میدهد و نوشتن ان بر عهده مدیریت حافظه میباشد. سیستم مدیریت پایگاه داده باید اطلاع داشته باشد که چه تغییراتی نوشته شده است و چه تغییراتی هنوز در حافظه نوشته نشده است. بنابراین یکی دیگر از پیچیدگیهای طراحی سیستمهای مدیریت پایگاه داده اینست که تغییراتی را برای سایرین قابل رویت کند که هنوز در حافظه سخت نوشته نشده است.
اعمال زمان ROLLBACK:
در زمان ROLLBACK ناموفق بودن تراکنش باید به DBMS اطلاع داده شود. پس از انکه سیستم مدیریت پایگاه داده مطلع شد تمامی تغییرات اعمال شده تا آن لحظه را UNDO میکند. البته توجه داشته باشید که در این زمان همانند زمان COMMIT قفلها نیز آزاد میشوند تا سایر تراکنشها بتوانند از منابع در اختیار این تراکنش استفاده کنند و درجه همروندی پایین نیاید.
پردازش پیامها در زمان اجرای تراکنشها :
به مثال زیر توجه کنید.
Read Sav_Amt Sav_Amt := Sav-Amt - 500 if Sav-Amt <0 then do put (“insufficient fund”) rollback end else do Write Sav_Amt Read Chk_Amt Chk_Amt := Chk_Amt + 500 Write Chk-Amt put (“transfer complete”) End transaction
در تراکنش بالا مبلغ 500 دلار از حساب فردی برداشته شده و به حساب دیگر او منتقل میشود. همانطور که مشاهده میکنید در خلال اجرای یک تراکنش ممکن است پیام هایی را به کاربر نمایش دهیم. حال در نظر بگیرید که در حین اجرا ما پیامی را در خروجی نمایش میدهیم و پس از آن تراکنش با شکست مواجه شده و ROLLBACK میگردد. در این شرایط پیامی به کاربر مبنی بر انتقال موفق نمایش داده شده است در حالی که در عمل تراکنش با شکست رو به رو شده است. برای حل این مشکل در ضمن کار پیامهای مختلفی که در خروجی باید نمایش داده شوند بافر میشوند تا پس از COMMIT یا ROLLBACK شدن به کاربر نمایش داده شوند. توجه داشته باشید که در زمان بافر کردن پیام ها، انها در دو گره پیامهای مربوط به COMMIT و پیامهای زمان ROLLBACK تقسیم میشوند تا هرکدام در شرایط خود نمایش داد شوند. این عمل توسط زیر سیستمی از DBMS بنام سیستم مدیریت ارتباطات داده ای (Data Communication Manager) انجام میگیرد.
انواع تراکنشها :
تراکنشها انواع و اقسام مختلفی دارند که به سبب پیچیدگی بعضی از آنها به لحاظ پیاده سازی ممکن است آنها را در برخی از پایگاه دادهها نداشته باشیم.
Flat Transactions:
سادهترین نوع تراکنشها میباشند که در تمامی پایگاههای داده پشتیبانی میشوند و مثال هایی که تا کنون در این نقاله زده شد از این دست میباشند.
Distributed Transactions:
این قبیل تراکنشها مربوط به پایگاه دادههای توزیع شده میباشند که دادههای آنها بر روی ماشینهای مختلفی قرار دارند. بر روی هریک از این ماشینها ممکن است DBMSهای مختلفی نیز نصب شده باشد که هر یک سیستم مدیریتی مربطو به خود را دارند. از آنجایی که هر یک از این ماشینها یک سیستم مدیریت پایگاه داده مستقل دارند بنابراین قوانین جامعیتی محلی ای را نیز باید لحاظ نمایند. البته باید توجه داشت که علاوع بر این قوانین محلی یک سری قوانین سراسری نیز وجود خواهد داشت که مربوط به کل پایگاه داده توزیع شده میباشد. بعنوان مثال سیستم در یکی سیستم دانشگاهی که در شهرهای مختلفی توزیع شده است، ممکن است بخواهیم تعداد کل دانشجویان ثبت نام شده در سیستم از هزار نفر بیشتر نباشد. عموما درچنین سیستم هایی یک DBMS مدیریت کننده نیز وجود دارد که مسئول برقراری هماهنگی بین سایر DBMSها و نیز اعمال اینگونه قوانین جامعیتی سراسری میباشد.
تراکنشهای توزیع شده یک یا چند تراکنش جزئی تشکیل شده اند که ممکن است هریک از آنها مربوط به یکی از DBMSهای سیستم باشد. چنین تراکنش هایی معمولا ابتدا توسط سیستم مدیریتی مرکزی دریافت میشوند و سپس هرکدام از پرس و جوهای داخلی آن به DBMS مربوطه ارسال میگردد. اجرای هرکدام از پرس و جوهای جزئی (که خود میتوانند تراکنشی مستقل نیر باشند) بطور مستقل و محلی بر روی ماشین مربوطه اجرا شده و در انتها نیز نتیجه اجرا به سیستم مدیریتی باز گردانده میشود. سیستم مدیریتی مرکزی منتظر میماند که تمامی تراکنشها اعلام COMMIT کنند تا از انجام موفقیت آمیز همه انها اطمینان حاصل نماید. پس از کسب اطمینان کل تراکنش توسط این سیستم مرکزی COMMIT شده و در نتیجه تغییرات بر روی پایگاه داده توزیه شده اعمال میشوند. به این سیاست COMMIT کردن، کامیت دو مرحله ای یا Two-phase Commit گفته میشود. توجه داشته باشید که در صورتی که هریک از DBMSها اعلام شکست نمایند تمامی تراکنش توزیع شده ROLLBACK میگردد.
tx_begin(); execute T1 //at site D execute T2 //at site C Execute T3 //at site B … tX_commit ();
همانطور که در مثال بالا مشاهده میکنید تراکنش اصلی از سه تراکنش T1، T2 و T3 تشکیل شده که مر بوط به سه سایت متفاوت میباشند. در زمانی تراکنش اصلی COMMIT خواهد شد که هر سه سایت اعلام موفقیت کنند.
تراکنشهای تو در تو (Nested Transaction):
این نوع از تراکنش نسبت به دو نوع تراکنش قبلی پیچیدگی بیشتری به لحاظ پیاده سازی و مدیریت دارند. این گونه تراکنشها عموما واحدهای کاری بزرگی هستند که در داخل آنها درختی از تراکنشهای تو در تو را داریم که مجموعه تمامی انها در نهایت یک کار واحد بلحاظ منطقی را انجام میدهند. هر یک از تراکنشهای داخلی بعنوان یک گره در این ساختار درختی قرار دارند که میتوانند پدر و یا فرزندانی داشته باشند.
• در تراکنشهای تو در تو شرایطی حاکم است.
• هر گره در ساختار درختی تراکنش تنها قادر به دیدن برادرهای خود میباشد. به بیان دیگر فرزندان برادران خود را نمیبیند و نسبت به انها هیچ اطلاعی ندارد.
• در تراکنشهای تو در تو امکان اجرای موازی فرزندان یک گره وجود دارد.
• امکان اجرای موازی تراکنشها منجر میشود به این که تراکنشهای داخلی قادر به دیدن خروجی حاصل از اجرا همدیگر نباشند.
• هر تراکنشی به طور مستقل ویژگی atomicity را دارد اما پایداری (durability) و کامیت شدن آنها وابسته به پدرانشان میباشد.
• در صورتی که پدری تصمیم بگیرد میتواند تمامی زیر تراکنش هایش را خاتمه (abort) دهد.
در تراکنشهای موازی COMMIT شدن یک گره پدر به دو صورت امکان پذیر است.
حالت AND: در این حالت یک تراکنش در صورتی کامیت خواهد شده که تمامی فرزندان آن با موفقیت اجرا و COMMIT شده باشند.
حالت OR: در این حالت اگر حتی یکی از تراکنشهای فرزند نیز موفق به COMMIT شده باشد تراکنش پدر نیز COMMIT خواهد شد.
تراکنشهای چند سطحی (Multi-level Transactions) :
این نوع نیز همانند تراکنشهای تو در تو پیچیده است. از نظر ساختاری تراکنشهای چند سطحی مشابه تراکنشهای تو در تو میباشند ولی به لحاظ مفهومی با یکدیگر متفاوت هستند. اولین تفاوت موجود بین این دو نوع اینست که هر زیر تراکنشی قادر است خروجی زیر تراکنشهای دیگر را ببیند. این مسئله باعث میشود که تنوانیم زیر تراکنشها را بصورت همروند و موازی اجرا کنیم که این دومین تفاوت مفهومی بین این دو میباشد. هنگامی که زیر تراکنش کامل شد (COMMIT) تمامی قفلهای مربوط به خود را آزاد میکند که این مورد نیز در مورد تراکنشهای تو در تو صادق نمیباشد. یکی از مهمترین تفاوتهای دیگر بین این دو نوع در اینست که در تراکنشهای چند سطحی تمامی برگها در یک سطح از درخت قرار دارند و تنها تراکنشهای برگ هستند که مستقیما به پایگاه داده مراجعه میکنند. در مورد کایت شدن نیز شروط مربوط به تراکنشهای تو در تو در اینجا وجود ندارند و زیر تراکنشها میتوانند بدون هیچ شرطی کامیت شوند.
تراکنشهای زنجیره ای (Chained Transaction):
همانطور که از نام این نوع از تراکنشها پیداست، این تراکنشها از زنجیره ای از زیر تراکنشهای پی در پی تشکیل شده اند. تا زمانی که تمامی حلقههای این زنجیر با موفقیت اجرا نشوند سیستم به حالت سازگاری نخواهد نرفت. دراین نوع از تراکنشهای COMMIT هر حلقه باعث پایداری شدن (durable) دادههای در پایگاه داده خواهد شد. این مسئله ممکن است پایگاه داده را به وضعیت ناسازگاری ببرد. در هنگام کامیت شدن هر حلقه قفلهای مربوط به آن نیز آزاد میشود.
حلقههای مختلف زنجیره تراکنشی میتوانند با یکدیگر تبادل اطلاعات کنند. البته توجه داشته باشید که منابعی که هر کدام از آنها بر روی آن کار میکنند با دیگری متفاوت میباشد. بعنوان نمونه تراکنشی را نظر بگیرد که قصد دارد متوسط مبلغ مکالمه تلفن همراه مشترکان یک مخابرات را محاسبه کند. بدلیل تعداد بالای مشترکان ممکن است این تراکنش را در قالب یک تراکنش زنجیره ای پیاده سازی کنیم که هر حلقه از آن مسئول محاسبه این مبلغ برای ده هزار نفر از کاربران باشد. توجه داشته باشید که برای بدست آوردن مقدار متوسط نیاز داریم که هر زیر تراکنشها قادر به تبادل اطلاعات باشند. از طرفی منابع مورد استفاده آنها (رکورد ها) با یکدیگر متفاوت خواهد بود و نمیتوانند تغییرات یکدیگر را ببینند. سوالی که مطرح میشود اینست که مبادله اطلاعات بین حلقههای تراکنش به چه صورت باید انجام شود؟ در جواب این سوال باید گفت که مبادله اطلاعات بین تراکنشها از طریق متغیرهای رابطه ای که هما متغیرهای پایگاه داده هستند انجام میگیرد.
SavePoint:
در برخی شرایط ممکن است بخواهیم در هنگام ROLLBACK مجددا به ابتدای تراکنش باز نگردیم تا مجبور باشیم دوباره کار را از ابتدا از سر بگیریم. بعنوان مثال تا قسمتی از تراکنش پیش رفتیم، به خطایی بر خورد میکنیم و میخواهیم از نقطه ای خاص از تراکنش کا را از سر بگیریم. در چنین کاربرد هایی از ابزاری بنام SavePoint استفاده میکنم.
برای روشنتر شدن مفهوم SavePoint فرض کنید قصد داریم بلیطی از تهران به سیدنی رزرو کنیم. برای این منظور ابتدا عمل رزرواسیون را از تهران به دوبی انجام میدهیم و سپس از دوبی به سنگاپور و در نهایت از سنگاپور به سیدنی. حال در این بین میتوانیم در نقطه تهران – دوبی SavePoint قرار دهیم تا در صورت بروز هرگونه خطا مجددا رزرواسیون را از ابتدا آغاز نکنیم. اگر در هنگام رزرو بلیط دوبی – سنگاپور خطایی بروز دهد میتوانیم به نقطه تهران – دوبی ROLLBACK کنیم و از آنجا مسیر دیگری را انتخاب کنیم. توجه داشته باشید که ROLLBACK به SavePoint وضعیت پایگاه داده به همان نقطه بازگردانده میشود.
begin transaction(); s1; sp1:= create savepoint(0); s2; sp2:= create savepoint(0); if (condition) rollback (spi); … … commit
Auto Transaction:
این قبیل تراکنشها تراکنشهای کوچکی هستند که توسط سیستم تعریف میشوند. بعنوان مثال سیستم برای انجام دستورات زیر تراکنش تعریف میکند :
Alter table, Create, delete, insert, open, drop, fetch, grant, revoke, select, truncate table, update
یکی از علتهای این امر اینست که در صورت بروز خطا در حین این تراکنشهای خود کار امکان اجرای مجدد هر کدام فراهم گردد.
شروع تراکنشها :
همانطور که گفته شد برای شروع تراکنشها میتوانیم صراحتا از BEGIN TRANSACTION استفاده کنیم. البته راهکار دیگری نیز وجود دارد که در آن میتوانیم به DBMS اعلام کنیم که با پایان یک تراکنش پیش از شروع تراکنش بعدی BEGIN TRANSACTION را قرار بده. برای این منظور از دستور زیر استفاده میکنیم :
Set implicit_transaction on
SET TRANSACTION READ ONLY
همچنین میتوان اجازه تغییر را به آن داد :
SET TRANSACTION READ WRITE
READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE
من الان دارم روی پروژه ای کار میکنم که هم نیاز به سایت دارد و هم نیاز به App اندروید. حالا میخواستم بدونم بهتره از کدام روش استفاده کنم برای اعتبار سنجی: 1- روش اعتبار سنجی مبتنی بر JWT که توی این پست شما توضیح دادین و یا 2- ASP.NET Core Identity
دکمه F5 را بزنید و برنامه را اجرا کنید. یک دکمه میبینید که Text آن عبارت + است.
سلام :)
اگر این آموزش برای مبتدیهاست:
من پروژه رو برای اندروید و ios ایجاد کردم و چنین دکمه، لیبل و عملکردی به صورت پیشفرض در Template پروژه نیست. به نظر میاد کاربر باید به صورت پیشفرض چند خط پایین بره، کنترلها رو اضافه کنه، کد رو بنویسه و چند خط بالا بیاد و از خط دکمه f5 شروع کنه به ادامه مقاله!
همچنین خوبه که توضیحی دقیقتر برای کلاس HelloWorldViewModel ارائه بشه. فکر کنم در آموزش قدم به قدم نیاز است هر مرحله که شما انجام میدید توضیح و آموزش داده بشه. مثلا BitViewModelBase چی هست؟! و اینکه چرا به این صورت در Xamarin.Form رویدادها به دکمهها نسبت داده میشه. به نظرم در بخشی از آموزش، مطالب دقیق گفته میشه و برای بعضی دیگه خیر.
شاید خوب باشه در ابتدا گفته بشه سطح آموزش و پیشنیازها چیه. من دانش کافی از XAML , Csharp دارم ولی به عنوان یک مبتدی وقتی بخوام از این فناوریها در یک مبحث جدید و متفاوت استفاده کنم احتمالا سوالات زیادی پیش میاد که ممکنه برای نویسنده مقاله بدیهی به نظر برسه.
موفق باشید :)
تا اینجا تمام قسمتهای این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شدهی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایهی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه میخواهیم این قسمت را با اطلاعات ذخیره شدهی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژهی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام میدهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شدهی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شدهی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.
تکمیل ساختار پروژهی IDP
تا اینجا برای IDP، یک پروژهی خالی وب را ایجاد و به مرور، آنرا تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژهی Class library کمکی را نیز به Solution آن اضافه میکنیم:
- DNT.IDP.DomainClasses
در این پروژه، کلاسهای متناظر با موجودیتهای جداول مرتبط با اطلاعات کاربران قرار میگیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آنرا تشکیل میدهد. همچنین به همراه تنظیمات و Seed اولیهی اطلاعات بانک اطلاعاتی نیز میباشد.
رشتهی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شدهاست.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژهی مشترک بین چند پروژهی دیگر قرار گرفتهاست. از آن جهت هش کردن کلمات عبور، در دو پروژهی DataLayer و همچنین Services استفاده میکنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار میکند، در این پروژه قرار گرفتهاست.
ساختار بانک اطلاعاتی کاربران IdentityServer
در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آنها تشکیل میشود:
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار میرود.
ساختار Claims او نیز به صورت زیر تعریف میشود که با تعریف یک Claim استاندارد، سازگاری دارد:
همچنین کاربر میتوان تعدادی لاگین نیز داشته باشد:
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آنها است.
در پروژهی DNT.IDP.DataLayer در پوشهی Configurations آن، کلاسهای UserConfiguration و UserClaimConfiguration را مشاهده میکنید که حاوی اطلاعات اولیهای برای تشکیل User 1 و User 2 به همراه Claims آنها هستند. این اطلاعات را دقیقا از فایل استاتیک Config که در قسمتهای قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کردهایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونهی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار میکند، چنین ساختاری را دارد:
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویسها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگیها، صورت گرفتهاند. همچنین در اینجا متد initializeDb را نیز مشاهده میکنید که با فراخوانی متد context.Database.Migrate، تمام کلاسهای Migrations پروژهی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال میکند.
غیرفعال کردن صفحهی Consent در Quick Start UI
در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایلهای Quick Start UI را به پروژهی IDP اضافه کردیم. در ادامه میخواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحهی Consent در Quick Start UI، لیست scopes درخواستی برنامهی کلاینت ذکر شده و سپس کاربر انتخاب میکند که کدامیک از آنها، باید به برنامهی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامههای کلاینت توسط ما توسعه یافتهاند، بیمعنا است و صرفا برای کلاینتهای ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا میکند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.
تغییر نام پوشهی Quickstart و سپس اصلاح فضای نام پیشفرض کنترلرهای آن
در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشهی Quickstart برنامهی IDP قرار گرفتهاند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشهی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیشفرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژهی جاری تغییر میدهیم. برای مثال کنترلر Account واقع در پوشهی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترلها عمل میکنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آنرا نیز کامپایل میکند و اگر خطایی در آنها وجود داشته باشد، امکان بررسی و رفع آنها پیش از اجرای برنامه، میسر است.
و یا میتوان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
تعامل با IdentityServer از طریق کدهای سفارشی
پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویسهای متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده میکند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شدهی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظهای برنامه را از متد ()Config.GetUsers دریافت میکنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی میکنیم:
- ابتدا یک TestUserStore را به صورت Singleton ثبت کردهاست.
- سپس سرویس پروفایل کاربران را اضافه کردهاست. این سرویس با پیاده سازی اینترفیس IProfileService تهیه میشود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آنها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمهی عبور و نام کاربری را در صورت استفادهی از Flow ویژهای به نام ResourceOwner که استفادهی از آن توصیه نمیشود (ROBC Flow)، انجام میدهد.
برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشهی پروژهی IDP با محتوای ذیل اضافه میکنیم:
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شدهاست که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار میکند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شدهاست. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شدهاست.
سپس یک ProfileService سفارشی را ثبت کردهایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی میشود:
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آنرا تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا میکند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن میتوان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفادههای بعدی، ذخیره کرد.
اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین میکنیم:
تا اینجا فقط این سرویسهای جدید را ثبت کردهایم، اما هنوز کار خاصی را انجام نمیدهند و باید از آنها در برنامه استفاده کرد.
اتصال IdentityServer به User Store سفارشی
در ادامه، سازندهی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی میکنیم:
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4 را انجام میدهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینتها را ارائه میدهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار میگیرد.
- IEventService رخدادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش میدهد.
- در آخر، TestUserStore تزریق شدهاست که میخواهیم آنرا با User Store سفارشی خودمان جایگزین کنیم. بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین میکنیم:
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه میداریم. از این جهت که قسمتهای لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده میکنند و آنرا در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده میکنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمهی عبور و همچنین یافتن کاربر متناظر با آن:
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شدهی به سیستم نمایش داده میشوند که بیانگر صحت عملکرد CustomUserProfileService است.
افزودن امکان ثبت کاربران جدید به برنامهی IDP
پس از اتصال قسمت login برنامهی IDP به بانک اطلاعاتی، اکنون میخواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبتنام را تشکیل میدهد، ابتدا با نام کاربری و کلمهی عبور شروع میشود:
سپس سایر خواصی که در اینجا اضافه میشوند:
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل میدهد. ابتدا نام کاربری و کلمهی عبور را در جدول کاربران ثبت میکند و سپس سایر خواص این ViewModel را در جدول UserClaims:
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه میدهد که توسط آن، کاربری میتواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده میشود. به همین جهت بهتر است فضای نام آنرا به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحهی لاگین در Login.cshtml
این لینک دقیقا در ذیل چکباکس Remember My Login اضافه شدهاست.
اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده میکنیم که صفحهی لاگین به همراه لینک ثبت نام ظاهر میشود:
و پس از کلیک بر روی آن، صفحهی ثبت کاربر جدید به صورت زیر نمایش داده خواهد شد:
برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شدهاست، این کاربر یکسری از لینکهای برنامهی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
تکمیل ساختار پروژهی IDP
تا اینجا برای IDP، یک پروژهی خالی وب را ایجاد و به مرور، آنرا تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژهی Class library کمکی را نیز به Solution آن اضافه میکنیم:
- DNT.IDP.DomainClasses
در این پروژه، کلاسهای متناظر با موجودیتهای جداول مرتبط با اطلاعات کاربران قرار میگیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آنرا تشکیل میدهد. همچنین به همراه تنظیمات و Seed اولیهی اطلاعات بانک اطلاعاتی نیز میباشد.
رشتهی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شدهاست.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژهی مشترک بین چند پروژهی دیگر قرار گرفتهاست. از آن جهت هش کردن کلمات عبور، در دو پروژهی DataLayer و همچنین Services استفاده میکنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار میکند، در این پروژه قرار گرفتهاست.
ساختار بانک اطلاعاتی کاربران IdentityServer
در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آنها تشکیل میشود:
namespace DNT.IDP.DomainClasses { public class User { [Key] [MaxLength(50)] public string SubjectId { get; set; } [MaxLength(100)] [Required] public string Username { get; set; } [MaxLength(100)] public string Password { get; set; } [Required] public bool IsActive { get; set; } public ICollection<UserClaim> UserClaims { get; set; } public ICollection<UserLogin> UserLogins { get; set; } } }
ساختار Claims او نیز به صورت زیر تعریف میشود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses { public class UserClaim { public int Id { get; set; } [MaxLength(50)] [Required] public string SubjectId { get; set; } public User User { get; set; } [Required] [MaxLength(250)] public string ClaimType { get; set; } [Required] [MaxLength(250)] public string ClaimValue { get; set; } } }
namespace DNT.IDP.DomainClasses { public class UserLogin { public int Id { get; set; } [MaxLength(50)] [Required] public string SubjectId { get; set; } public User User { get; set; } [Required] [MaxLength(250)] public string LoginProvider { get; set; } [Required] [MaxLength(250)] public string ProviderKey { get; set; } } }
در پروژهی DNT.IDP.DataLayer در پوشهی Configurations آن، کلاسهای UserConfiguration و UserClaimConfiguration را مشاهده میکنید که حاوی اطلاعات اولیهای برای تشکیل User 1 و User 2 به همراه Claims آنها هستند. این اطلاعات را دقیقا از فایل استاتیک Config که در قسمتهای قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کردهایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونهی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار میکند، چنین ساختاری را دارد:
public interface IUsersService { Task<bool> AreUserCredentialsValidAsync(string username, string password); Task<User> GetUserByEmailAsync(string email); Task<User> GetUserByProviderAsync(string loginProvider, string providerKey); Task<User> GetUserBySubjectIdAsync(string subjectId); Task<User> GetUserByUsernameAsync(string username); Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId); Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId); Task<bool> IsUserActiveAsync(string subjectId); Task AddUserAsync(User user); Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey); Task AddUserClaimAsync(string subjectId, string claimType, string claimValue); }
تنظیمات نهایی این سرویسها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگیها، صورت گرفتهاند. همچنین در اینجا متد initializeDb را نیز مشاهده میکنید که با فراخوانی متد context.Database.Migrate، تمام کلاسهای Migrations پروژهی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال میکند.
غیرفعال کردن صفحهی Consent در Quick Start UI
در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایلهای Quick Start UI را به پروژهی IDP اضافه کردیم. در ادامه میخواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحهی Consent در Quick Start UI، لیست scopes درخواستی برنامهی کلاینت ذکر شده و سپس کاربر انتخاب میکند که کدامیک از آنها، باید به برنامهی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامههای کلاینت توسط ما توسعه یافتهاند، بیمعنا است و صرفا برای کلاینتهای ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا میکند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.
تغییر نام پوشهی Quickstart و سپس اصلاح فضای نام پیشفرض کنترلرهای آن
در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشهی Quickstart برنامهی IDP قرار گرفتهاند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشهی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیشفرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژهی جاری تغییر میدهیم. برای مثال کنترلر Account واقع در پوشهی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترلها عمل میکنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آنرا نیز کامپایل میکند و اگر خطایی در آنها وجود داشته باشد، امکان بررسی و رفع آنها پیش از اجرای برنامه، میسر است.
و یا میتوان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account; @using DNT.IDP.Controllers.Consent; @using DNT.IDP.Controllers.Grants; @using DNT.IDP.Controllers.Home; @using DNT.IDP.Controllers.Diagnostics; @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
تعامل با IdentityServer از طریق کدهای سفارشی
پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویسهای متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده میکند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شدهی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظهای برنامه را از متد ()Config.GetUsers دریافت میکنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی میکنیم:
public static class IdentityServerBuilderExtensions { public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users) { builder.Services.AddSingleton(new TestUserStore(users)); builder.AddProfileService<TestUserProfileService>(); builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>(); return builder; } }
- سپس سرویس پروفایل کاربران را اضافه کردهاست. این سرویس با پیاده سازی اینترفیس IProfileService تهیه میشود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آنها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمهی عبور و نام کاربری را در صورت استفادهی از Flow ویژهای به نام ResourceOwner که استفادهی از آن توصیه نمیشود (ROBC Flow)، انجام میدهد.
برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشهی پروژهی IDP با محتوای ذیل اضافه میکنیم:
using DNT.IDP.Services; using Microsoft.Extensions.DependencyInjection; namespace DNT.IDP { public static class IdentityServerBuilderExtensions { public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder) { // builder.Services.AddScoped<IUsersService, UsersService>(); builder.AddProfileService<CustomUserProfileService>(); return builder; } } }
سپس یک ProfileService سفارشی را ثبت کردهایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی میشود:
namespace DNT.IDP.Services { public class CustomUserProfileService : IProfileService { private readonly IUsersService _usersService; public CustomUserProfileService(IUsersService usersService) { _usersService = usersService; } public async Task GetProfileDataAsync(ProfileDataRequestContext context) { var subjectId = context.Subject.GetSubjectId(); var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId); context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList(); } public async Task IsActiveAsync(IsActiveContext context) { var subjectId = context.Subject.GetSubjectId(); context.IsActive = await _usersService.IsUserActiveAsync(subjectId); } } }
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن میتوان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفادههای بعدی، ذخیره کرد.
اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین میکنیم:
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddIdentityServer() .AddDeveloperSigningCredential() .AddCustomUserStore() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients());
اتصال IdentityServer به User Store سفارشی
در ادامه، سازندهی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی میکنیم:
public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IAuthenticationSchemeProvider schemeProvider, IEventService events, TestUserStore users = null) { _users = users ?? new TestUserStore(TestUsers.Users); _interaction = interaction; _clientStore = clientStore; _schemeProvider = schemeProvider; _events = events; }
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینتها را ارائه میدهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار میگیرد.
- IEventService رخدادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش میدهد.
- در آخر، TestUserStore تزریق شدهاست که میخواهیم آنرا با User Store سفارشی خودمان جایگزین کنیم. بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین میکنیم:
private readonly TestUserStore _users; private readonly IUsersService _usersService; public AccountController( // ... IUsersService usersService) { _usersService = usersService; // ... }
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده میکنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمهی عبور و همچنین یافتن کاربر متناظر با آن:
public async Task<IActionResult> Login(LoginInputModel model, string button) { //... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username);
افزودن امکان ثبت کاربران جدید به برنامهی IDP
پس از اتصال قسمت login برنامهی IDP به بانک اطلاعاتی، اکنون میخواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبتنام را تشکیل میدهد، ابتدا با نام کاربری و کلمهی عبور شروع میشود:
public class RegisterUserViewModel { // credentials [MaxLength(100)] public string Username { get; set; } [MaxLength(100)] public string Password { get; set; }
public class RegisterUserViewModel { // ... // claims [Required] [MaxLength(100)] public string Firstname { get; set; } [Required] [MaxLength(100)] public string Lastname { get; set; } [Required] [MaxLength(150)] public string Email { get; set; } [Required] [MaxLength(200)] public string Address { get; set; } [Required] [MaxLength(2)] public string Country { get; set; }
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل میدهد. ابتدا نام کاربری و کلمهی عبور را در جدول کاربران ثبت میکند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser { Password=model.Password.GetSha256Hash(), Username=model.Username, IsActive=true }; userToCreate.UserClaims.Add(newUserClaim("country",model.Country)); userToCreate.UserClaims.Add(newUserClaim("address",model.Address)); userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname)); userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname)); userToCreate.UserClaims.Add(newUserClaim("email",model.Email)); userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
این فایل، view متناظر با ViewModel فوق را ارائه میدهد که توسط آن، کاربری میتواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده میشود. به همین جهت بهتر است فضای نام آنرا به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحهی لاگین در Login.cshtml
این لینک دقیقا در ذیل چکباکس Remember My Login اضافه شدهاست.
اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده میکنیم که صفحهی لاگین به همراه لینک ثبت نام ظاهر میشود:
و پس از کلیک بر روی آن، صفحهی ثبت کاربر جدید به صورت زیر نمایش داده خواهد شد:
برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شدهاست، این کاربر یکسری از لینکهای برنامهی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.