تذکر: این یک پست آموزشی برای عموم نیست! بلکه تنها جایی برای یادداشتهای من حین یادگیریه تا بهتر به خاطر بسپارم و در صورت لزوم به اونها مراجعه کنم.
قسمت ۸۱ تا ۹۳
کنترلر Login:
این کنترلر شبیه به register هست و دو تا روت get و post داره و مثل register متدهای showLoginForm و login اون در فایل AuthenticatesUsers در vendor قرار داره.
متد guard چیه؟
...
وقتی که لاگین با موفقیت انجام میشه ما ریدایرکت میشیم به صفحهٔ home، اما این پروسه چطور رخ میده؟ توی کنترلر LoginController یک property داریم به اسم redirectTo:
protected #redirectTo = RouteServiceProvider::Home;
پس RouteServiceProvider یک Provider هست و محل قرارگیریش در app/Providers که اونجا مقادیر متفاوتی ست شده از جمله، HOME، که با تغییر این public const میتونیم مسیر redirect رو عوض کنیم.
توی تصویر زیر یک نمایی از جدول users در دیتابیس رو داریم که یک فیلدی داره به اسم remember_token و این فیلد در واقع اشاره داره به Remember Me checkbox که اگر تیک بخوره یک توکنی ست میشه و لاراول در دفعات بعدی session رو چک میکنه و اگر اوکی باشه، اتوماتیک لاگین میشه:
این session تا یه زمان مشخصی فعاله و بعدش منقضی میشه، با منقضی شدن session دیگه اون remember_token کارایی نداره و نمیشه لاگین کرد مگر اینکه دوباره دستی به سیستم لاگین کنیم و ادامهٔ ماجرا.
برای تنظیم مدت زمان انقضای session باید وارد پوشهٔ config و فایل session.php بشیم؛ و مقدار lifetime رو تغییر بدیم و یا اینکه اگر متغیر SESSION_LIFETIME رو توی فایل env اگر وجود داره تغییر بدیم و اگر نیست تعریف کنیم و بهش مقدار بدیم.
مثل رجیستر و لاگین، password reset هم post و get داره.
از اونجایی که وقتی ما توی لوکالهاست هستیم و دسترسی به سرویس ارسال ایمیل نداریم؛ برخلاف سرور و هاست که توش میشه این سرویس رو کانفیگ کرد و بهش دسترسی داشت؛ باید از سرویس هایی که توی development مورد استفاده قرار میگیرن مثل mailtrap.io استفاده کرد.
بعد از رجیستر و لاگین توی این سیستم؛ توی بخش Demo index اطلاعات مربوط به سرویس SMTP ما اومده که میتونیم اون رو توی app لاراولی خودمون ست کنیم و ازش استفاده کنیم.
توی فایل env میتونیم اطلاعات ایمیل رو در این بخش وارد کنیم:
نمونهی پر شده:
خب حالا کافیهٔ که توی روت get مربوط به ریست پسورد؛ ایمیل خودمون رو وارد کنیم تا لینک ریست پسورد برای ما ارسال بشه، وقتی که ما درخواست ریست پسورد میدیم؛ در واقع توی جدول reset_passwords در دیتابیس یک ریکورد جدید ایجاد میشه که حاوی ایمیل، توکن پسورد ریست و... هست که عکسش رو در زیر گذاشتم؛ حالا کافیه که پسورد جدید رو بدیم و تمام؛ آها یک چیز دیگه اینکه توکن ریست و ایمیل هم در URL مربوط به فرم وارد کردن پسورد جدید دیده میشه:
این قابلیت اصلا چی هست؟
فرض کنید یکی بیاد با کلی ایمیل جعلی یه سری اکانت توی سایت ما بسازه و ما هم مثلا به ازای هر اکانت تازه ساخته شده یه سری تسهیلات بدیم؛ این کار راه رو برای سو استفاده خیلی باز میکنه، پس برای اینکه از شدت این ماجرا تا حد زیادی کم کنیم؛ باید کاری کنیم تا زمانی که کاربر ایمیل ثبت نامی که براش اومده رو تایید نکرده اجازهٔ فعالیت یا یه سری فعالیت خاص رو نداشته باشه.
برای فعال سازی این سیستم، کافیه که به راحتی توی web.php یعنی فایل web routes بیاییم و به Auth::routes() آرگومان بدیم:
Auth::routes(['verify' => true]);
علاوه بر این، باید کلاس MustVerifyEmail رو هم روی مدل یوز Implement کنیم:
class User extends Authenticatable implements MustVerifyEmail { ..... }
و نکتهٔ بعدی اینکه وقتی implement شد، حتما باید use هم شود.
خب خب، اگر بخوایم قبل از verify کردن ایمیل، جلوی کاربر برای دسترسی به صفحات سایت رو بگیریم باید یک middleware اضافه کنیم:
توی تصویر بالا که یک نمایی از جدول users رو نشون میده، بعد از تایید ایمیل، فیلد email_verified_at یک مقدار گرفته (تاریخ) و طبق همون سیستم ما متوجه میشه که این کاربر ایمیلش تایید شده یا خیر.
این سیستم چیه و چطوری کار میکنه؟
تصور کنید که کاربری در سیستم لاگین کرده و session فعال داره اما قراره یک عملیات خاص رو توی سایت انجام بده که نیازه تا مطمئن بشیم فقط و فقط خودش پشت سیستمه، در این حالت از Password Confirmation استفاده میشه. یعنی برای انجام یک کار خاص در عین حالی که session فعال داریم؛ اما باز از کاربر درخواست پسورد میکنیم.
روتهای این سرویس با Auth::routes ایجاد میشن که شامل get و post هست.
توی تصویر پایین، به web.php یک روت جدید اضافه کردیم؛ اما براش دو تا middleware تعریف کردیم؛ یکی auth که نشون میده کاربر حتما باید لاگین کرده باشه و دیگری password.confirm که دوباره از کاربر پسورد درخواست میکنه.
این password confirmation تا سه ساعت فعاله، اما به راحتی میتونیم توی config و بخش auth میزان password_timeout رو کم و زیاد کنیم.
ویوهای مربوط به این سیستم در مسیر resources/views/auth قرار دارن، برای مثال login.blade.php از app.blade.php که توی پوشهٔ layouts قرار داره extend میشه.
توی app.blade.php یک directive داریم که navbar توش قرار داره و این طوری عمل میکنه که میاد چک میکنه که آیا کاربر مهمان هست یا خیر، و طبق اون یک view خاص رو نمایش میده:
دو تا directive مهم در viewهای مربوط به auth، میتونیم به موارد زیر اشاره کنیم:
@auth ... @endauth // @guest ... @endguest
یه directive دیگه @error که میاد و چک میکنه و اگر ارور داشتیم فلان کلاس رو قرار بده یا فلان پیام رو نشون بده و... که همگی برای هندل کردن کارهای فرانت ما هست که با نگاه کردن به viewها به راحتی میشه از اونها سر در آورد.
مورد ۱- auth: چک کردن اینکه کاربر لاگین شده یا خیر. ما میتونیم این میدلور رو روی یک روت یا حتی در constructor مربوط به کنترلر مد نظر قرار بدیم.
مورد ۲- password.confirm: نشون دادن صفحه درخواست مجدد پسورد.
مورد ۳- verified: بررسی اینکه کاربر ایمیلش رو تایید کرده یا خیر.
مورد ۴- auth.basic: نمونهٔ ساده تر auth هست و به این شکل عمل میکنه که یک پاپ آپ نشون میده و از اونجا میخواد که authentication انجام بده، اما خود auth، ما رو به صفحهٔ لاگین پیج ریدایرکت میکنه.
این middlewareها که در موردشون صحبت کردیم داخل app/Http/Middleware/Kernel.php قرار دارن و براشون alias هم تعریف شده (توی یک آرایهٔ associative):
یکی از متدهای Auth facade (فساد در واقع یک دیزاین پترنه که میاد و از پیچیدگی چندین کلاس کم میکنه و به راحتی میتونیم از متدهای اون کلاسها استفاده کنیم...) user هست که برای ما اسم userای که لاگین کرده رو بر میگردونه:
// returns the name of the logged-in user Auth::user(); // using helper instead of facade auth()->user(); auth()->id(); // check method, returns true or false if a user is logged-in or not auth()->check(); //logout method, logs out the user auth()->logout(); // login method, this method logs in a specific user $user = User::find(2); auth()->login($user); // or simply: auth()->loginUsingId($num);
کنترل سطح دسترسی به بخشهای مختلف برنامه با توجه به نقشی که آن یوزر خاص دارد. مثلا admin میتونه کاربر رو اضافه/حذف کنه اما یک یوزر عادی نباید یک چنین دسترسی داشته باشه.
در مبحث authorization کاربر لاگین کرده و حالا باید سطوح دسترسی اون رو اوکی کنیم.
برای درک بهتر این مبحث یک جدول جدید به database اضافه کردیم:
توی تصویر زیر داریم میبینیم که مدل Post تعریف شده و یک متدی داره به اسم user که رابطه رو برقرار کرده و نوع رابطه یک به چند هست؛ یعنی هر user چندین پست داره، اما هر پست تنها متعلق هست به یک user خاص، جلمه $this داره میگه که این پست متعلق هست به فلان یوزر:
توی تصویر زیر مدل User رو میبینیم که متدی داره به اسم posts که رابطه رو برقرار میکنه، به این صورت که میگه این user، فلان پستها رو داره:
تصویری از migration مربوط به post:
کل پروسه ساخت مدل، مایگریشن و کنترلر با artisan سادهسازی شده و کافیه که دستور زیر رو بزنیم تا همشون رو برامون بسازه:
php artisan make:model Post -mc
در واقع آپشن m برای ما model رو میسازه و آپشن c برای ما controller رو ایجاد میکنه.
** نکتهٔ مهم، توی نسخههای بعد از ۶ میتونیم به جای bigIncreaments برای id از id() استفاده کنیم. همینطور foreignId جایگزین unsignedBigInteger شده.
مجموعه ای gateها میشه policy:
وقتی کاربری میخواد کاری رو انجام بده ما اون رو از یک دروازه یا gate عبور می دیم تا چک کنیم که آیا دسترسی دارد یا خیر.
خب خب، میخوایم با اون جدول posts چیکار کنیم؟ میخوایم اجازهٔ ویرایش رو تنها به صاحب همون پست بدیم. برای کنترل این عملیات از gate استفاده میکنیم.
منطق سطح دسترسی یه چیز تو مایههای عکس زیره، توی تصویر زیره، اول میاییم چک میکنیم کدوم کاربر لاگین کرده، بعدش میایم میبینیم قراره به کدوم پست دسترسی داشته باشه و داخل شرط میزان user_id اون پست رو با id یوزر لاگین کرده مقایسه میکنیم و اگر برابر بود، باقی عملیات...:
اما توی لاراول با استفاده از gate و policy این عملیات خیلی ساده و راحت شده:
برای تعریف gate، باید وارد AuthServiceProvider بشیم و توی متد boot به این شکل یک گیت اضافه کنیم:
Gate::define('update-post', function($user, $post){ if ($post->user_id === $post->id){ return true; } else{ return false; } });
که البته اون update-post یک اسم دلخواه هست و می تونیم هر چیز دیگه ای براش بذاریم؛ اما باید توجه کنیم که این نام گذاری قواعد و conventionهایی داره که اگر رعایت بشه بهتره.
خب، حالا که قواعد مربوط به این gate رو تعریف کردیم؛ میتونیم ازش توی کنترلر خودمون استفاده کنیم؛ به شکل زیر:
$user = auth()->user(); $post = Post::find(1); $allow = Gate::allows('update-post', $post); if ($allow){ //... }
در ادامه میخوایم سطح دسترسی userای رو چک کنیم که اصلا لاگین (به هر صورت یک یوزر خاص رو میخوایم بررسی کنیم) برای این کار از متد forUser مربوط به Gate facade استفاده میکنیم:
علاوه بر allows متدی داریم به اسم can که روی شی برگشته از user مربوط به auth ران میشه:
و همینطور authorize که مثل can عمل میکنه، اما این متد روی خود کنترلر که میتونیم با $this بهش دسترسی داشته باشیم ران میشه، چرا که کنترلرهای ما از یک کلاس اصلی یعنی Controller گسترش پیدا میکنن و این کنترلر هم AuthorizesRequests رو use میکنه، پس:
$this->authorize('update-post', $post);
اما authorize اینطور عمل میکنه که دیگه true و false بر نمیگردونه و اگر اوکی باشه که ماجرا ادامه پیدا میکنه اما اگر اوکی نبود؛ صفحه زیر رو برای ما نمایش میده:
معمولا همهٔ پروژه ها یک user با سطح دسترسی بالا دارن که هر کاری میتونه انجام بده، در واقع همون Admin user، حالا ما باید کاری کنیم که این user از شروط gate کنار گذاشته بشه، برای این کار میتونیم مثلا طبق type یوزر در table بیاییم این کار رو بکنیم یا اینکه از نوع خاصی از gate استفاده کنیم که در ادامه بهش میپردازیم:
یه چیز دیگه، علاوه بر متد allows یک متدی داریم به اسم denies که برعکس allows عمل میکنه.
مبحث Gate::before و Gate::after:
توی مثال قبلی، ما برای رد کردن بررسی دسترسی admin، اومدیم و یه شرط نوشتیم؛ که خب طبیعتا کار هم میکنه، اما مشکلی که داره اینه که ما برای gateهای مختلف مجبوریم این شرط رو بذاریم و این کد ما رو کثیف میکنه، برای حل این مشکل از متدهای before و after مربوط به Gate facade میشه:
خب توی تصویر بالا اما اومدیم یک Gate::before تعریف کردیم، این متد یک Colsure میگیره به که سه تا پارامتر ورودی داره، اولی user دومی نوع کاری که قراره انجام بدیم، یعنی ability یا action و در نهایت params یا پارامترهایی که می گیره، حالا قبل از update-post این Gate::before اجرا میشه و دیگه نیازی نیست ما برای هر gate این شرط رو تعریف کنیم.
** نکته:
حالا ما برای دونستن اینکه این closure مربوط به این before چه ورودیهایی داره یا باید doc بخونیم یا اینکه با تابع func_get_args داخل بدنه closure چک کنیم چه ورودیهایی می گیره.
اما میریم سراغ Gate::after، مهم نیست که حتما این Gate::after بعد از Gate::define قرار بگیره، فقط برای خوانایی میتونیم این کار رو بکنیم. ترتیب اجرا شدن gateها اینطوری هست که اول before اجرا میشه، اگر true بود؛ مستقیم میره توی after اما اگر false بود؛ اول میره توی Gate::define و بعدش حتما after ران میشه. با func_get_args اومدیم after رو چک کردیم و دیدیم که چهار تار ورودی میگیره:
به مجموعهای از gateها میگیم policy، یکی از علتهای مهم استفاده از gateها جلوگیری از بهم ریختگی و آشفتگی کدها توی متد boot در AuthServiceProvider هست. حالا کافیه gateهای مرتبط رو داخل policy ها قرار بدیم؛ مثلا policy پستها، کتگوریها و...
برای ایجاد یک policy از artisan کمک میگیریم:
php artisan make:policy PostPolicy --model=Post
حالا توی app یک پوشهٔ جدید به نام Policies ایجاد میشه که policyهای ما اینجا قرار می گیره. با قرار دادن آپشن --model ما میاییم و این policy رو با مدل Post مپ میکنیم. حالا اگر وارد PostPolicy.php بشیم میبینیم که این policy یه سری متد داره مثل update یا delete و... که یه جورایی مرتبط با همون عملیات CRUD میشه و به راحتی میتونیم از اونها استفاده کنیم؛ برای مثال، توی تصویر زیر ما برای PostPolicy و توی متد update یک شرطی رو بررسی کردیم که true یا false برمیگردونه:
یا مثال دیگه اینه که به راحتی یه شرط میذاریم که اگر کاربر ما admin بود حق داره فلان کار رو بکنه:
برای اینکه بتونیم از این policy استفاده کنیم باید اون رو توی AuthServiceProvider رجیستر کنیم:
برای رجیستر کردن Policy اومدیم و توی آرایهٔ associative با نام policies داخل AuthServiceProvider عبارت زیر رو وارد کردیم؛ اما نکته اش اینه که چرا مثل مثال از رشته استفاده نکردیم؟ چون ::class خود به خود اسم اون namespace رو به صورت string برمیگردونه و نیازی به نوشتن رشته نداریم.
حالا چطور باید از این Policy استفاده کنیم؟ خیلی راحت توی Controller می تونیم ازش استفاده کنیم:
$user = auth()->user(); $post = Post::find(2); $allow = Gate::allows('update', $post); dd($allow);
دیگه نیازی نیست که توی allows دقیقا اسم policy رو مشخص کنیم و خود لاراول این رو هندل میکنه که این $post مربوط به فلان policy میشه.
خب، اما اگر نگاهی به متدهای policy بندازیم میبینیم که همگی متدها به غیر از create آرگومان $post رو دارن، خب حالا اگر بخوایم که یک post ایجاد کنیم؛ چطور به gate حالی کنیم که دنبال فلان Policy هستیم؟ به این شکل:
در واقع اومدیم و کلاس Post::class رو براش فرستادیم که میشه همون مدل posts.
اما بریم سراغ سایر روشهای استفاده از Policy:
$can = $user->can('update', $post); $this->authorize('update', $post); $this->authorize('create', Post::class);
مبحث middleware در policy:
Route::put('/posts/{post}', function(Post $post){ //.... })->middleware('can:update,post ');
برای create کردن:
Route::post('/posts', function() { //.... })->middleware('can:update,App\Post ');
ما به راحتی میتونیم توی blade از policyهای خودمون استفاده کنیم؛ یک directive داریم به اسم @can که میتونیم authorize رو چک کنیم:
@can('update', $post) // for create: App\Post::class // @else //we have @elsecan() too // @canend