IIS ماژولی به نام URL Rewrite برای اینکار داره. با برنامه نویسی هم قابل کنترل است.
با آمدن ORMها به دنیای برنامه نویسی، کار برنامه نویسی نسبت به قبل سادهتر و راحتتر شد. عدم استفاده کوئریهای دستی، پشتیبانی از چند دیتابیس و از همه مهمتر و اصلیترین هدف این ابزار "تنها درگیری با اشیا و مدل شیء گرایی" کار را پیش از پیش آسانتر نمود.
در این بین به راحتی میتوان چندین نمونه از این ORMها را نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آنها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفادهی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش میآید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آنها بهتر هستند؟ شاید در اولین مورد کتابخانهی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیتهای آن صحبت میکنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگیهای متعدد و دارا بودن بسیاری از قابلیتهایی که اصلا در بانکهای اطلاعاتی موبایل قابل اجرا نیست، باعث میشود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.
معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
OrmLight
این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شدهاست که بسیاری از برنامه نویسان از آن در برنامههای اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.
نحوه معرفی جداول در این فریمورک به صورت زیر است:
در این بین به راحتی میتوان چندین نمونه از این ORMها را نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آنها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفادهی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش میآید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آنها بهتر هستند؟ شاید در اولین مورد کتابخانهی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیتهای آن صحبت میکنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگیهای متعدد و دارا بودن بسیاری از قابلیتهایی که اصلا در بانکهای اطلاعاتی موبایل قابل اجرا نیست، باعث میشود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.
معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
- سبک بودن: مهمترین مورد سبک بودن آن است؛ چه از لحاظ اجرای برنامه و چه از لحاظ حجم نهایی برنامه
- سریع بودن: مطمئنا ORMهای طراحی شدهی موجود، از سرعت خیلی بدی برخوردار نخواهند بود؛ اگر سر زبان هم افتاده باشند. ولی باز هم انتخاب سریع بودن یک ORM، مورد علاقهی بسیاری از ماهاست.
- یادگیری آسان و کانفیگ راحت تر.
OrmLight
این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شدهاست که بسیاری از برنامه نویسان از آن در برنامههای اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.
نحوه معرفی جداول در این فریمورک به صورت زیر است:
@DatabaseTable(tableName = "users") public class User { @DatabaseField(id = true) private String username; @DatabaseField private String password; public User() { // ORMLite needs a no-arg constructor } public User(String username, String password) { this.username = username; this.password = password; } // Implementing getter and setter methods public String getUserame() { return this.username; } public void setName(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
سورس این فریمورک را میتوان در گیت هاب یافت و مستندات آن در این آدرس قرار دارند.
SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد میماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی میکند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی میکند.
برای استفاده از این فریمورک نیاز است ابتدا متادیتاهای زیر را به فایل manifest اضافه کنید:
SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد میماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی میکند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی میکند.
برای استفاده از این فریمورک نیاز است ابتدا متادیتاهای زیر را به فایل manifest اضافه کنید:
<meta-data android:name="DATABASE" android:value="my_database.db" /> <meta-data android:name="VERSION" android:value="1" /> <meta-data android:name="QUERY_LOG" android:value="true" /> <meta-data android:name="DOMAIN_PACKAGE_NAME" android:value="com.my-domain" />
برای تبدیل یک کلاس به جدول هم از کلاس این فریم ورک ارث بری میکنیم:
public class User extends SugarRecord<User> { String username; String password; int age; @Ignore String bio; //this will be ignored by SugarORM public User() { } public User(String username, String password,int age){ this.username = username; this.password = password; this.age = age; } }
باقی عملیات آن از قبیل اضافه کردن یک رکورد جدید یا حذف رکورد(ها) به صورت زیر است:
User johndoe = new User(getContext(),"john.doe","secret",19); johndoe.save(); //ذخیره کاربر جدید در دیتابیس //حذف تمامی کاربرانی که سنشان 19 سال است List<User> nineteens = User.find(User.class,"age = ?",new int[]{19}); foreach(user in nineteens) { user.delete(); }
GreenDAO
موقعیکه بحث کارآیی و سرعت پیش میآید نام GreenDAO هست که میدرخشد. طبق گفتهی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامههایی است که از این فریمورک استفاده میکنند. جدول زیر مقایسهای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل میکند.
موقعیکه بحث کارآیی و سرعت پیش میآید نام GreenDAO هست که میدرخشد. طبق گفتهی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامههایی است که از این فریمورک استفاده میکنند. جدول زیر مقایسهای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل میکند.
غیر از اینها در زمینهی حجم هم حرفهایی برای گفتن دارد. حجم این کتابخانه کمتر از 100 کیلوبایت است که در اندازهی APK اثر چندانی نخواهد داشت.
آموزش راه اندازی آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.
Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که میتوانید روش استفادهی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
آموزش راه اندازی آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.
Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که میتوانید روش استفادهی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
<meta-data android:name="AA_DB_NAME" android:value="my_database.db" /> <meta-data android:name="AA_DB_VERSION" android:value="1" />
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActiveAndroid.initialize(this); //ادامه برنامه } }
@Table(name = "User") public class User extends Model { @Column(name = "username") public String username; @Column(name = "password") public String password; public User() { super(); } public User(String username,String password) { super(); this.username = username; this.password = password; } }
ORMDroid
از آن دست کتابخانههایی است که سادگی و کم حجم بودن شعار آنان است و سعی دارند تا حد ممکن همه چیز را خودکار کرده و کمترین کانفیگ را نیاز داشته باشد. حجم فعلی آن حدود 20 کیلوبایت بوده و نمیخواهند از 30 کیلوبایت تجاوز کند.
برای استفادهی از آن ابتدا دو خط زیر را جهت معرفی تنظیمات به manifest اضافه کنید:
<meta-data android:name="ormdroid.database.name" android:value="your_database_name" /> <meta-data android:name="ormdroid.database.visibility" android:value="PRIVATE||WORLD_READABLE||WORLD_WRITEABLE" />
ORMDroidApplication.initialize(someContext);
public class Person extends Entity { public int id; public String name; public String telephone; } //==================== Person p = Entity.query(Person.class).where("id=1").execute(); p.telephone = "555-1234"; p.save(); // یا Person person = Entity.query(Person.class).where(eql("id", id)).execute(); p.telephone = "555-1234"; p.save();
کد بالا دقیقا یادآوری به EF هست ولی حیف که از Linq پشتیبانی نمیشود.
سورس آن در گیت هاب
در اینجا سعی کردیم تعدادی از کتابخانههای محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمیشود. ORMهای دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.
نکته نهایی اینکه خوب میشود دوستانی که از این ORMهای مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آنها بیان کنند و مزایا و معایب آنها را بیان کنند.
سورس آن در گیت هاب
در اینجا سعی کردیم تعدادی از کتابخانههای محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمیشود. ORMهای دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.
نکته نهایی اینکه خوب میشود دوستانی که از این ORMهای مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آنها بیان کنند و مزایا و معایب آنها را بیان کنند.
مطالب
آموزش TypeScript #4
در پستهای قبل با کلیات و primitive types در زبان TypeScript آشنا شدیم:
اما به صورت معمول سعی میشود هر ماژول در یک فایل جداگانه تعریف شود.
استفاده از چند ماژول در یک فایل به مرور، درک پروژه را سخت خواهد کرد و در
هنگام توسعه امکان برخورد با مشکل وجود خواهد داشت. برای مثال اگر یک فایل
به نام MyModule.ts داشته باشیم که یک ماژول به این نام را شامل شود بعد از
کامپایل یک فایل به نام MyModule.js ایجاد خواهد شد.
کلاس ها:
برای تعریف یک کلاس میتوانیم همانند دات نت از کلمه کلیدی class استفاده کنیم. بعد از تعریف کلاس میتوانیم متغیرها و توابع مورد نظر را در این کلاس قرار داده و تعریف کنیم.
نکته مهم و جالب قسمت بالا کلمه export است. export معادل public در دات نت
است و کلاس logger را قابل دسترس در خارج ماژول Utilities خواهد کرد. اگر
از export در هنگام تعریف کلاس استفاده نکنیم این کلاس فقط در سایر
کلاسهای تعریف شده در داخل همان ماژول قابل دسترس است.
تابع log که در کلاس بالا تعریف کردیم به صورت پیش فرض public یا عمومی است و نیاز به استفاده export نیست.
برای استفاده از کلاس بالا باید این کلمه کلیدی new استفاده کنیم.
برای تعریف سازنده برای کلاس بالا باید از کلمه کلیدی constructor استفاده نماییم:
استفاده از پارامترهای Rest
منظور از پارامترهای Rest یعنی در هنگام فراخوانی توابع محدودیتی برای تعداد پارامترها نیست که معادل params در دات نت است. برای تعریف این گونه پارامترهاکافیست به جای params از ... استفاده نماییم.
تعریف توابع خصوصی
در TypeScript امکان توابع خصوصی با کلمه کلیدی private امکان پذیر است. همانند دات نت با استفاده از کلمه کلیدی private میتوانیم کلاسی تعریف کنیم که فقط برای همان کلاس قابل دسترس باشد(به صورت پیش فرض توابع به صورت عمومی هستند).
از آن جا که تابع getTimeStamp به صورت خصوصی تعریف شده است در نتیجه امکان
استفاده از آن در خارج کلاس وجود ندارد. اگر سعی بر استفاده این تابع
داشته باشیم توسط کامپایلر با یک warning مواجه خواهیم شد.
و استفاده از این تابع بدون وهله سازی از کلاس :
Function Overload
همان گونه که در دات نت امکان overload کردن توابع میسر است در TypeScript هم این امکان وجود دارد.
ادامه دارد...
در این پست به مفاهیم شی گرایی در این زبان میپردازیم.
تعریف یک ماژول: برای تعریف یک ماژول باید از کلمه کلیدی module استفاده
کنید. یک ماژول معادل یک ظرف است برای نگهداری کلاسها و اینترفیسها و
سایر ماژول ها. کلاسها و اینترفیسها در TypeScript میتوانند به صورت
internal یا public باشند(به صورت پیش فرض internal است؛ یعنی فقط در همان ماژول قابل استفاده و فراخوانی است). هر چیزی که در داخل یک ماژول تعریف میشود
محدوده آن در داخل آن ماژول خواهد بود. اگر قصد توسعه یک پروژه در مقیاس
بزرگ را دارید میتوانید همانند دات نت که در آن امکان تعریف فضای نامهای
تودرتو امکان پذیر است در TypeScript نیز، ماژولهای تودرتو تعریف
کنید. برای مثال:
module MyModule1 { module MyModule2 { } }
کلاس ها:
برای تعریف یک کلاس میتوانیم همانند دات نت از کلمه کلیدی class استفاده کنیم. بعد از تعریف کلاس میتوانیم متغیرها و توابع مورد نظر را در این کلاس قرار داده و تعریف کنیم.
module Utilities { export class Logger { log(message: string): void{ if(typeofwindow.console !== 'undefined') { window.console.log(message); } } } }
تابع log که در کلاس بالا تعریف کردیم به صورت پیش فرض public یا عمومی است و نیاز به استفاده export نیست.
برای استفاده از کلاس بالا باید این کلمه کلیدی new استفاده کنیم.
window.onload = function() { varlogger = new Utilities.Logger(); logger.log('Logger is loaded'); };
export class Logger{ constructor(private num: number) { }
با کمی دقت متوجه تعریف متغیر num به صورت private خواهید شد که برخلاف انتظار ما در زبانهای دات نتی است. بر خلاف دات نت در زبان TypeScript، دسترسی به متغیر تعریف شده در سازنده با کمک اشاره گر this در هر جای کلاس ممکن میباشد. در نتیجه نیازی به تعریف متغیر جدید و پاس دادن مقادیر این متغیرها به این فیلدها نمیباشد.
اگر به تابع log دقت کنید خواهید دید که یک پارامتر ورودی به نام message دارد که نوع آن string است. در ضمن Typescript از پارامترهای اختیاری( پارامتر با مقدار پیش فرض) نیز پشتیبانی میکند. مثال:
اگر به تابع log دقت کنید خواهید دید که یک پارامتر ورودی به نام message دارد که نوع آن string است. در ضمن Typescript از پارامترهای اختیاری( پارامتر با مقدار پیش فرض) نیز پشتیبانی میکند. مثال:
pad(num: number, len: number= 2, char: string= '0')
منظور از پارامترهای Rest یعنی در هنگام فراخوانی توابع محدودیتی برای تعداد پارامترها نیست که معادل params در دات نت است. برای تعریف این گونه پارامترهاکافیست به جای params از ... استفاده نماییم.
function addManyNumbers(...numbers: number[]) { var sum = 0; for(var i = 0; i < numbers.length; i++) { sum += numbers[i]; } returnsum; } var result = addManyNumbers(1,2,3,5,6,7,8,9);
در TypeScript امکان توابع خصوصی با کلمه کلیدی private امکان پذیر است. همانند دات نت با استفاده از کلمه کلیدی private میتوانیم کلاسی تعریف کنیم که فقط برای همان کلاس قابل دسترس باشد(به صورت پیش فرض توابع به صورت عمومی هستند).
module Utilities { Export class Logger { log(message: string): void{ if(typeofwindow.console !== 'undefined') { window.console.log(this.getTimeStamp() + ' -'+ message); window.console.log(this.getTimeStamp() + ' -'+ message); } } private getTimeStamp(): string{ var now = newDate(); return now.getHours() + ':'+ now.getMinutes() + ':'+ now.getSeconds() + ':'+ now.getMilliseconds(); } } }
یک نکته مهم این است که کلمه private فقط برای توابع و متغیرها قابل استفاده است.
تعریف توابع static:
در TypeScript امکان تعریف توابع static وجود دارد. همانند دات نت باید از کلمه کلیدی static استفاده کنیم.
classFormatter { static pad(num: number, len: number, char: string): string{ var output = num.toString(); while(output.length < len) { output = char + output; } returnoutput; } } }
Formatter.pad(now.getSeconds(), 2, '0') +
همان گونه که در دات نت امکان overload کردن توابع میسر است در TypeScript هم این امکان وجود دارد.
static pad(num: number, len?: number, char?: string); static pad(num: string, len?: number, char?: string); static pad(num: any, len: number= 2, char: string= '0') { var output = num.toString(); while(output.length < len) { output = char + output; } returnoutput; }
ادامه دارد...
در قسمت قبلی نحوهی ساخت اسمبلی را یاد گرفتیم. ولی ممکن است که بخواهید اسمبلی را از طریق Assembly Linker یا AL.exe ایجاد کنید. این روش موقعی سودمند است که بخواهید یک اسمبلی از ماژولها از کامپایلرهای مختلف را ایجاد کنید یا اینکه کامپایلر شما مانند کامپایلر سی شارپ از دستور یا سوئیچی مشابه addmodule استفاده نمیکند. یا حتی اینکه در زمان کامپایل هنوز اطلاعاتی از نیازمندیهای اسمبلیها ندارید و به بعد موکول میکنید. از AL همچنین میتوانید در زمینهی ساخت اسمبلیهای فقط ریسورس هم استفاده کنید که میتواند جهت انجام localization به کار رود. AL میتواند یک فایل dll یا exe تولید کند که شامل یک فایل manifest بوده که اشاره به ماژولهای تشکیل دهندهاش دارد.
نحوهی ساخت اسمبلی با استفاده از ابزار AL :
تصویر زیر نتیجهی دستور بالاست:
csc /t:module RUT.cs csc /t:module FUT.cs al /out: MultiFileLibrary.dll /t:library FUT.netmodule RUT.netmodule
در این مثال ما دو ماژول جدا به نامهای RUT.netmodule و FUT.netmodule را در یک اسمبلی ایجاد کردهایم. داخل این اسمبلیها جدول متادیتا یا بخش IL از ماژولها به چشم نمیخورد. به این معنی که کد IL و جداول مربوطه به آن، هر کدام داخل ماژول یا فایل خودش بوده و در اسمبلی کدی وجود ندارد و تنها یک جدول مانیفست جهت شناسایی ماژولهایش دارد. شکل بالا گویای اطلاعات داخلی اسمبلی است که میتوانید با تصویری که در قسمت قبلی درج شده مقایسه کنید.
تصویر قسمت قبلی جهت مقایسه:
در این حالت سه فایل تشکیل شده است که یکی از آنها MultiFileLibrary.dll ، FUT.netmodule و RUT.netmodule است و در استفاده از این ابزار هیچ راهی برای داشتن یک تک فایل وجود ندارد.
این ابزار همچنین میتواند فایلهای CUI ,GUI و ... را با سوئیچهای زیر هم تولید کند:
البته اینکار تا حدی غیر معمول است که یک فایل exe بخواهد کدهای IL ابتدایی را از ماژولهای جداگانه بخواند. در صورتیکه چنین قصدی را دارید، باید یکی از ماژولها را به عنوان مدخل ورودی Main تعریف کنید تا برنامه از آنجا آغاز به کار کند. نحوهی ساخت یک فایل اجرایی و معرفی ماژول Main به شکل زیر است:
در اولین خط مانند سابق فایل netmodule تهیه میگردد و در خط دوم، داخل اسمبلی قرار میگیرد. ولی به علت استفاده از سوئیچ main یک تابع عمومی global به نام EntryPoint__ هم تعریف میگردد که کد IL آن به شرح زیر است:
کد بالا یک کد ساده است که میگوید داخل فایل Program.netmodule در نوع Program متدی وجود دارد به نام Main که محل آغازین برنامه است. البته این روش ایجاد فایلهای EXE، بدین شکل توصیه چندانی نمیشود و ذکر این مطلب فقط اطلاع از وجود چنین قابلیتی بود.
این ابزار همچنین میتواند فایلهای CUI ,GUI و ... را با سوئیچهای زیر هم تولید کند:
/t[arget]:exe, /t[arget]:winexe, or /t[arget]:appcontainerexe
البته اینکار تا حدی غیر معمول است که یک فایل exe بخواهد کدهای IL ابتدایی را از ماژولهای جداگانه بخواند. در صورتیکه چنین قصدی را دارید، باید یکی از ماژولها را به عنوان مدخل ورودی Main تعریف کنید تا برنامه از آنجا آغاز به کار کند. نحوهی ساخت یک فایل اجرایی و معرفی ماژول Main به شکل زیر است:
csc /t:module /r:MultiFileLibrary.dll Program.cs al /out:Program.exe /t:exe /main:Program.Main Program.netmodule
.method privatescope static void __EntryPoint$PST06000001() cil managed { .entrypoint // Code size 8 (0x8) .maxstack 8 IL_0000: tail. IL_0002: call void [.module 'Program.netmodule']Program::Main() IL_0007: ret } // end of method 'Global Functions'::__EntryPoint
کد بالا یک کد ساده است که میگوید داخل فایل Program.netmodule در نوع Program متدی وجود دارد به نام Main که محل آغازین برنامه است. البته این روش ایجاد فایلهای EXE، بدین شکل توصیه چندانی نمیشود و ذکر این مطلب فقط اطلاع از وجود چنین قابلیتی بود.
نظرات مطالب
قابلیت Attribute Routing در ASP.NET MVC 5
مطابق مقالهای که ابتدای بحث لینک داده شده، امکان ترکیب هر دو حالت attribute routing و convention-based routing با هم وجود دارد و اگر در حالت قدیمی یعنی convention-based routing، شما مثل قبل یک default route تعریف کرده باشید، دیگر نیازی به ذکر و تکرار default route هم نام یک کنترلر در حالت attribute routing نخواهید داشت و فقط جایی که واقعا نیاز است باید از آن استفاده کرد. attribute routing کار رو برای تعریف قیدها یا constraints خیلی ساده و طبیعی کرده.
در این مقاله در ادامهی مطلبی که تحت عنوان «آموزش مفاهیم Data Warehouse» توسط آقای شاه قلی منتشر شده بود، به بررسی بیشتر مفهوم انبار داده ( Data Warehouse ) پرداخته میشود.
مقدمه
در سازمان ها، دادهها و اطلاعات معمولاً به دو شکل در سیستمها پیاده سازی میگردد:
این نکته حائز اهمیت است که سیستمهای اطلاعاتی یک سیستم یا محصول نیستند که بتوان آنها را خریداری کرد. بلکه یک راهبرد (Solution, Approach) هستند و در حقیقت هر راهبردی مربوط به یک نوع کسب و کار (Business) و یا سازمان میباشد و نمیتوان فرمول واحدی را برای حتی سازمانهای مشابه، ارائه نمود.
گارتنر در ابتدای سال 2011 گزارشی را منتشر کرده که نشان میدهد بازار BI با 9.7 % رشد، ارزشی بالغ بر 10.8 بیلیون دلار داشته، ولی متاسفانه پروژههای آن به طور متوسط با 75% شکست مواجه شده است. در حالیکه 4 سال پیش، این رقم حدود 50% بود. این موسسه BI را پنجمین اولویت مدیران IT ذکر کرده است.
مفاهیم و مباحث مربوط به Data Warehouse به اواسط دهه 1980 برمی گردد، به زمانی که IBM تحقیقاتی را در این زمینه شروع کرد و نتیجه آنرا «Information Warehouse» نامید و هنوز هم در برخی منابع از این واژه بجای Data Warehouse استفاده میشود. از این پس برای راحتی از اختصار DW بجای Data Warehouse استفاده میشود. انبارهای داده جهت رفع نیاز رو به رشد مدیریت دادهها و اطلاعات سازمانی که توسط پایگاههای داده سیستمهای عملیاتی غیر ممکن بود، ساخته شدند.
انبار داده به مجموعه ای از دادهها گفته میشود که از منابع مختلف اطلاعاتی سازمان جمع آوری، دسته بندی و ذخیره میشود. در واقع یک انبار داده مخزن اصلی کلیه دادههای حال و گذشته یک سازمان میباشد که برای همیشه جهت انجام عملیات گزارش گیری و آنالیز در دسترس مدیران میباشد. انبارههای داده حاوی داده هایی هستند که به مرور زمان از سیستمهای عملیاتی آنلاین سازمان، استخراج میشوند. بنابراین سوابق کلیه اطلاعات و یا بخش عظیمی از آنها را میتوان در انباره دادهها مشاهده نمود.
از آنجائیکه انجام عملیات آماری و گزارشات پیچیده دارای بار کاری بسیار سنگینی برای سرورهای پایگاه داده میباشند، وجود انبار داده سبب میگردد که این گونه عملیات تاثیری بر فعالیت برنامههای کاربردی سازمان نداشته باشد.
همانگونه که پایگاه داده سیستمهای عملیاتی سازمان (برنامههای کاربردی) به گونه ای طراحی میشوند که انجام تغییر، حذف و اضافه داده به سرعت صورت پذیرد، در مقابل انبار دادهها دارای معماری ویژه ای میباشند که موجب تسریع انجام عملیات آماری و گزارش گیری میشود. در حقیقت میتوان اینگونه بیان نمود که انباره داده یک مخزن فعال و هوشمند از اطلاعات است که قادر است اطلاعات را از محیطهای گوناگون جمع آوری و مدیریت کرده و نهایتا پخش نماید و در صورت لزوم نیز سیاستهای تجاری را روی آنها اجرا نماید.
Bill Inmon:
او را پدر DW مینامند، از دیدگاه او DW هسته مرکزی چیزی است که او آنرا CIF اختصار (Corporate Information Factory) مینامد، که پایه و اساس BI بر مبنای آن قرار دارد. وی از طرفداران Top-Down Design میباشد که معتقد است در زمان طراحی باید با دیدی سازمانی، CIF را مدل سازی، ولی بصورت دپارتمانی پیاده سازی کرد (Think Globally, Implement Locally). در این نوع طراحی از DW به Data Mart خواهیم رسید.
Ralph Kimball Ph.D:
به نظر وی DW چیزی نیست جز یک کپی از دادههای عملیاتی که به طرز خاصی برای گزارشات و تحلیلهای آماری، آماده و ساختمند شده است. به بیان دیگر DW سیستمی است جهت استخراج، پالایش، تطبیق و تحویل اطلاعات منابع داده ای به یک بانک اطلاعاتی Dimensional و اجرای Query و گزارشات آماری و تحلیلی برای اهداف تصمیم گیری و استراتژیک سازمان.
وی معرفی کننده یکی از اساسیترین مفاهیم طراحی یعنی Dimensional Modeling است؛ ماحصل چنین ایده ای، اساس شکل گیری مدلی است که امروزه کارشناسان آنرا به نام Cube میشناسند. وی از طرفداران Bottom-Up Design است که در این نگرش از Data Mart به DW میرسیم. این روش به نظر عملیتر از روشی میباشد که به یکباره DW جامع و کامل برای اهداف سازمانی طراحی و پیاده سازی گردد.
تعریف انبار داده:
W.H.Inmon پدر DW آنرا چنین تعریف میکند:
مقدمه
در سازمان ها، دادهها و اطلاعات معمولاً به دو شکل در سیستمها پیاده سازی میگردد:
• سیستمهای عملیاتی OLTP:
این سیستمها باعث میگردند تا چرخ کسب و کار بگردد. وجود این سیستمها سبب میشود تا دادههای مربوط به کسب و کار، به بانک اطلاعاتی وارد شوند. این سیستمها عموماً:
o به دلیل کوتاهی عملیات دارای سرعت قابل توجهی میباشند.
o محیطی جهت ورود دادهها میباشند.
o معمولاً اپراتورها، استفاده کنندههای آن هستند.
• سیستمهای اطلاعاتی OLAP ، DW/BI، DSS :
این سیستمها باعث میگردند تا چرخش کسب و کار را بنگرید. فلسفه بکارگیری این سیستمها در سازمان این است که اطلاعات مورد نیاز مدیران، از درون دادههای سیستمهای عملیاتی موجود، استخراج گردد. این سیستمها عموماً:
سیستمهای عملیاتی در جامعه ما سابقه بیشتری داشته و متخصصین فناوری اطلاعات عموماً با طراحی و تولید چنین سیستم هایی آشنایی کافی دارند. متاسفانه جایگاه سیستمهای اطلاعاتی در جامعه ما کمتر شناخته شده و متخصصین فناوری اطلاعات بندرت با مفاهیم و نحوه پیاده سازی آن آشنایی دارند.o به دلیل آنالیز حجم انبوهی از داده ها، معمولاً کندتر از سیستمهای عملیاتی میباشند.
o محیطی جهت تولید گزارشات تحلیلی و آماری میباشند.
o معمولاً مدیران و تصمیم گیرندگان سازمان ها، استفاده کنندگان آن میباشند.
این نکته حائز اهمیت است که سیستمهای اطلاعاتی یک سیستم یا محصول نیستند که بتوان آنها را خریداری کرد. بلکه یک راهبرد (Solution, Approach) هستند و در حقیقت هر راهبردی مربوط به یک نوع کسب و کار (Business) و یا سازمان میباشد و نمیتوان فرمول واحدی را برای حتی سازمانهای مشابه، ارائه نمود.
گارتنر در ابتدای سال 2011 گزارشی را منتشر کرده که نشان میدهد بازار BI با 9.7 % رشد، ارزشی بالغ بر 10.8 بیلیون دلار داشته، ولی متاسفانه پروژههای آن به طور متوسط با 75% شکست مواجه شده است. در حالیکه 4 سال پیش، این رقم حدود 50% بود. این موسسه BI را پنجمین اولویت مدیران IT ذکر کرده است.
مفاهیم و مباحث مربوط به Data Warehouse به اواسط دهه 1980 برمی گردد، به زمانی که IBM تحقیقاتی را در این زمینه شروع کرد و نتیجه آنرا «Information Warehouse» نامید و هنوز هم در برخی منابع از این واژه بجای Data Warehouse استفاده میشود. از این پس برای راحتی از اختصار DW بجای Data Warehouse استفاده میشود. انبارهای داده جهت رفع نیاز رو به رشد مدیریت دادهها و اطلاعات سازمانی که توسط پایگاههای داده سیستمهای عملیاتی غیر ممکن بود، ساخته شدند.
انبار داده به مجموعه ای از دادهها گفته میشود که از منابع مختلف اطلاعاتی سازمان جمع آوری، دسته بندی و ذخیره میشود. در واقع یک انبار داده مخزن اصلی کلیه دادههای حال و گذشته یک سازمان میباشد که برای همیشه جهت انجام عملیات گزارش گیری و آنالیز در دسترس مدیران میباشد. انبارههای داده حاوی داده هایی هستند که به مرور زمان از سیستمهای عملیاتی آنلاین سازمان، استخراج میشوند. بنابراین سوابق کلیه اطلاعات و یا بخش عظیمی از آنها را میتوان در انباره دادهها مشاهده نمود.
از آنجائیکه انجام عملیات آماری و گزارشات پیچیده دارای بار کاری بسیار سنگینی برای سرورهای پایگاه داده میباشند، وجود انبار داده سبب میگردد که این گونه عملیات تاثیری بر فعالیت برنامههای کاربردی سازمان نداشته باشد.
همانگونه که پایگاه داده سیستمهای عملیاتی سازمان (برنامههای کاربردی) به گونه ای طراحی میشوند که انجام تغییر، حذف و اضافه داده به سرعت صورت پذیرد، در مقابل انبار دادهها دارای معماری ویژه ای میباشند که موجب تسریع انجام عملیات آماری و گزارش گیری میشود. در حقیقت میتوان اینگونه بیان نمود که انباره داده یک مخزن فعال و هوشمند از اطلاعات است که قادر است اطلاعات را از محیطهای گوناگون جمع آوری و مدیریت کرده و نهایتا پخش نماید و در صورت لزوم نیز سیاستهای تجاری را روی آنها اجرا نماید.
Bill Inmon:
او را پدر DW مینامند، از دیدگاه او DW هسته مرکزی چیزی است که او آنرا CIF اختصار (Corporate Information Factory) مینامد، که پایه و اساس BI بر مبنای آن قرار دارد. وی از طرفداران Top-Down Design میباشد که معتقد است در زمان طراحی باید با دیدی سازمانی، CIF را مدل سازی، ولی بصورت دپارتمانی پیاده سازی کرد (Think Globally, Implement Locally). در این نوع طراحی از DW به Data Mart خواهیم رسید.
Ralph Kimball Ph.D:
به نظر وی DW چیزی نیست جز یک کپی از دادههای عملیاتی که به طرز خاصی برای گزارشات و تحلیلهای آماری، آماده و ساختمند شده است. به بیان دیگر DW سیستمی است جهت استخراج، پالایش، تطبیق و تحویل اطلاعات منابع داده ای به یک بانک اطلاعاتی Dimensional و اجرای Query و گزارشات آماری و تحلیلی برای اهداف تصمیم گیری و استراتژیک سازمان.
وی معرفی کننده یکی از اساسیترین مفاهیم طراحی یعنی Dimensional Modeling است؛ ماحصل چنین ایده ای، اساس شکل گیری مدلی است که امروزه کارشناسان آنرا به نام Cube میشناسند. وی از طرفداران Bottom-Up Design است که در این نگرش از Data Mart به DW میرسیم. این روش به نظر عملیتر از روشی میباشد که به یکباره DW جامع و کامل برای اهداف سازمانی طراحی و پیاده سازی گردد.
تعریف انبار داده:
W.H.Inmon پدر DW آنرا چنین تعریف میکند:
The Data Warehouse is a collection of Integrated, Subject-Oriented databases designed to support the DSS function, where each unit of data is Non-Volatile and relevant to some moment in Time
از تعریف فوق دو مورد دیگر نیز به طور ضمنی استنباط میشود:
به بیان دیگر DW راهبردی است که دسترسی آسان به اطلاعات درست (Right Information)، در زمانی درست (Right Time) ، به کاربران درست (Right Users)، را فراهم میآورد تا «تصمیم گیری سازمانی» قابل انجام باشد. DW صرفاً یک محصول نرم افزاری و یا سخت افزاری نیست که بتوان آنرا خریداری نمود بلکه فراتر از آن و در حقیقت یک محیط پردازشی میباشد که کاربران میتوانند از درون آن اطلاعات مورد نیاز خود را بیابند.
DW اطلاعات خود را از سایر بانکهای اطلاعاتی از نوع OLTP و یا سایر DWهای لایه پایینتر و به صورت دسته ای (Batch) و یا انبوه (Bulk Loading) جمع آوری میکند. یک DW به صورت سنتی باید شامل دادههای Historic سازمان باشد و میتوان اینگونه بیان نمود که در DW هرچه دادههای قدیمیتری موجود باشد، اعتبار تحلیلهای آماری سیستم افزایش خواهد یافت.
دادههای سیستم عملیاتی را نمیتوان بلافاصله درون بانک اطلاعاتی DW لود نمود، چنین داده هایی باید آماده سازی، پالایش و همگون گردند تا شرایط لود در DW را داشته باشند. حداقل کاری که انتظار داریم یک DW در مورد دادهها برای ما برآورده سازد شامل موارد زیر است:
ویژگیهای دادههای درون DW
دادههای DW از نگاه Inmon دارای 4 ویژگی اصلی زیر هستند:
o انبار داده به طور فیزیکی، کاملاً جدا از سایر سیستمهای عملیاتی است.
o دادههای DW مجموعه ای Aggregated و Atomic از دادههای تراکنشهای سیستمهای عملیاتی است که سوای کاربرد آنها در سیستمهای عملیاتی، برای مقاصد مدیریتی نیز استفاده خواهد شد.
به بیان دیگر DW راهبردی است که دسترسی آسان به اطلاعات درست (Right Information)، در زمانی درست (Right Time) ، به کاربران درست (Right Users)، را فراهم میآورد تا «تصمیم گیری سازمانی» قابل انجام باشد. DW صرفاً یک محصول نرم افزاری و یا سخت افزاری نیست که بتوان آنرا خریداری نمود بلکه فراتر از آن و در حقیقت یک محیط پردازشی میباشد که کاربران میتوانند از درون آن اطلاعات مورد نیاز خود را بیابند.
DW اطلاعات خود را از سایر بانکهای اطلاعاتی از نوع OLTP و یا سایر DWهای لایه پایینتر و به صورت دسته ای (Batch) و یا انبوه (Bulk Loading) جمع آوری میکند. یک DW به صورت سنتی باید شامل دادههای Historic سازمان باشد و میتوان اینگونه بیان نمود که در DW هرچه دادههای قدیمیتری موجود باشد، اعتبار تحلیلهای آماری سیستم افزایش خواهد یافت.
دادههای سیستم عملیاتی را نمیتوان بلافاصله درون بانک اطلاعاتی DW لود نمود، چنین داده هایی باید آماده سازی، پالایش و همگون گردند تا شرایط لود در DW را داشته باشند. حداقل کاری که انتظار داریم یک DW در مورد دادهها برای ما برآورده سازد شامل موارد زیر است:
o استخراج دادهها از منابع مختلف (مبدإ)با هر با اجرای پروسه فوق یکی از سه مورد زیر، بسته به نیاز طراحی و محدودیتهای تکنولوژی رخ خواهد داد:
o تبدیل دادهها به فرمتی یکسان
o لود دادهها به جداول مربوطه (مقصد)
o تمام دادهها در DW با دادههای جدید جایگزین خواهند گردید(Full Load, Initial Load, Full Refresh).
o دادههای جدید به دادههای موجود اضافه خواهند گردید (Incremental Load (Inserted data.
o نسخه جدیدی از دادههای کنونی به سیستم اضافه خواهند گردید (Incremental Load (Updated data.
ویژگیهای دادههای درون DW
دادههای DW از نگاه Inmon دارای 4 ویژگی اصلی زیر هستند:
o فقط خواندنی (Non-Volatile):
هیچ رکوردی و یا داده ای Update نخواهد شد و صرفاً رکوردهایی که محتوای مقادیر جدید دادهها هستند، به سیستم اضافه خواهند شد.
o موضوع گرا (Subject-Oriented):
منظور از «موضوع» پایههای اساسی یک کسب و کار هستند، به شکلی که با حذف یکی از این پایه ها، شاید ماهیت آن کسب و کار از ریشه دگرگون شود. برای مثال موضوعاتی چون «مشتری» و یا «بیمه نامه» برای شرکتهای بیمه.
o جامع (Integrated):
باید تمامی کدهایی که در سیستمهای عملیاتی وجود دارند و معانی یکسانی دارند، برای مثال کد جنسیت، در DW به یک روش ذخیره و نمایش داده شوند.
o زمانگرا (Time Variant):
هر رکورد باید حاوی فیلد و یا کلیدی باشد که نمایانگر این باشد که این رکورد در چه زمانی ایجاد، استخراج و ذخیره شده است. از آنجا که دادههای درون سیستمهای عملیاتی آخرین و به روزترین داده هر سیستم میباشد، نیازی به وجود چنین عنصری در سیستمهای OLTP احساس نمیگردد، ولی چون در DW تمام دادههای نسخ قدیمی دادههای سیستمهای عملیاتی موجود میباشد، باید حتماً مشخص گردد که هر داده ای در سیستمهای عملیاتی در چه زمانی، چه مقادیری داشته است. این عنصر زمانی کمک میکند تا بتوانیم:
منبع: کتاب آقای خشایار جام سحر با عنوان بانک داده تجمیعیo گذشته را آنالیز کنیم.
o اطلاعات مربوط به حال حاضر را بدست آوریم.
o آینده را پیش بینی کنیم.
Comparison Kimball vs. Inmon
Inmon
Continuous & Discrete Dimension Management
Define data management via dates in your data
Continuous time
Kimball
Slowly Changing Dimension Management
Define data management via versioning
Type I
Inmon
Continuous & Discrete Dimension Management
Define data management via dates in your data
Continuous time
When is a record activeDiscrete time
Start and end dates
A point in time
Snapshot
Kimball
Slowly Changing Dimension Management
Define data management via versioning
Type I
Change record as requiredType II
No History
Manage all changesType III
History is recorded
Some history is parallel
Limit to defined history
Kimball | Inmon |
Business-Process-Oriented Stresses Dimensional Model, Not E-R | Subject-Oriented Integrated Non-Volatile Time-Variant |
Bottom-Up and Evolutionary | Top-Down |
Integration Achieved via Conformed Dimensions | Integration Achieved via an Assumed Enterprise Data Model |
Star Schemas Enforce Query Semantics | Characterizes Data marts as Aggregates |
Kimball | Inmon | |
Bottom-up | Top-down | Overall approach |
Data marts model a business process;enterprise is achieved with conformed dims | Enterprise-wide DW feeds departmental DBs | Architectural structure |
Fairly simple | Quite complex | Complexity of method |
Process oriented | Subject or data driven | Data orientation |
Dimensional modeling; departs from traditional relational modeling | Traditional ERDs and DIS | Tools |
High | Low | End user accessibility |
Slowly Changing | Continuous & Discrete | Timeframe |
Dimension keys | Timestamps | Methods |
ASP.NET Core Identity به همراه دو قابلیت جدید است که پیاده سازی سطوح دسترسی پویا را با سهولت بیشتری میسر میکند:
الف) Policies
ب) Role Claims
سیاستهای دسترسی یا Policies در ASP.NET Core Identity
ASP.NET Core Identity هنوز هم از مفهوم Roles پشتیبانی میکند. برای مثال میتوان مشخص کرد که اکشن متدی و یا تمام اکشن متدهای یک کنترلر تنها توسط کاربران دارای نقش Admin قابل دسترسی باشند. اما نقشها نیز در این سیستم جدید تنها نوعی از سیاستهای دسترسی هستند.
برای مثال در اینجا دسترسی به امکانات مدیریت نقشهای سیستم، به نقش ثابت و از پیش تعیین شدهی Admin منحصر شدهاست و تمام کاربرانی که این نقش به آنها انتساب داده شود، امکان استفادهی از این قابلیتها را خواهند یافت.
اما نقشهای ثابت، بسیار محدود و غیر قابل انعطاف هستند. برای رفع این مشکل مفهوم جدیدی را به نام Policy اضافه کردهاند.
سیاستهای دسترسی بر اساس Requirements و یا نیازهای سیستم تعیین میشوند و تعیین نقشها، تنها یکی از قابلیتهای آنها هستند.
برای مثال اگر بخواهیم تک نقش Admin را به صورت یک سیاست دسترسی جدید تعریف کنیم، روش کار به صورت ذیل خواهد بود:
در تنظیمات متد AddAuthorization، یک سیاست دسترسی جدید تعریف شدهاست که جهت برآورده شدن نیازمندیهای آن، کاربر سیستم باید دارای نقش Admin باشد که نمونهای از نحوهی استفادهی از آنرا با ذکر [Authorize(Policy="RequireAdministratorRole")] ملاحظه کردید.
و یا بجای اینکه چند نقش مجاز به دسترسی منبعی را با کاما از هم جدا کنیم:
میتوان یک سیاست دسترسی جدید را به نحو ذیل تعریف کرد که شامل تمام نقشهای مورد نیاز باشد و سپس بجای ذکر Roles، از نام این Policy جدید استفاده کرد:
به این صورت
سیاستهای دسترسی تنها به نقشها محدود نیستند:
برای مثال میتوان دسترسی به یک منبع را بر اساس User Claims یک کاربر به نحوی که ملاحظه میکنید، محدود کرد:
سیاستهای دسترسی پویا در ASP.NET Core Identity
مهمترین مزیت کار با سیاستهای دسترسی، امکان سفارشی سازی و تهیهی نمونههای پویای آنها هستند؛ موردی که با نقشهای ثابت سیستم قابل پیاده سازی نبوده و در نگارشهای قبلی، جهت پویا سازی آن، یکی از روشهای بسیار متداول، تهیهی فیلتر Authorize سفارشی سازی شده بود. اما در اینجا دیگر نیازی نیست تا فیلتر Authorize را سفارشی سازی کنیم. با پیاده سازی یک AuthorizationHandler جدید و معرفی آن به سیستم، پردازش سیاستهای دسترسی پویای به منابع، فعال میشود.
پیاده سازی سیاستهای پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
ابتدا باید یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی مانند روشی که در پروژهی DNT Identity بکار گرفته شدهاست، خالی است و صرفا به عنوان نشانهای جهت یافت AuthorizationHandler استفاده کنندهی از آن استفاده میشود. در اینجا در صورت نیاز میتوان یک سری خاصیت اضافه را تعریف کرد تا آنها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.
2- پیاده سازی یک AuthorizationHandler استفاده کنندهی از نیازمندی دسترسی تعریف شده
پس از اینکه نیازمندی DynamicPermissionRequirement را تعریف کردیم، در ادامه باید یک AuthorizationHandler استفاده کنندهی از آن را تعریف کنیم:
کار با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن، توسط ASP.NET Core Identity استفاده میشود. بنابراین در اینجا DynamicPermissionRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آنرا بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری میتواند به منبع درخواستی، بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت میکند.
منطق سفارشی پیاده سازی شده نیز به این صورت است:
نام ناحیه، کنترلر و اکشن متد درخواستی کاربر از مسیریابی جاری استخراج میشوند. سپس توسط سرویس سفارشی ISecurityTrimmingService تهیه شده، بررسی میکنیم که آیا کاربر جاری به این سه مؤلفه دسترسی دارد یا خیر؟
3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحلهی زیر است:
ابتدا باید DynamicPermissionsAuthorizationHandler تهیه شده را به سیستم تزریق وابستگیها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه DynamicPermission تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه میکنیم. همانطور که ملاحظه میکنید یک وهلهی جدید از DynamicPermissionRequirement در اینجا ثبت شدهاست. همین وهله به متد HandleRequirementAsync نیز ارسال میشود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، میتوان خواص مرتبطی را به کلاس DynamicPermissionRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را میتوان ثبت کرد و نه Handler آنرا. این Handler از سیستم تزریق وابستگیها، بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا میشود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آنرا به سیستم تزریق وابستگیها معرفی کردهاید).
پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
به صورت خودکار توسط DynamicPermissionsAuthorizationHandler مدیریت میشود.
سرویس ISecurityTrimmingService چگونه کار میکند؟
کدهای کامل ISecurityTrimmingService را در کلاس SecurityTrimmingService میتوانید مشاهده کنید.
پیشنیاز درک عملکرد آن، آشنایی با دو قابلیت زیر هستند:
الف) «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامهی ASP.NET Core»
دقیقا از همین سرویس توسعه داده شدهی در مطلب فوق، در اینجا نیز استفاده شدهاست؛ با یک تفاوت تکمیلی:
از متد GetAllSecuredControllerActionsWithPolicy جهت یافتن تمام اکشن متدهایی که مزین به ویژگی Authorize هستند و دارای Policy مساوی DynamicPermission میباشند، در کنترلر DynamicRoleClaimsManagerController برای لیست کردن آنها استفاده میشود. اگر این اکشن متد مزین به ویژگی DisplayName نیز بود (مانند مثال فوق و یا کنترلر نمونه DynamicPermissionsSampleController)، از مقدار آن برای نمایش نام این اکشن متد استفاده خواهد شد.
بنابراین همینقدر که تعریف ذیل یافت شود، این اکشن متد نیز در صفحهی مدیریت سطوح دسترسی پویا لیست خواهد شد.
به این ترتیب میتوان به یک نقش، تعدادی اکشن متد را نسبت داد و سطوح دسترسی به آنها را پویا کرد. اما ذخیره سازی آنها چگونه است و چگونه میتوان به اطلاعات نهایی ذخیره شده دسترسی پیدا کرد؟
مفهوم جدید Role Claims در ASP.NET Core Identity
تا اینجا موفق شدیم تمام اکشن متدهای دارای سیاست دسترسی سفارشی سازی شدهی خود را لیست کنیم، تا بتوان آنها را به صورت دلخواهی انتخاب کرد و سطوح دسترسی به آنها را به صورت پویا تغییر داد. اما این اکشن متدهای انتخاب شده را در کجا و به چه صورتی ذخیره کنیم؟
برای ذخیره سازی این اطلاعات نیازی نیست تا جدول جدیدی را به سیستم اضافه کنیم. جدول جدید AppRoleClaims به همین منظور تدارک دیده شدهاست.
وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث میبرد. هدف از نقشها، گروه بندی کاربران است. توسط Role Claims میتوان مشخص کرد این نقشها چه کارهایی را میتوانند انجام دهند. اگر از قسمت قبل بخاطر داشته باشید، سرویس توکار UserClaimsPrincipalFactory دارای مرحلهی 5 ذیل است:
«5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه میشوند. در ASP.NET Identity Core نقشها نیز میتوانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).»
به این معنا که با لاگین شخص به سیستم، تمام اطلاعات مرتبط به او که در جدول AppRoleClaims وجود دارند، به کوکی او به صورت خودکار اضافه خواهند شد و دسترسی به آنها فوق العاده سریع است.
در کنترلر DynamicRoleClaimsManagerController، یک Role Claim Type جدید به نام DynamicPermissionClaimType اضافه شدهاست و سپس ID اکشن متدهای انتخابی را به نقش جاری، تحت Claim Type عنوان شده، اضافه میکند (تصویر فوق). این ID به صورت area:controller:action طراحی شدهاست. به همین جهت است که در DynamicPermissionsAuthorizationHandler همین سه جزء از سیستم مسیریابی استخراج و در سرویس SecurityTrimmingService مورد بررسی قرار میگیرد:
در اینجا user همان کاربرجاری سیستم است. HasClaim جزو متدهای استاندارد آن است و Type انتخابی، همان نوع سفارشی مدنظر ما است. currentClaimValue دقیقا همان ID اکشن متد جاری است که توسط کنار هم قرار دادن area:controller:action تشکیل شدهاست.
متد HasClaim هیچگونه رفت و برگشتی را به بانک اطلاعاتی ندارد و اطلاعات خود را از کوکی شخص دریافت میکند. متد user.IsInRole نیز به همین نحو عمل میکند.
Tag Helper جدید SecurityTrimming
اکنون که سرویس ISecurityTrimmingService را پیاده سازی کردهایم، از آن میتوان جهت توسعهی SecurityTrimmingTagHelper نیز استفاده کرد:
عملکرد آن نیز بسیار ساده است. اگر کاربر، به area:controller:action جاری دسترسی داشت، این Tag Helper کاری را انجام نمیدهد. اگر خیر، متد SuppressOutput را فراخوانی میکند. این متد سبب خواهد شد، هر آنچه که داخل تگهای این TagHelper قرار گرفته، در صفحه رندر نشوند و از خروجی آن حذف شوند. به این ترتیب، کاربر به اطلاعاتی که به آن دسترسی ندارد (مانند لینک به مدخلی خاص را) مشاهده نخواهد کرد. به این مفهوم security trimming میگویند.
نمونهای از کاربرد آنرا در ReportsMenu.cshtml_ میتوانید مشاهده کنید:
در اینجا اگر کاربر جاری به کنترلر DynamicPermissionsTest و اکشن متد Products آن دسترسی پویا نداشته باشد، محتوای قرارگرفتهی داخل تگ security-trimming را مشاهده نخواهد کرد.
برای آزمایش آن یک کاربر جدید را به سیستم DNT Identity اضافه کنید. سپس آنرا در گروه نقشی مشخص قرار دهید (منوی مدیریتی،گزینهی مدیریت نقشهای سیستم). سپس به این گروه دسترسی به تعدادی از آیتمهای پویا را بدهید (گزینهی مشاهده و تغییر لیست دسترسیهای پویا). سپس با این اکانت جدید به سیستم وارد شده و بررسی کنید که چه تعدادی از آیتمهای منوی «گزارشات نمونه» را میتوانید مشاهده کنید (تامین شدهی توسط ReportsMenu.cshtml_).
مدیریت اندازهی حجم کوکیهای ASP.NET Core Identity
همانطور که ملاحظه کردید، جهت بالابردن سرعت دسترسی به اطلاعات User Claims و Role Claims، تمام اطلاعات مرتبط با آنها، به کوکی کاربر وارد شدهی به سیستم، اضافه میشوند. همین مساله در یک سیستم بزرگ با تعداد صفحات بالا، سبب خواهد شد تا حجم کوکی کاربر از 5 کیلوبایت بیشتر شده و توسط مرورگرها مورد قبول واقع نشوند و عملا سیستم از کار خواهد افتاد.
برای مدیریت یک چنین مسالهای، امکان ذخیره سازی کوکیهای شخص در داخل بانک اطلاعاتی نیز پیش بینی شدهاست. زیر ساخت آنرا در مطلب «تنظیمات کش توزیع شدهی مبتنی بر SQL Server در ASP.NET Core» پیشتر در این سایت مطالعه کردید و در پروژهی DNT Identity بکارگرفته شدهاست.
اگر به کلاس IdentityServicesRegistry مراجعه کنید، یک چنین تنظیمی در آن قابل مشاهده است:
در ASP.NET Identity Core، امکان تدارک SessionStore سفارشی برای کوکیها نیز وجود دارد. این SessionStore باید با پیاده سازی اینترفیس ITicketStore تامین شود. دو نمونه پیاده سازی ITicketStore را در لایه سرویسهای پروژه میتوانید مشاهده کنید:
الف) DistributedCacheTicketStore
ب) MemoryCacheTicketStore
اولی از همان زیرساخت «تنظیمات کش توزیع شدهی مبتنی بر SQL Server در ASP.NET Core» استفاده میکند و دومی از IMemoryCache توکار ASP.NET Core برای پیاده سازی مکان ذخیره سازی محتوای کوکیهای سیستم، بهره خواهد برد.
باید دقت داشت که اگر حالت دوم را انتخاب کنید، با شروع مجدد برنامه، تمام اطلاعات کوکیهای کاربران نیز حذف خواهند شد. بنابراین استفادهی از حالت ذخیره سازی آنها در بانک اطلاعاتی منطقیتر است.
نحوهی تنظیم سرویس ITicketStore را نیز در متد setTicketStore میتوانید مشاهده کنید و در آن، در صورت انتخاب حالت بانک اطلاعاتی، ابتدا تنظیمات کش توزیع شده، صورت گرفته و سپس کلاس DistributedCacheTicketStore به عنوان تامین کنندهی ITicketStore به سیستم تزریق وابستگیها معرفی میشود.
همین اندازه برای انتقال محتوای کوکیهای کاربران به سرور کافی است و از این پس تنها اطلاعاتی که به سمت کلاینت ارسال میشود، ID رمزنگاری شدهی این کوکی است، جهت بازیابی آن از بانک اطلاعاتی و استفادهی خودکار از آن در برنامه.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
الف) Policies
ب) Role Claims
سیاستهای دسترسی یا Policies در ASP.NET Core Identity
ASP.NET Core Identity هنوز هم از مفهوم Roles پشتیبانی میکند. برای مثال میتوان مشخص کرد که اکشن متدی و یا تمام اکشن متدهای یک کنترلر تنها توسط کاربران دارای نقش Admin قابل دسترسی باشند. اما نقشها نیز در این سیستم جدید تنها نوعی از سیاستهای دسترسی هستند.
[Authorize(Roles = ConstantRoles.Admin)] public class RolesManagerController : Controller
اما نقشهای ثابت، بسیار محدود و غیر قابل انعطاف هستند. برای رفع این مشکل مفهوم جدیدی را به نام Policy اضافه کردهاند.
[Authorize(Policy="RequireAdministratorRole")] public IActionResult Get() { /* .. */ }
برای مثال اگر بخواهیم تک نقش Admin را به صورت یک سیاست دسترسی جدید تعریف کنیم، روش کار به صورت ذیل خواهد بود:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthorization(options => { options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Admin")); }); }
و یا بجای اینکه چند نقش مجاز به دسترسی منبعی را با کاما از هم جدا کنیم:
[Authorize(Roles = "Administrator, PowerUser, BackupAdministrator")]
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
[Authorize(Policy = "ElevatedRights")] public IActionResult Shutdown() { return View(); }
سیاستهای دسترسی تنها به نقشها محدود نیستند:
services.AddAuthorization(options => { options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")); });
[Authorize(Policy = "EmployeeOnly")] public IActionResult VacationBalance() { return View(); }
سیاستهای دسترسی پویا در ASP.NET Core Identity
مهمترین مزیت کار با سیاستهای دسترسی، امکان سفارشی سازی و تهیهی نمونههای پویای آنها هستند؛ موردی که با نقشهای ثابت سیستم قابل پیاده سازی نبوده و در نگارشهای قبلی، جهت پویا سازی آن، یکی از روشهای بسیار متداول، تهیهی فیلتر Authorize سفارشی سازی شده بود. اما در اینجا دیگر نیازی نیست تا فیلتر Authorize را سفارشی سازی کنیم. با پیاده سازی یک AuthorizationHandler جدید و معرفی آن به سیستم، پردازش سیاستهای دسترسی پویای به منابع، فعال میشود.
پیاده سازی سیاستهای پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class DynamicPermissionRequirement : IAuthorizationRequirement { }
2- پیاده سازی یک AuthorizationHandler استفاده کنندهی از نیازمندی دسترسی تعریف شده
پس از اینکه نیازمندی DynamicPermissionRequirement را تعریف کردیم، در ادامه باید یک AuthorizationHandler استفاده کنندهی از آن را تعریف کنیم:
public class DynamicPermissionsAuthorizationHandler : AuthorizationHandler<DynamicPermissionRequirement> { private readonly ISecurityTrimmingService _securityTrimmingService; public DynamicPermissionsAuthorizationHandler(ISecurityTrimmingService securityTrimmingService) { _securityTrimmingService = securityTrimmingService; _securityTrimmingService.CheckArgumentIsNull(nameof(_securityTrimmingService)); } protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, DynamicPermissionRequirement requirement) { var mvcContext = context.Resource as AuthorizationFilterContext; if (mvcContext == null) { return Task.CompletedTask; } var actionDescriptor = mvcContext.ActionDescriptor; var area = actionDescriptor.RouteValues["area"]; var controller = actionDescriptor.RouteValues["controller"]; var action = actionDescriptor.RouteValues["action"]; if(_securityTrimmingService.CanCurrentUserAccess(area, controller, action)) { context.Succeed(requirement); } else { context.Fail(); } return Task.CompletedTask; } }
در کلاس تهیه شده باید متد HandleRequirementAsync آنرا بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری میتواند به منبع درخواستی، بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت میکند.
منطق سفارشی پیاده سازی شده نیز به این صورت است:
نام ناحیه، کنترلر و اکشن متد درخواستی کاربر از مسیریابی جاری استخراج میشوند. سپس توسط سرویس سفارشی ISecurityTrimmingService تهیه شده، بررسی میکنیم که آیا کاربر جاری به این سه مؤلفه دسترسی دارد یا خیر؟
3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحلهی زیر است:
private static void addDynamicPermissionsPolicy(this IServiceCollection services) { services.AddScoped<IAuthorizationHandler, DynamicPermissionsAuthorizationHandler>(); services.AddAuthorization(opts => { opts.AddPolicy( name: ConstantPolicies.DynamicPermission, configurePolicy: policy => { policy.RequireAuthenticatedUser(); policy.Requirements.Add(new DynamicPermissionRequirement()); }); }); }
سپس یک Policy جدید را با نام دلخواه DynamicPermission تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه میکنیم. همانطور که ملاحظه میکنید یک وهلهی جدید از DynamicPermissionRequirement در اینجا ثبت شدهاست. همین وهله به متد HandleRequirementAsync نیز ارسال میشود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، میتوان خواص مرتبطی را به کلاس DynamicPermissionRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را میتوان ثبت کرد و نه Handler آنرا. این Handler از سیستم تزریق وابستگیها، بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا میشود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آنرا به سیستم تزریق وابستگیها معرفی کردهاید).
پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy = ConstantPolicies.DynamicPermission)] [DisplayName("کنترلر نمونه با سطح دسترسی پویا")] public class DynamicPermissionsSampleController : Controller
سرویس ISecurityTrimmingService چگونه کار میکند؟
کدهای کامل ISecurityTrimmingService را در کلاس SecurityTrimmingService میتوانید مشاهده کنید.
پیشنیاز درک عملکرد آن، آشنایی با دو قابلیت زیر هستند:
الف) «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامهی ASP.NET Core»
دقیقا از همین سرویس توسعه داده شدهی در مطلب فوق، در اینجا نیز استفاده شدهاست؛ با یک تفاوت تکمیلی:
public interface IMvcActionsDiscoveryService { ICollection<MvcControllerViewModel> MvcControllers { get; } ICollection<MvcControllerViewModel> GetAllSecuredControllerActionsWithPolicy(string policyName); }
بنابراین همینقدر که تعریف ذیل یافت شود، این اکشن متد نیز در صفحهی مدیریت سطوح دسترسی پویا لیست خواهد شد.
[Authorize(Policy = ConstantPolicies.DynamicPermission)]
ابتدا به مدیریت نقشهای ثابت سیستم میرسیم. سپس به هر نقش میتوان یک Claim جدید را با مقدار area:controller:action انتساب داد.
به این ترتیب میتوان به یک نقش، تعدادی اکشن متد را نسبت داد و سطوح دسترسی به آنها را پویا کرد. اما ذخیره سازی آنها چگونه است و چگونه میتوان به اطلاعات نهایی ذخیره شده دسترسی پیدا کرد؟
مفهوم جدید Role Claims در ASP.NET Core Identity
تا اینجا موفق شدیم تمام اکشن متدهای دارای سیاست دسترسی سفارشی سازی شدهی خود را لیست کنیم، تا بتوان آنها را به صورت دلخواهی انتخاب کرد و سطوح دسترسی به آنها را به صورت پویا تغییر داد. اما این اکشن متدهای انتخاب شده را در کجا و به چه صورتی ذخیره کنیم؟
برای ذخیره سازی این اطلاعات نیازی نیست تا جدول جدیدی را به سیستم اضافه کنیم. جدول جدید AppRoleClaims به همین منظور تدارک دیده شدهاست.
وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث میبرد. هدف از نقشها، گروه بندی کاربران است. توسط Role Claims میتوان مشخص کرد این نقشها چه کارهایی را میتوانند انجام دهند. اگر از قسمت قبل بخاطر داشته باشید، سرویس توکار UserClaimsPrincipalFactory دارای مرحلهی 5 ذیل است:
«5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه میشوند. در ASP.NET Identity Core نقشها نیز میتوانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).»
به این معنا که با لاگین شخص به سیستم، تمام اطلاعات مرتبط به او که در جدول AppRoleClaims وجود دارند، به کوکی او به صورت خودکار اضافه خواهند شد و دسترسی به آنها فوق العاده سریع است.
در کنترلر DynamicRoleClaimsManagerController، یک Role Claim Type جدید به نام DynamicPermissionClaimType اضافه شدهاست و سپس ID اکشن متدهای انتخابی را به نقش جاری، تحت Claim Type عنوان شده، اضافه میکند (تصویر فوق). این ID به صورت area:controller:action طراحی شدهاست. به همین جهت است که در DynamicPermissionsAuthorizationHandler همین سه جزء از سیستم مسیریابی استخراج و در سرویس SecurityTrimmingService مورد بررسی قرار میگیرد:
return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType && claim.Value == currentClaimValue);
متد HasClaim هیچگونه رفت و برگشتی را به بانک اطلاعاتی ندارد و اطلاعات خود را از کوکی شخص دریافت میکند. متد user.IsInRole نیز به همین نحو عمل میکند.
Tag Helper جدید SecurityTrimming
اکنون که سرویس ISecurityTrimmingService را پیاده سازی کردهایم، از آن میتوان جهت توسعهی SecurityTrimmingTagHelper نیز استفاده کرد:
public override void Process(TagHelperContext context, TagHelperOutput output) { context.CheckArgumentIsNull(nameof(context)); output.CheckArgumentIsNull(nameof(output)); // don't render the <security-trimming> tag. output.TagName = null; if(_securityTrimmingService.CanCurrentUserAccess(Area, Controller, Action)) { // fine, do nothing. return; } // else, suppress the output and generate nothing. output.SuppressOutput(); }
نمونهای از کاربرد آنرا در ReportsMenu.cshtml_ میتوانید مشاهده کنید:
<security-trimming asp-area="" asp-controller="DynamicPermissionsTest" asp-action="Products"> <li> <a asp-controller="DynamicPermissionsTest" asp-action="Products" asp-area=""> <span class="left5 fa fa-user" aria-hidden="true"></span> گزارش از لیست محصولات </a> </li> </security-trimming>
برای آزمایش آن یک کاربر جدید را به سیستم DNT Identity اضافه کنید. سپس آنرا در گروه نقشی مشخص قرار دهید (منوی مدیریتی،گزینهی مدیریت نقشهای سیستم). سپس به این گروه دسترسی به تعدادی از آیتمهای پویا را بدهید (گزینهی مشاهده و تغییر لیست دسترسیهای پویا). سپس با این اکانت جدید به سیستم وارد شده و بررسی کنید که چه تعدادی از آیتمهای منوی «گزارشات نمونه» را میتوانید مشاهده کنید (تامین شدهی توسط ReportsMenu.cshtml_).
مدیریت اندازهی حجم کوکیهای ASP.NET Core Identity
همانطور که ملاحظه کردید، جهت بالابردن سرعت دسترسی به اطلاعات User Claims و Role Claims، تمام اطلاعات مرتبط با آنها، به کوکی کاربر وارد شدهی به سیستم، اضافه میشوند. همین مساله در یک سیستم بزرگ با تعداد صفحات بالا، سبب خواهد شد تا حجم کوکی کاربر از 5 کیلوبایت بیشتر شده و توسط مرورگرها مورد قبول واقع نشوند و عملا سیستم از کار خواهد افتاد.
برای مدیریت یک چنین مسالهای، امکان ذخیره سازی کوکیهای شخص در داخل بانک اطلاعاتی نیز پیش بینی شدهاست. زیر ساخت آنرا در مطلب «تنظیمات کش توزیع شدهی مبتنی بر SQL Server در ASP.NET Core» پیشتر در این سایت مطالعه کردید و در پروژهی DNT Identity بکارگرفته شدهاست.
اگر به کلاس IdentityServicesRegistry مراجعه کنید، یک چنین تنظیمی در آن قابل مشاهده است:
var ticketStore = provider.GetService<ITicketStore>(); identityOptionsCookies.ApplicationCookie.SessionStore = ticketStore; // To manage large identity cookies
الف) DistributedCacheTicketStore
ب) MemoryCacheTicketStore
اولی از همان زیرساخت «تنظیمات کش توزیع شدهی مبتنی بر SQL Server در ASP.NET Core» استفاده میکند و دومی از IMemoryCache توکار ASP.NET Core برای پیاده سازی مکان ذخیره سازی محتوای کوکیهای سیستم، بهره خواهد برد.
باید دقت داشت که اگر حالت دوم را انتخاب کنید، با شروع مجدد برنامه، تمام اطلاعات کوکیهای کاربران نیز حذف خواهند شد. بنابراین استفادهی از حالت ذخیره سازی آنها در بانک اطلاعاتی منطقیتر است.
نحوهی تنظیم سرویس ITicketStore را نیز در متد setTicketStore میتوانید مشاهده کنید و در آن، در صورت انتخاب حالت بانک اطلاعاتی، ابتدا تنظیمات کش توزیع شده، صورت گرفته و سپس کلاس DistributedCacheTicketStore به عنوان تامین کنندهی ITicketStore به سیستم تزریق وابستگیها معرفی میشود.
همین اندازه برای انتقال محتوای کوکیهای کاربران به سرور کافی است و از این پس تنها اطلاعاتی که به سمت کلاینت ارسال میشود، ID رمزنگاری شدهی این کوکی است، جهت بازیابی آن از بانک اطلاعاتی و استفادهی خودکار از آن در برنامه.
کدهای کامل این سری را در مخزن کد DNT Identity میتوانید ملاحظه کنید.
قسمت اول این بحث و همچنین پیشنیاز آنرا در اینجا و اینجا میتوانید مطالعه نمائید.
همهی اینها بسیار هم نیکو! اما ... آیا واقعا باید به ازای هر روال رویدادگردانی یک Attached property نوشت تا بتوان از آن در الگوی MVVM استفاده کرد؟ برای یکی دو مورد شاید اهمیتی نداشته باشد؛ اما کم کم با بزرگتر شدن برنامه نوشتن این Attached properties تبدیل به یک کار طاقت فرسا میشود و اشخاص را از الگوی MVVM فراری خواهد داد.
برای حل این مساله، تیم Expression Blend راه حلی را ارائه دادهاند به نام Interaction.Triggers که در ادامه به توضیح آن پرداخته خواهد شد.
ابتدا نیاز خواهید داشت تا SDK مرتبط با Expression Blend را دریافت کنید: (^)
سپس با فایل System.Windows.Interactivity.dll موجود در آن کار خواهیم داشت.
یک مثال عملی:
فرض کنید میخواهیم رویداد Loaded یک View را در ViewModel دریافت کنیم. زمان وهله سازی یک ViewModel با زمان وهله سازی View یکی است، اما بسته به تعداد عناصر رابط کاربری قرار گرفته در View ، زمان بارگذاری نهایی آن ممکن است متفاوت باشد به همین جهت رویداد Loaded برای آن درنظر گرفته شده است. خوب، ما الان در ViewModel نیاز داریم بدانیم که چه زمانی کار بارگذاری یک View به پایان رسیده.
یک راه حل آنرا در قسمت قبل مشاهده کردید؛ باید برای این کار یک Attached property جدید نوشت چون نمیتوان Command ایی را به رویداد Loaded انتساب داد یا Bind کرد. اما به کمک امکانات تعریف شده در System.Windows.Interactivity.dll به سادگی میتوان این رویداد را به یک Command استاندارد ترجمه کرد:
<Window x:Class="WpfEventTriggerSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:vm="clr-namespace:WpfEventTriggerSample.ViewModels"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding DoLoadCommand}"
CommandParameter="I am loaded!" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock Text="Testing InvokeCommandAction..."
Margin="5" VerticalAlignment="Top" />
</Grid>
</Window>
ابتدا ارجاعی به اسمبلی System.Windows.Interactivity.dll باید به پروژه اضافه شود. سپس فضای نام xmlns:i باید به فایل XAML جاری مطابق کدهای فوق اضافه گردد. در نهایت به کمک Interaction.Triggers آن، ابتدا نام رویداد مورد نظر را مشخص میکنیم (EventName) و سپس به کمک InvokeCommandAction، این رویداد به یک Command استاندارد ترجمه میشود.
ViewModel این View هم میتواند به شکل زیر باشد که با کلاس DelegateCommand آن در پیشنیازهای بحث جاری آشنا شدهاید.
using WpfEventTriggerSample.Helper;
namespace WpfEventTriggerSample.ViewModels
{
public class MainWindowViewModel
{
public DelegateCommand<string> DoLoadCommand { set; get; }
public MainWindowViewModel()
{
DoLoadCommand = new DelegateCommand<string>(doLoadCommand, canDoLoadCommand);
}
private void doLoadCommand(string param)
{
//do something
}
private bool canDoLoadCommand(string param)
{
return true;
}
}
}
به این ترتیب حجم قابل ملاحظهای از کد نویسی Attached properties مورد نیاز، به سادهترین شکل ممکن، کاهش خواهد یافت.
بدیهی است این Interaction.Triggers را جهت تمام عناصر UI ایی که حداقل یک رویداد منتسب تعریف شده داشته باشند، میتوان بکار گرفت؛ مثلا تبدیل رویداد Click یک دکمه به یک Command استاندارد:
<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding DoClick}"
CommandParameter="I am loaded!" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
تا اینجا بجای قرار دادن مستقیم قسمت مدیریت هویت کاربران، داخل یک یا چند برنامهی مختلف، این دغدغهی مشترک (common concern) بین برنامهها را به یک برنامهی کاملا مجزای دیگری به نام Identity provider و یا به اختصار IDP منتقل و همچنین دسترسی به کلاینت MVC برنامهی گالری تصاویر را نیز توسط آن امن سازی کردیم. اما هنوز یک قسمت باقی ماندهاست: برنامهی کلاینت MVC، منابع خودش را از یک برنامهی Web API دیگر دریافت میکند و هرچند دسترسی به برنامهی MVC امن شدهاست، اما دسترسی به منابع برنامهی Web API آن کاملا آزاد و بدون محدودیت است. بنابراین امن سازی Web API را توسط IDP، در این قسمت پیگیری میکنیم. پیش از مطالعهی این قسمت نیاز است مطلب «آشنایی با JSON Web Token» را مطالعه کرده و با ساختار ابتدایی یک JWT آشنا باشید.
بررسی Hybrid Flow جهت امن سازی Web API
این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمتهای قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر میشود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.
در اینجا، ابتدا برنامهی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال میکند که response type آن از نوع code id_token است (یا همان مشخصهی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آنها در این درخواست ذکر شدهاند. در سمت IDP، کاربر با ارائهی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحهی اجازهی دسترسی به اطلاعات کاربر (صفحهی consent) را ارائه میدهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامهی وب ارسال میکند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی میکند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه میدهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت میگیرد که به آن استفادهی از back channel هم گفته میشود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامهی کلاینت نیز میباشد. در پاسخ، دو توکن جدید را دریافت میکنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک میشود. سپس هر دو توکن اعتبارسنجی میشوند. در این مرحله، میانافزار اعتبارسنجی، هویت کاربر را از identity token استخراج میکند. به این ترتیب امکان وارد شدن به برنامهی کلاینت میسر میشود. در اینجا همچنین access token ای نیز صادر شدهاست.
اکنون علاقمند به کار با Web API برنامهی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامهی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژهای با کلید Authorization اضافه میشود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی میشود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.
امن سازی دسترسی به Web API
تنظیمات برنامهی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع میکنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
هدف آن داشتن access token ای است که در قسمت Audience آن، نام این ApiResource، درج شده باشد؛ پیش از اینکه دسترسی به API را پیدا کند. برای تعریف آن، متد جدید GetApiResources را به صورت فوق به کلاس Config اضافه میکنیم.
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر میکنیم:
اکنون باید متد جدید GetApiResources را به کلاس src\IDP\DNT.IDP\Startup.cs معرفی کنیم که توسط متد AddInMemoryApiResources به صورت زیر قابل انجام است:
تنظیمات برنامهی MVC Client
اکنون نوبت انجام تنظیمات برنامهی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر میکنیم:
تنظیمات برنامهی Web API
اکنون میخواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شدهاست، دسترسی خواهد داشت.
برای این منظور به پوشهی پروژهی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بستهی نیوگت AccessTokenValidation نصب شود:
اکنون کلاس startup در سطح Web API را در فایل src\WebApi\ImageGallery.WebApi.WebApp\Startup.cs به صورت زیر تکمیل میکنیم:
متد AddAuthentication یک defaultScheme را تعریف میکند که در بستهی IdentityServer4.AccessTokenValidation قرار دارد و این scheme در اصل دارای مقدار Bearer است.
سپس متد AddIdentityServerAuthentication فراخوانی شدهاست که به آدرس IDP اشاره میکند که مقدار آنرا در فایل appsettings.json قرار دادهایم. از این آدرس برای بارگذاری متادیتای IDP استفاده میشود. کار دیگر این میانافزار، اعتبارسنجی access token رسیدهی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شدهی در سمت IDP اشاره میکند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شدهاست یا خیر؟
پس از تنظیم این میانافزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
محل فراخوانی UseAuthentication باید پیش از فراخوانی app.UseMvc باشد تا پس از اعتبارسنجی درخواست، به میانافزار MVC منتقل شود.
اکنون میتوانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
ارسال Access Token به همراه هر درخواست به سمت Web API
تا اینجا اگر مراحل اجرای برنامهها را طی کنید، مشاهده خواهید کرد که برنامهی MVC Client دیگر کار نمیکند و نمیتواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواستهای رسیدهی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آنها با شکست مواجه میشود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح میکنیم تا در صورت وجود Access token، آنرا به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
اسمبلی این سرویس برای اینکه به درستی کامپایل شود، نیاز به این وابستگیها نیز دارد:
در اینجا با استفاده از سرویس IHttpContextAccessor، به HttpContext جاری درخواست دسترسی یافته و سپس توسط متد GetTokenAsync، توکن دسترسی آنرا استخراج میکنیم. سپس این توکن را در صورت وجود، توسط متد SetBearerToken به عنوان هدر Authorization از نوع Bearer، به سمت Web API ارسال خواهیم کرد.
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.
اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار میکند.
بررسی محتوای Access Token
اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، میتوانیم با مراجعهی به سایت jwt.io، محتویات آنرا بررسی نمائیم:
که در حقیقت این محتوا را به همراه دارد:
در اینجا در لیست scope، مقدار imagegalleryapi وجود دارد. همچنین در قسمت audience و یا aud نیز ذکر شدهاست. بنابراین یک چنین توکنی قابلیت دسترسی به Web API تنظیم شدهی ما را دارد.
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.
مدیریت صفحهی عدم دسترسی به Web API
با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحهی دریافت رضایت کاربر نیز ظاهر میشود:
در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامهی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
برای اینکه این صفحهی نمایش استثناء را با صفحهی عدم دسترسی جایگزین کنیم، میتوان پس از دریافت response از سمت Web API، به StatusCode مساوی Unauthorized = 401 به صورت زیر عکسالعمل نشان داد:
فیلتر کردن تصاویر نمایش داده شده بر اساس هویت کاربر وارد شدهی به سیستم
تا اینجا هرچند دسترسی به API امن شدهاست، اما هنوز کاربر وارد شدهی به سیستم میتواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج میکنیم:
اگر به قسمت «بررسی محتوای Access Token» مطلب جاری دقت کنید، مقدار Id کاربر در خاصیت sub این Access token قرار گرفتهاست که روش دسترسی به آنرا در ابتدای اکشن متد GetImages فوق ملاحظه میکنید.
مرحلهی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
پس از این تغییرات، اکشن متد GetImages سمت API چنین پیاده سازی را پیدا میکند که در آن بر اساس Id شخص وارد شدهی به سیستم، صرفا لیست تصاویر مرتبط با او بازگشت داده خواهد شد و نه لیست تصاویر تمام کاربران سیستم:
اکنون اگر از برنامهی کلاینت خارج شده و مجددا به آن وارد شویم، تنها لیست تصاویر مرتبط با کاربر وارد شده، نمایش داده میشوند.
هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشدهاند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه میشود:
و سپس در تمام اکشن متدهای دیگر، در ابتدای آنها باید این بررسی را انجام دهیم و در صورت شکست آن return Unauthorized را بازگشت دهیم.
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی میکنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.
ارسال سایر User Claims مانند نقشها به همراه یک Access Token
برای تکمیل قسمت ارسال تصاویر میخواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته میتوان مستقیما در برنامهی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونهای از آنرا در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشتهای به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامهی MVC client که درخواست دریافت access token را از IDP میدهد، متداول است و نه برنامهی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
در اینجا ذکر claimTypes است که سبب خواهد شد نقش کاربر جاری به توکن دسترسی اضافه شود.
سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود میکنیم:
همچنین در این اکشن متد، پیش از فراخوانی متد AddImageAsync نیاز است مشخص کنیم OwnerId این تصویر کیست تا رکورد بانک اطلاعاتی تصویر آپلود شده، دقیقا به اکانت متناظری در سمت IDP مرتبط شود:
نکتهی مهم: در اینجا نباید این OwnerId را از سمت برنامهی کلاینت MVC به سمت برنامهی Web API ارسال کرد. برنامهی Web API باید این اطلاعات را از access token اعتبارسنجی شدهی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشدهی ارسالی به سمت Web API سادهاست؛ اما access tokenها دارای امضای دیجیتال هستند.
در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به PayingUser میکنیم:
علاوه بر آن، در کنترلر ImageGallery.MvcClient.WebApp\Controllers\GalleryController.cs نیاز است فیلتر Authorize زیر نیز به اکشن متد نمایش صفحهی AddImage اضافه شود تا فراخوانی مستقیم آدرس آن در مرورگر، توسط سایر کاربران میسر نباشد:
این مورد را باید به متد AddImage در حالت دریافت اطلاعات از کاربر نیز افزود تا اگر شخصی مستقیما با این قسمت کار کرد، حتما سطح دسترسی او بررسی شود:
برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینهی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحهی اول سایت، مشاهده خواهید کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
بررسی Hybrid Flow جهت امن سازی Web API
این Flow را پیشتر نیز مرور کرده بودیم. تفاوت آن با قسمتهای قبل، در استفاده از توکن دومی است به نام access token که به همراه identity token از طرف IDP صادر میشود و تا این قسمت از آن بجز در قسمت «دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint» استفاده نکرده بودیم.
در اینجا، ابتدا برنامهی وب، یک درخواست اعتبارسنجی را به سمت IDP ارسال میکند که response type آن از نوع code id_token است (یا همان مشخصهی Hybrid Flow) و همچنین تعدادی scope نیز جهت دریافت claims متناظر با آنها در این درخواست ذکر شدهاند. در سمت IDP، کاربر با ارائهی مشخصات خود، اعتبارسنجی شده و پس از آن IDP صفحهی اجازهی دسترسی به اطلاعات کاربر (صفحهی consent) را ارائه میدهد. پس از آن IDP اطلاعات code و id_token را به سمت برنامهی وب ارسال میکند. در ادامه کلاینت وب، توکن هویت رسیده را اعتبارسنجی میکند. پس از موفقیت آمیز بودن این عملیات، اکنون کلاینت درخواست دریافت یک access token را از IDP ارائه میدهد. اینکار در پشت صحنه و بدون دخالت کاربر صورت میگیرد که به آن استفادهی از back channel هم گفته میشود. یک چنین درخواستی به token endpoint، شامل اطلاعات code و مشخصات دقیق کلاینت جاری است. به عبارتی نوعی اعتبارسنجی هویت برنامهی کلاینت نیز میباشد. در پاسخ، دو توکن جدید را دریافت میکنیم: identity token و access token. در اینجا access token توسط خاصیت at_hash موجود در id_token به آن لینک میشود. سپس هر دو توکن اعتبارسنجی میشوند. در این مرحله، میانافزار اعتبارسنجی، هویت کاربر را از identity token استخراج میکند. به این ترتیب امکان وارد شدن به برنامهی کلاینت میسر میشود. در اینجا همچنین access token ای نیز صادر شدهاست.
اکنون علاقمند به کار با Web API برنامهی کلاینت MVC خود هستیم. برای این منظور access token که اکنون در برنامهی MVC Client در دسترس است، به صورت یک Bearer token به هدر ویژهای با کلید Authorization اضافه میشود و به همراه هر درخواست، به سمت API ارسال خواهد شد. در سمت Web API این access token رسیده، اعتبارسنجی میشود و در صورت موفقیت آمیز بودن عملیات، دسترسی به منابع Web API صادر خواهد شد.
امن سازی دسترسی به Web API
تنظیمات برنامهی IDP
برای امن سازی دسترسی به Web API از کلاس src\IDP\DNT.IDP\Config.cs در سطح IDP شروع میکنیم. در اینجا باید یک scope جدید مخصوص دسترسی به منابع Web API را تعریف کنیم:
namespace DNT.IDP { public static class Config { // api-related resources (scopes) public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource( name: "imagegalleryapi", displayName: "Image Gallery API", claimTypes: new List<string> {"role" }) }; }
پس از آن در قسمت تعریف کلاینت، مجوز درخواست این scope جدید imagegalleryapi را نیز صادر میکنیم:
AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address, "roles", "imagegalleryapi" },
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddIdentityServer() .AddDeveloperSigningCredential() .AddTestUsers(Config.GetUsers()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()); }
تنظیمات برنامهی MVC Client
اکنون نوبت انجام تنظیمات برنامهی MVC Client در فایل ImageGallery.MvcClient.WebApp\Startup.cs است. در اینجا در متد AddOpenIdConnect، درخواست scope جدید imagegalleryapi را صادر میکنیم:
options.Scope.Add("imagegalleryapi");
تنظیمات برنامهی Web API
اکنون میخواهیم مطمئن شویم که Web API، به access token ای که قسمت Audience آن درست مقدار دهی شدهاست، دسترسی خواهد داشت.
برای این منظور به پوشهی پروژهی Web API در مسیر src\WebApi\ImageGallery.WebApi.WebApp وارد شده و دستور زیر را صادر کنید تا بستهی نیوگت AccessTokenValidation نصب شود:
dotnet add package IdentityServer4.AccessTokenValidation
using IdentityServer4.AccessTokenValidation; namespace ImageGallery.WebApi.WebApp { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(defaultScheme: IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddIdentityServerAuthentication(options => { options.Authority = Configuration["IDPBaseAddress"]; options.ApiName = "imagegalleryapi"; });
سپس متد AddIdentityServerAuthentication فراخوانی شدهاست که به آدرس IDP اشاره میکند که مقدار آنرا در فایل appsettings.json قرار دادهایم. از این آدرس برای بارگذاری متادیتای IDP استفاده میشود. کار دیگر این میانافزار، اعتبارسنجی access token رسیدهی به آن است. مقدار خاصیت ApiName آن، به نام API resource تعریف شدهی در سمت IDP اشاره میکند. هدف این است که بررسی شود آیا خاصیت aud موجود در access token رسیده به مقدار imagegalleryapi تنظیم شدهاست یا خیر؟
پس از تنظیم این میانافزار، اکنون نوبت به افزودن آن به ASP.NET Core request pipeline است:
namespace ImageGallery.WebApi.WebApp { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseAuthentication();
اکنون میتوانیم اجبار به Authorization را در تمام اکشن متدهای این Web API در فایل ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs فعالسازی کنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller {
ارسال Access Token به همراه هر درخواست به سمت Web API
تا اینجا اگر مراحل اجرای برنامهها را طی کنید، مشاهده خواهید کرد که برنامهی MVC Client دیگر کار نمیکند و نمیتواند از فیلتر Authorize فوق رد شود. علت اینجا است که در حال حاضر، تمامی درخواستهای رسیدهی به Web API، فاقد Access token هستند. بنابراین اعتبارسنجی آنها با شکست مواجه میشود.
برای رفع این مشکل، سرویس ImageGalleryHttpClient را به نحو زیر اصلاح میکنیم تا در صورت وجود Access token، آنرا به صورت خودکار به هدرهای ارسالی توسط HttpClient اضافه کند:
using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace ImageGallery.MvcClient.Services { public interface IImageGalleryHttpClient { Task<HttpClient> GetHttpClientAsync(); } /// <summary> /// A typed HttpClient. /// </summary> public class ImageGalleryHttpClient : IImageGalleryHttpClient { private readonly HttpClient _httpClient; private readonly IConfiguration _configuration; private readonly IHttpContextAccessor _httpContextAccessor; public ImageGalleryHttpClient( HttpClient httpClient, IConfiguration configuration, IHttpContextAccessor httpContextAccessor) { _httpClient = httpClient; _configuration = configuration; _httpContextAccessor = httpContextAccessor; } public async Task<HttpClient> GetHttpClientAsync() { var currentContext = _httpContextAccessor.HttpContext; var accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); if (!string.IsNullOrWhiteSpace(accessToken)) { _httpClient.SetBearerToken(accessToken); } _httpClient.BaseAddress = new Uri(_configuration["WebApiBaseAddress"]); _httpClient.DefaultRequestHeaders.Accept.Clear(); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); return _httpClient; } } }
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.1.1.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="2.1.1.0" /> <PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="5.2.0.0" /> <PackageReference Include="IdentityModel" Version="3.9.0" /> </ItemGroup> </Project>
البته پس از این تغییرات نیاز است به کنترلر گالری مراجعه و از متد جدید GetHttpClientAsync بجای خاصیت HttpClient قبلی استفاده کرد.
اکنون اگر برنامه را اجرا کنیم، پس از لاگین، دسترسی به Web API امن شده، برقرار شده و برنامه بدون مشکل کار میکند.
بررسی محتوای Access Token
اگر بر روی سطر if (!string.IsNullOrWhiteSpace(accessToken)) در سرویس ImageGalleryHttpClient یک break-point را قرار دهیم و محتویات Access Token را در حافظه ذخیره کنیم، میتوانیم با مراجعهی به سایت jwt.io، محتویات آنرا بررسی نمائیم:
که در حقیقت این محتوا را به همراه دارد:
{ "nbf": 1536394771, "exp": 1536398371, "iss": "https://localhost:6001", "aud": [ "https://localhost:6001/resources", "imagegalleryapi" ], "client_id": "imagegalleryclient", "sub": "d860efca-22d9-47fd-8249-791ba61b07c7", "auth_time": 1536394763, "idp": "local", "role": "PayingUser", "scope": [ "openid", "profile", "address", "roles", "imagegalleryapi" ], "amr": [ "pwd" ] }
همچنین اگر دقت کنید، Id کاربر جاری در خاصیت sub آن قرار دارد.
مدیریت صفحهی عدم دسترسی به Web API
با اضافه شدن scope جدید دسترسی به API در سمت IDP، این مورد در صفحهی دریافت رضایت کاربر نیز ظاهر میشود:
در این حالت اگر کاربر این گزینه را انتخاب نکند، پس از هدایت به برنامهی کلاینت، در سطر response.EnsureSuccessStatusCode استثنای زیر ظاهر خواهد شد:
An unhandled exception occurred while processing the request. HttpRequestException: Response status code does not indicate success: 401 (Unauthorized). System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
public async Task<IActionResult> Index() { var httpClient = await _imageGalleryHttpClient.GetHttpClientAsync(); var response = await httpClient.GetAsync("api/images"); if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden) { return RedirectToAction("AccessDenied", "Authorization"); } response.EnsureSuccessStatusCode();
فیلتر کردن تصاویر نمایش داده شده بر اساس هویت کاربر وارد شدهی به سیستم
تا اینجا هرچند دسترسی به API امن شدهاست، اما هنوز کاربر وارد شدهی به سیستم میتواند تصاویر سایر کاربران را نیز مشاهده کند. بنابراین قدم بعدی امن سازی API، عکس العمل نشان دادن به هویت کاربر جاری سیستم است.
برای این منظور به کنترلر ImageGallery.WebApi.WebApp\Controllers\ImagesController.cs سمت API مراجعه کرده و Id کاربر جاری را از لیست Claims او استخراج میکنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpGet()] public async Task<IActionResult> GetImages() { var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value;
مرحلهی بعد، مراجعه به ImageGallery.WebApi.Services\ImagesService.cs و تغییر متد GetImagesAsync است تا صرفا بر اساس ownerId دریافت شده کار کند:
namespace ImageGallery.WebApi.Services { public class ImagesService : IImagesService { public Task<List<Image>> GetImagesAsync(string ownerId) { return _images.Where(image => image.OwnerId == ownerId).OrderBy(image => image.Title).ToListAsync(); }
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpGet()] public async Task<IActionResult> GetImages() { var ownerId = this.User.Claims.FirstOrDefault(claim => claim.Type == "sub").Value; var imagesFromRepo = await _imagesService.GetImagesAsync(ownerId); var imagesToReturn = _mapper.Map<IEnumerable<ImageModel>>(imagesFromRepo); return Ok(imagesToReturn); }
هنوز یک مشکل دیگر باقی است: سایر اکشن متدهای این کنترلر Web API همچنان محدود به کاربر جاری نشدهاند. یک روش آن تغییر دستی تمام کدهای آن است. در این حالت متد IsImageOwnerAsync زیر، جهت بررسی اینکه آیا رکورد درخواستی متعلق به کاربر جاری است یا خیر، به سرویس تصاویر اضافه میشود:
namespace ImageGallery.WebApi.Services { public class ImagesService : IImagesService { public Task<bool> IsImageOwnerAsync(Guid id, string ownerId) { return _images.AnyAsync(i => i.Id == id && i.OwnerId == ownerId); }
اما روش بهتر انجام این عملیات را که در قسمت بعدی بررسی میکنیم، بر اساس بستن دسترسی ورود به اکشن متدها بر اساس Authorization policy است. در این حالت اگر کاربری مجوز انجام عملیاتی را نداشت، اصلا وارد کدهای یک اکشن متد نخواهد شد.
ارسال سایر User Claims مانند نقشها به همراه یک Access Token
برای تکمیل قسمت ارسال تصاویر میخواهیم تنها کاربران نقش خاصی قادر به انجام اینکار باشند. اما اگر به محتوای access token ارسالی به سمت Web API دقت کرده باشید، حاوی Identity claims نیست. البته میتوان مستقیما در برنامهی Web API با UserInfo Endpoint، برای دریافت اطلاعات بیشتر، کار کرد که نمونهای از آنرا در قسمت قبل مشاهده کردید، اما مشکل آن زیاد شدن تعداد رفت و برگشتهای به سمت IDP است. همچنین باید درنظر داشت که فراخوانی مستقیم UserInfo Endpoint جهت برنامهی MVC client که درخواست دریافت access token را از IDP میدهد، متداول است و نه برنامهی Web API.
برای رفع این مشکل باید در حین تعریف ApiResource، لیست claim مورد نیاز را هم ذکر کرد:
namespace DNT.IDP { public static class Config { // api-related resources (scopes) public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource( name: "imagegalleryapi", displayName: "Image Gallery API", claimTypes: new List<string> {"role" }) }; }
سپس کار با اکشن متد CreateImage در سمت API را به نقش PayingUser محدود میکنیم:
namespace ImageGallery.WebApi.WebApp.Controllers { [Route("api/images")] [Authorize] public class ImagesController : Controller { [HttpPost] [Authorize(Roles = "PayingUser")] public async Task<IActionResult> CreateImage([FromBody] ImageForCreationModel imageForCreation) {
var ownerId = User.Claims.FirstOrDefault(c => c.Type == "sub").Value; imageEntity.OwnerId = ownerId; // add and save. await _imagesService.AddImageAsync(imageEntity);
نکتهی مهم: در اینجا نباید این OwnerId را از سمت برنامهی کلاینت MVC به سمت برنامهی Web API ارسال کرد. برنامهی Web API باید این اطلاعات را از access token اعتبارسنجی شدهی رسیده استخراج و استفاده کند؛ از این جهت که دستکاری اطلاعات اعتبارسنجی نشدهی ارسالی به سمت Web API سادهاست؛ اما access tokenها دارای امضای دیجیتال هستند.
در سمت کلاینت نیز در فایل ImageGallery.MvcClient.WebApp\Views\Shared\_Layout.cshtml نمایش لینک افزودن تصویر را نیز محدود به PayingUser میکنیم:
@if(User.IsInRole("PayingUser")) { <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li> <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li> }
namespace ImageGallery.MvcClient.WebApp.Controllers { [Authorize] public class GalleryController : Controller { [Authorize(Roles = "PayingUser")] public IActionResult AddImage() { return View(); }
[HttpPost] [Authorize(Roles = "PayingUser")] [ValidateAntiForgeryToken] public async Task<IActionResult> AddImage(AddImageViewModel addImageViewModel)
برای آزمایش این قسمت یکبار از برنامه خارج شده و سپس با اکانت User 1 که PayingUser است به سیستم وارد شوید. در ادامه از منوی بالای سایت، گزینهی Add an image را انتخاب کرده و تصویری را آپلود کنید. پس از آن، این تصویر آپلود شده را در لیست تصاویر صفحهی اول سایت، مشاهده خواهید کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.