نظرات مطالب
ASP.NET MVC و Identity 2.0 : مفاهیم پایه
سلام
میخاستم بدونم در این سیستم وقتی که یک رول از یک کاربر گرفته میشود همان لحظه رول گرفته میشود یا کاربر باید log off شود تا اعمال شود؟
در این مقاله هدف این است که GraphQL را در ASP.NET Core راه اندازی کنیم. از یک کتابخانه ثالث برای آسانتر کردن یکپارچگی استفاده میکنیم و همچنین با جزئیات، توضیح خواهیم داد که چگونه میتوان از elementهای مربوط به GraphQL مثل (Type ،Query و Schema) برای کامل کردن فرآیند یکپارچگی در ASP.NET Core استفاده کنیم.
GraphQL و تفاوتهای آن با REST
GraphQl یک query language میباشد که queryها را با استفاده از type systemهایی که ما برای دادهها تعریف میکنیم، اجرا میکند. GraphQL به هیچ زبان یا پایگاه داده مشخصی گره نخورده است.
- GraphQL نیازمند رفت و برگشتهای کمتری به server، به منظور بازیابی دادهها برای template یا view است. همراه با REST ما مجبور هستیم که چندیدن endpoint مثلا (... api/students, api/courses, api/instructors ) را برای گرفتن همه دادههای که برای template یا view نیاز داریم ملاقات کنیم؛ ولی این شرایط در GraphQL برقرار نیست. با GraphQL ما تنها یک query را ایجاد میکنیم که چندین تابع (resolver) را در سمت سرور فراخوانی میکند و همه دادهها را از منابع مختلفی، در یک درخواست برگشت میدهد.
- همراه با REST، همانطور که Application ما رشد میکند، تعداد endpointها هم زیاد میشوند که این نیازمند زمان بیشتری برای نگهداری میباشد. اما با GraphQL ما تنها یک endpoint داریم؛ همین!
- با استفاده از GraphQL، ما هرگز به مشکل گرفتن دادههایی کم یا زیاد از منبع روبرو نخواهیم شد. به این خاطر است که ما queryها را با فیلدهایی که چیزهایی را که نیاز داریم، نشان میدهند، تعریف میکنیم. در این صورت ما همیشه چیزهایی را که درخواست دادهایم، دریافت میکنیم.
بنابراین اگر یک query شبیه زیر را ارسال کنیم :
query OwnersQuery { owners { name account { type } } }
{ "data": { "owners": [ { "name": "John Doe", "accounts": [ { "type": "Cash" }, { "type": "Savings" } ] } ] } }
شروع کار
یک پروژه جدید را با استفاده از دستور زیر ایجاد میکنیم:
dotnet new api -n ASPCoreGraphQL
پوشه Contracts شامل واسطهای مورد نیاز برای repository logic میباشد:
namespace ASPCoreGraphQL.Contracts { public interface IOwnerRepository { } }
namespace ASPCoreGraphQL.Contracts { public interface IAccountRepository { } }
در پوشه Models، کلاسهای مدل را نگه داری میکنیم؛ به همراه یک کلاس context و کلاسهای configuration:
public class Owner { [Key] public Guid Id { get; set; } [Required(ErrorMessage = "Name is required")] public string Name { get; set; } public string Address { get; set; } public ICollection<Account> Accounts { get; set; } } public class Account { [Key] public Guid Id { get; set; } [Required(ErrorMessage = "Type is required")] public TypeOfAccount Type { get; set; } public string Description { get; set; } [ForeignKey("OwnerId")] public Guid OwnerId { get; set; } public Owner Owner { get; set; } } public enum TypeOfAccount { Cash, Savings, Expense, Income }
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { } public DbSet<Owner> Owners { get; set; } public DbSet<Account> Accounts { get; set; } }
در پوشه Repository، کلاسهای مرتبط با منطق بازیابی دادهها را داریم:
public class OwnerRepository : IOwnerRepository { private readonly ApplicationContext _context; public OwnerRepository(ApplicationContext context) { _context = context; } }
public class AccountRepository : IAccountRepository { private readonly ApplicationContext _context; public AccountRepository(ApplicationContext context) { _context = context; } }
repository logic، یک راه اندازی ابتدایی بدون هیچ لایه اضافهتری است.
کلاس context و کلاسهای repository، در فایل Startup.cs ثبت میشوند:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddScoped<IOwnerRepository, OwnerRepository>(); services.AddScoped<IAccountRepository, AccountRepository>(); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddJsonOptions(options => options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore); }
Integration of GraphQL in ASP.NET Core
برای کار کردن با GraphQL در ابتدا نیاز است که کتابخانه GraphQL را نصب کنیم. به همین منظور در ترمینال مربوط به VS Code، دستور زیر را وارد میکنیم:
dotnet add package GraphQL
و همچنین کتابخانهی زیر که به ما کمک میکند تا GraphQL.NET را به عنوان یک وابستگی دریافت کنیم:
dotnet add package GraphQL.Server.Transports.AspNetCore
dotnet add package GraphQL.Server.Ui.Playground
(Creating GraphQL Specific Objects (Type, Query, Schema
کار را با ایجاد کردن یک پوشه جدید به نام GraphQL و سپس در آن ایجاد یک پوشه دیگر به نام GraphQLSchema، شروع میکنیم. در پوشه GraphQLSchema، یک کلاس را به نام AppSchema، ایجاد میکنیم.
کلاس AppSchema باید از کلاس Schema ارث بری کند تا در فضای نام GraphQL.Types قرار گیرد. در سازنده این کلاس، IDependencyResolver را تزریق میکنیم که قرار است به ما کمک کند تا اشیاء Query ،Mutation یا Subscription را resolve کنیم:
public class AppSchema : Schema { public AppSchema(IDependencyResolver resolver) :base(resolver) { } }
فعلا این کلاس را در همین حالت میگذاریم و سپس یک پوشه را به نام GraphQLTypes در پوشه GraphQL ایجاد میکنیم. در پوشه GraphQLTypes یک کلاس را به نام OwnerType ایجاد میکنیم:
public class OwnerType : ObjectGraphType<Owner> { public OwnerType() { Field(x => x.Id, type: typeof(IdGraphType)).Description("Id property from the owner object."); Field(x => x.Name).Description("Name property from the owner object."); Field(x => x.Address).Description("Address property from the owner object."); } }
از کلاس OwnerType به عنوان یک جایگزین برای مدل Owner درون یک GraphQL API استفاده میکنیم. این کلاس از نوع جنریک ObjectGraphType ارث بری میکند. با متد Field، فیلدهایی را که بیانگر خصوصیات مدل Owner میباشند، مشخص میکنیم.
در ابتدا واسط IOwnerRepository و کلاس OwnerRepository را به حالت زیر ویرایش میکنیم:
public interface IOwnerRepository { IEnumerable<Owner> GetAll(); } public class OwnerRepository : IOwnerRepository { private readonly ApplicationContext _context; public OwnerRepository(ApplicationContext context) { _context = context; } public IEnumerable<Owner> GetAll() => _context.Owners.ToList(); }
در ادامه یک پوشه دیگر را به نام GraphQLQueries در پوشهی GraphQL ایجاد و سپس در آن یک کلاس را به نام AppQuery ایجاد و آن را به حالت زیر ویرایش میکنیم:
public class AppQuery : ObjectGraphType { public AppQuery(IOwnerRepository repository) { Field<ListGraphType<OwnerType>>( "owners", resolve: context => repository.GetAll() ); } }
توضیحات AppQuery
همانطور که میبینیم این کلاس از ObjectGraphType ارث بری میکند. در سازنده کلاس، IOwnerRepository را تزریق میکنیم و یک فیلد را به منظور برگشت دادن نتیجه برای یک Query مشخص، ایجاد میکنیم. در این کلاس، از نوع جنریک متد Field، استفاده کردهایم که تعدادی type را به عنوان یک پارامتر جنریک پذیرش میکند که این بیانگر GraphQL.NET برای type های معمول در NET. میباشد. علاوه بر ListGraphType، نوعهایی مثل IntGraphType و StringGraphType و ... وجود دارند (لیست کامل).
پارامتر owners نام فیلد میباشد (query مربوط به کلاینت باید با این نام مطابقت داشته باشد) و پارامتر دوم نتیجه میباشد.
بعد از انجام این مقدمات، اکنون کلاس AppSchema را باز میکنیم و به حالت زیر آن را ویرایش میکنیم:
public class AppSchema : Schema { public AppSchema(IDependencyResolver resolver) :base(resolver) { Query = resolver.Resolve<AppQuery>(); } }
Libraries and Schema Registration
در کلاس Startup نیاز است کتابخانههای نصب شده و هم چنین کلاس schema ایجاد شده را ثبت کنیم. این کار را با ویرایش کردن متد ConfigureServices و Configure انجام میدهیم:
public void ConfigureServices(IServiceCollection services) { ... services.AddScoped<IDependencyResolver>(s => new FuncDependencyResolver(s.GetRequiredService)); services.AddScoped<AppSchema>(); services.AddGraphQL(o => { o.ExposeExceptions = false; }) .AddGraphTypes(ServiceLifetime.Scoped); ... }
در نهایت در متد schema Configure را به خط لوله درخواستها (request’s pipeline) اضافه میکنیم و هم چنین ابزار Playground UI:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseGraphQL<AppSchema>(); app.UseGraphQLPlayground(options: new GraphQLPlaygroundOptions()); app.UseMvc(); }
برای آزمایش GraphQL API امان، از ابزار GraphQL.UI.Playground استفاده میکنیم. در ابتدا پروژه را با دستور زیر اجرا میکنیم:
dotnet run
و سپس در مرورگر به این آدرس میرویم:
https://localhost:5001/ui/playground
کدهای مربوط به این قسمت را از اینجا دریافت کنید .ASPCoreGraphQL.zip
پس از تکمیل کنترل دسترسیها به قسمتهای مختلف برنامه بر اساس نقشهای انتسابی به کاربر وارد شدهی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شدهی آن است.
افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard
در اینجا قصد داریم صفحهای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شدهی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شدهی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه میکنیم:
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه میکنیم:
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقشهای Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شدهی به سیستم (isLoggedIn) قابل مشاهده باشد:
نمایش و یا مخفی کردن قسمتهای مختلف صفحه بر اساس نقشهای کاربر وارد شدهی به سیستم
در ادامه میخواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شدهی سمت سرور را بازگشت دهند. دکمهی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمهی دوم توسط کاربری با نقشهای Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر میدهیم:
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفتهاند. مقدار دهی آنها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام میشود. اکنون که این دو خاصیت مقدار دهی شدهاند، میتوان از آنها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمتهای مختلف صفحه استفاده کرد:
دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال میکند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل میتوان در AuthService تولید نمود:
روش دوم انجام اینکار که مرسومتر است، اضافه کردن خودکار این هدر به تمام درخواستهای ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل میکنیم:
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام میشود:
در اینجا نحوهی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده میکنید.
در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعهدهندههای مرورگر مشاهده خواهید کرد:
در سازندهی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شدهاست که این سرویس نیز دارای HttpClient تزریق شدهی در سازندهی آن است. به همین جهت Angular تصور میکند که ممکن است در اینجا یک بازگشت بینهایت بین این interceptor و سرویس Auth رخدهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار میکنند، در اینجا فراخوانی نمیکنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را میتوان به صورت ذیل برطرف کرد:
ابتدا سرویس Injector را به سازندهی کلاس AuthInterceptor تزریق میکنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازندهی کلاس):
تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
اکنون پس از افزودن AuthInterceptor، میتوان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویسهای Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازندهی CallProtectedApiComponent تزریق میکنیم:
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:
مدیریت خودکار خطاهای عدم دسترسی ارسال شدهی از سمت سرور
ممکن است کاربری درخواستی را به منبع محافظت شدهای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده میتوان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحهی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
در اینجا ابتدا نیاز است سرویس Router، به سازندهی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیتهای 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه میکنیم:
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
چون این نقش جدید به کاربر جاری انتساب داده نشدهاست (جزو اطلاعات سمت سرور او نیست)، اگر آنرا توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت میشود:
نکتهی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور
اگر برنامهی سمت سرور ما که توکنها را اعتبارسنجی میکند، ریاستارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکنهای فعلی او برگشت خواهند خورد و از مرحلهی تعیین اعتبار رد نمیشوند. در این حالت کاربر خطای 401 را دریافت میکند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکیهای برنامههای ASP.NET Core هاست شدهی در IIS پس از ریاستارت آن» را فراموش نکنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard
در اینجا قصد داریم صفحهای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شدهی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شدهی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه میکنیم:
>ng g c Dashboard/CallProtectedApi
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component"; const routes: Routes = [ { path: "callProtectedApi", component: CallProtectedApiComponent, data: { permission: { permittedRoles: ["Admin", "User"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شدهی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active"> <a [routerLink]="['/callProtectedApi']">Call Protected Api</a> </li>
نمایش و یا مخفی کردن قسمتهای مختلف صفحه بر اساس نقشهای کاربر وارد شدهی به سیستم
در ادامه میخواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شدهی سمت سرور را بازگشت دهند. دکمهی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمهی دوم توسط کاربری با نقشهای Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر میدهیم:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-call-protected-api", templateUrl: "./call-protected-api.component.html", styleUrls: ["./call-protected-api.component.css"] }) export class CallProtectedApiComponent implements OnInit { isAdmin = false; isUser = false; result: any; constructor(private authService: AuthService) { } ngOnInit() { this.isAdmin = this.authService.isAuthUserInRole("Admin"); this.isUser = this.authService.isAuthUserInRole("User"); } callMyProtectedAdminApiController() { } callMyProtectedApiController() { } }
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()"> Call Protected Admin API [Authorize(Roles = "Admin")] </button> <button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()"> Call Protected API ([Authorize]) </button> <div *ngIf="result"> <pre>{{result | json}}</pre> </div>
دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال میکند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل میتوان در AuthService تولید نمود:
getBearerAuthHeader(): HttpHeaders { return new HttpHeaders({ "Content-Type": "application/json", "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}` }); }
روش دوم انجام اینکار که مرسومتر است، اضافه کردن خودکار این هدر به تمام درخواستهای ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل میکنیم:
import { Injectable } from "@angular/core"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import { AuthService, AuthTokenType } from "./auth.service"; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام میشود:
import { HTTP_INTERCEPTORS } from "@angular/common/http"; import { AuthInterceptor } from "./services/auth.interceptor"; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class CoreModule {}
در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعهدهندههای مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors: Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
import { Injector } from "@angular/core"; constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService);
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
اکنون پس از افزودن AuthInterceptor، میتوان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویسهای Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازندهی CallProtectedApiComponent تزریق میکنیم:
constructor( private authService: AuthService, private httpClient: HttpClient, @Inject(APP_CONFIG) private appConfig: IAppConfig, ) { }
callMyProtectedAdminApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); } callMyProtectedApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:
مدیریت خودکار خطاهای عدم دسترسی ارسال شدهی از سمت سرور
ممکن است کاربری درخواستی را به منبع محافظت شدهای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده میتوان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحهی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); });
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor( private injector: Injector, private router: Router) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); }); } else { // login page return next.handle(request); } } }
using ASPNETCore2JwtAuthentication.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; namespace ASPNETCore2JwtAuthentication.WebApp.Controllers { [Route("api/[controller]")] [EnableCors("CorsPolicy")] [Authorize(Policy = CustomRoles.Editor)] public class MyProtectedEditorsApiController : Controller { public IActionResult Get() { return Ok(new { Id = 1, Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]", Username = this.User.Identity.Name }); } } }
callMyProtectedEditorsApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
نکتهی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور
اگر برنامهی سمت سرور ما که توکنها را اعتبارسنجی میکند، ریاستارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکنهای فعلی او برگشت خواهند خورد و از مرحلهی تعیین اعتبار رد نمیشوند. در این حالت کاربر خطای 401 را دریافت میکند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکیهای برنامههای ASP.NET Core هاست شدهی در IIS پس از ریاستارت آن» را فراموش نکنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
مطالب
PowerShell 7.x - قسمت سیزدهم - ساخت یک Static Site Generator ساده توسط PowerShell و GitHub Actions
در این مطلب میخواهیم یک مثال دیگر از PowerShell را به همراه GitHub Actions را بررسی کنیم. هدف ایجاد یک Static Site Generator و در نهایت پابلیش خروجی استاتیک بر روی GitHub Pages است. روالی که در ادامه بررسی میکنیم صرفاً یک مثال از ترکیب این تکنولوژیها است و قاعدتاً روشهای سادهتر و سرراستتری نیز برای اینکار وجود دارد. به عنوان مثال میتوانید از Jekyll که یک SSG مبتنی بر Ruby است نیز استفاده کنید که GitHub Pages، به صورت پیشفرض از آن پشتیبانی میکند. در اینحالت به محض پوش کردن سایت بر روی ریپوزیتوری (با فرض اینکه این امکان را فعال کرده باشید) به صورت خودکار سایت بیلد شده و خروجی بر روی یک برنچ دیگر قرار خواهد گرفت و در نهایت برنچ بیلد شده توسط GitHub Pages میزبانی خواهد شد (البته امکان تغییر برنچ پیشفرض را نیز دارید). اما اگر بخواهیم کل فرآیند بیلد را به صورت سفارشی انجام دهیم، میتوانیم از GitHub Actions استفاده کنیم؛ یعنی مشابه کاری که Jekyll انجام میدهد. به محض پوش کردن محتوا، یک اسکریپت PowerShell برای اینکار فراخوانی شود و خروجی نهایی بر روی یک برنچ دیگر منتشر شود. خروجی نهایی این چنین قالبی خواهد داشت:
همانطور که در کد فوق مشاهده میکنید، تابع Copy-ToBuild فراخوانی شده است. درون این تابع، ابتدا لیاوتهای موردنیاز برای تولید صفحات HTML را درون یک متغییر با نام layouts قرار دادهایم. درون لیاوتها یکسری placeholder برای قرارگیری قسمتهای مختلف سایت تعریف کردهایم که قرار است توسط تابع عنوان شده، جایگزین شوند. در ادامه توسط تابع Get-Posts، تمامی مطالب درون دایرکتوری posts را واکشی کرده و برای هر کدام، صفحهی HTML معادل آن را تولید کردهایم. برای پارز کردن قسمت Front Matter هر بلاگپست نیز از پکیج powershell-yaml استفاده شدهاست. درون تابع Get-Posts باید هر مطلب را همراه با لیاوت اصلی سایت، به HTML تبدیل کنیم. بنابراین یکسری عملیات string replacement درون تابع عنوان شده انجام گرفته است. در نهایت، مطالب همراه با لیاوت مناسبی درون دایرکتوری build ذخیره خواهند شد و سپس یک لیست از مطالب پارز شده به عنوان خروجی تابع برگردانده خواهد شد. در نهایت درون تابع Copy-ToBuild، یکسری card برای نمایش آخرین مطالب بلاگ تهیه شده، سپس خروجی نهایی درون فایل index.html همراه با قالب اصلی ذخیره خواهد شد.
نکته: در اینجا از فونت آقای راستیکردار استفاده شده است؛ با آرزوی بهبودی و سلامتی ایشان.
ساختار پروژه
ساختاری که برای پروژه در نظر گرفتهام به صورت زیر است:
├── _layout │ ├── _footer.html │ ├── _header.html │ ├── _nav.html │ └── main.html ├── build ├── img ├── posts └── set-posts.ps1
- دایرکتوری layout_: درون این دایرکتوری، ساختار اصلی بلاگ را قرار دادهایم. در ادامه محتویات هر فایل را مشاهده خواهید کرد:
<!--main.html--> <!DOCTYPE html> <html dir="rtl"> {{header}} <body> {{nav}} <main> {{content}} </main> {{footer}} </body> <!--_header.html--> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{title}}</title> <link href="https://cdn.jsdelivr.net/gh/rastikerdar/samim-font@v4.0.5/dist/font-face.css" rel="stylesheet" type="text/css" /> <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> <style> * { } </style> </head> <!--_nav.html--> <header> <nav> <div> <div> <a href="#">بلاگ من</a> </div> <div> <ul> {{nav}} </ul> </div> </div> </nav> </header> <!--_footer.html--> <footer> <div> <div> <p> تمامی حقوق محفوظ است </p> </div> </div> </footer>
- دایرکتوری build: درون این دایرکتوری، خروجیهای HTML که قرار است توسط اسکریپت PowerShell جنریت شوند، قرار خواهند گرفت. این پوشه در واقع قرار است توسط GitHub Pages میزبانی شود.
- دایرکتوری img: درون این دایرکتوری، تصاویر مربوط به هر بلاگپست را قرار خواهیم داد.
- دایرکتوری posts: درون این دایرکتوری، مطالبمان را با فرمت Markdown، قرار خواهیم داد. به عنوان مثال در ادامه یک نمونه از آن را مشاهده خواهید کرد (در کد زیر از Front Matter برای اضافه کردن یکسری متادیتای موردنیاز که حین بیلد شدن ضروری هستند استفاده شدهاست)
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- # اولین پست من ## اولین پست من ### اولین پست من #### اولین پست من ##### اولین پست من ###### اولین پست من لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. چاپگرها و متون بلکه روزنامه و مجله در ستون و سطرآنچنان که لازم است و برای شرایط فعلی تکنولوژی مورد نیاز و کاربردهای متنوع با هدف بهبود ابزارهای کاربردی میباشد. کتابهای زیادی در شصت و سه درصد گذشته، حال و آینده شناخت فراوان جامعه و متخصصان را میطلبد تا با نرم افزارها شناخت بیشتری را برای طراحان رایانه ای علی الخصوص طراحان خلاقی و فرهنگ پیشرو در زبان فارسی ایجاد کرد. در این صورت میتوان امید داشت که تمام و دشواری موجود در ارائه راهکارها و شرایط سخت تایپ به پایان رسد وزمان مورد نیاز شامل حروفچینی دستاوردهای اصلی و جوابگوی سوالات پیوسته اهل دنیای موجود طراحی اساسا مورد استفاده قرار گیرد. <img src="/img/graphql.jpg"/>
همانطور که مشاهده میکنید، مسیر تصویر استفاده شده در بلاگپست، از دایرکتوری img خوانده شدهاست.
- فایل set_post.ps1: موتور اصلی جنریت کردن صفحات HTML این فایل میباشد. در ادامه محتویات آن را مشاهده خواهید کرد. سپس هر کدام از توابع استفاده شده را یکییکی توضیح خواهیم داد:
Function Get-Layouts { $headerLayout = Get-Content -Path ./_layout/_header.html -Raw $homeLayout = Get-Content -Path ./_layout/main.html -Raw $footerLayout = Get-Content -Path ./_layout/_footer.html -Raw Return @{ Header = $headerLayout Home = $homeLayout Footer = $footerLayout } } Function Get-PostFrontMatter($postContent) { $frontMatter = [regex]::Match($postContent, "(---(?:\r?\n(?!--|\s*$).*)*)\s*((?:\r?\n(?!---).*)*\r?\n---)") Return $frontMatter } Function Set-Headings($postHtml) { Return $postHtml -Replace '<h(\d) id="(.*)">', { $level = $_.Groups[1].Value $id = $_.Groups[2].Value $class = Switch ($level) { '1' { 'text-4xl font-bold mb-2' } '2' { 'text-3xl font-bold mb-2' } '3' { 'text-2xl font-bold mb-2' } '4' { 'text-xl font-bold mb-2' } '5' { 'text-lg font-bold mb-2' } '6' { 'text-base font-bold mb-2' } } "<h$level class='$class' id='$id'>" } } Function ConvertTo-Slug { [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$String ) process { $slug = $String -replace '[^\w\s-]', '' # remove non-word characters except hyphens $slug = $slug -replace '\s+', '-' # replace whitespace with a single hyphen $slug = $slug -replace '^-+|-+$', '' # remove leading/trailing hyphens $slug = $slug.ToLower() # convert to lowercase Write-Output $slug } } Function Get-Posts { $markdownPosts = Get-ChildItem -Path ./posts -Filter *.md $posts = @() Foreach ($post in $markdownPosts) { $postContent = Get-Content -Path $post.FullName -Raw $frontMatter = Get-PostFrontMatter $postContent $frontMatterObject = $frontMatter | ConvertFrom-Yaml $slug = $frontMatterObject.slug ?? (ConvertTo-Slug "$($frontMatterObject.date)-$($frontMatterObject.title)") $body = $postContent.Replace($frontMatter.Value, "") | ConvertFrom-Markdown $postHtml = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{title}}', $frontMatterObject.title ` -replace '{{nav}}', (Set-Navs) ` -replace '{{content}}', $body.Html ` -replace '{{footer}}', $layouts.Footer $postHtml = Set-Headings $postHtml $postHtml | Out-File -FilePath ./build/$slug.html $posts += @{ title = $frontMatterObject.title slug = $slug excerpt = $frontMatterObject.excerpt date = $frontMatterObject.date author = $frontMatterObject.author body = $body.Html } } Return $posts } Function Set-Archive { $posts = Get-Posts $archive = @() $archive = @" <ul> $($posts | ForEach-Object { "<li><a href='$($_.slug).html'>$($_.title)</a></li>" }) </ul> "@ Return $archive -join "`r`n" } Function Copy-ToBuild { $layouts = Get-Layouts $latestPosts = Get-Posts | ForEach-Object { @" <div> <img src="https://via.placeholder.com/300x200" alt="$($_.title)"> <h2>$($_.title)</h2> <p>$($_.excerpt)</p> <a href="$($_.slug).html">ادامه مطلب</a> </div> "@ } $homeLayout = $layouts.Home -replace '{{header}}', $layouts.Header ` -replace '{{nav}}', (Set-Navs) ` -replace '{{title}}', 'بلاگ من' ` -replace '{{content}}', ('<div>' + $latestPosts + '</div>') ` -replace '{{footer}}', $layouts.Footer $homeLayout | Out-File -FilePath ./build/index.html Copy-Item -Path ./img -Destination ./build -Recurse -Force } Function Set-Navs { $navs = @( @{ title = "صفحه اصلی" url = "/sample" }, @{ title = "درباره ما" url = "/sample/about.html" }, @{ title = "تماس با ما" url = "/sample/contact.html" } ) $navLayout = Get-Content -Path ./_layout/_nav.html -Raw $navLayout -replace '{{nav}}', ($navs | ForEach-Object { "<li><a href=""$($_.url)""text-gray-700 hover:text-gray-800 m-2"">$($_.title)</a></li>" }) } Copy-ToBuild
ایجاد GitHub Actions Workflow
در ادامه برای ساختن workflow نهایی باید با کمک GitHub Actions، اسکریپت PowerShellی را که ساختیم، اجرا کنیم. این اسکریپت ابتدا پروژه را clone کرده، سپس وابستگی موردنیاز را نصب کرده و در نهایت اسکریپت را اجرا خواهد کرد:
name: Deploy static content to Pages on: push: branches: - main workflow_dispatch: permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: false jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Install powershell-yaml module shell: pwsh run: | Set-PSRepository PSGallery -InstallationPolicy Trusted Install-Module powershell-yaml -ErrorAction Stop - name: Setup Pages uses: actions/configure-pages@v3 - name: Build Static Site shell: pwsh run: | . ./set-posts.ps1 - name: Upload Static Site Artifact uses: actions/upload-pages-artifact@v1 with: path: build - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v2
نحوه انتشار یک مطلب جدید
- درون دایرکتوری posts، مطلب موردنظر را به همراه Front Matter زیر ایجاد کرده و سپس محتویات مطلب را بعد از آن وارد کنید:
--- title: اولین پست من slug: hello date: 2023-04-26 author: سیروان عفیفی tags: [tag1, tag2, tag3] excerpt: این یک پست تستی است در مورد اینکه چطور میتوانیم از این قالب استفاده کنیم --- content
- تغییرات ایجاد شده را کامیت و سپس پوش کنید. به محض پوش کردن تغییرات، GitHub Actions پروسه بیلد را انجام خواهد داد و بلافاصله میتوانید تغییرات را مشاهده نمائید.
نکته: قابلیت GitHub Pages به صورت پیشفرض فعال نیست. برای فعال کردن آن ابتدا باید به قسمت تنظیمات ریپوزیتوری GitHubتان مراجعه کرده و سپس از تب Pages، فیلد Source را بر روی GitHub Actions قرار دهید:
بنابراین بعد از پوش کردن تغییرات workflowایی که ایجاد کردیم توسط Source شناسایی خواهد شد و سپس وبسایت از طریق آدرس https://<username>.github.io/<repository> در دسترس قرار خواهد گرفت:
کدهای این مطلب را میتوانید از اینجا دریافت کنید.
اشتراکها
کتابخانه mresize
Event based jQuery element resize
The plugin does not use any kind of timer(s) to detect size changes. It uses the resize event on (invincible) iframe(s) which makes it perform much better than other solutions which use timers to poll element size. The script detects size changes made from JS, CSS, animations etc. and it works on any element able to contain other elements (e.g. div, p, li etc.). To use it on other inline elements (e.g. images) you need to add a wrapper and call the event on it. Demo
نظرات مطالب
بازگردانی پایگاه داده بدون فایل لاگ
سه راه حل اینجا ذکر شده . آزمایش کنید کدام یکی برای وضعیت شما مفید است.