اعتبارسنجی در Angular 2 توسط JWT
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: هشت دقیقه

در مقالاتی که در سایت منتشر شده‌است، آشنایی و همچنین نحوه پیاده سازی Json Web Token را فرا گرفتیم. در اینجا میخواهیم با استفاده از توکن تولید شده، برنامه‌های Angular2 یا هر نوع فریمورک spa را با آن ارتباط دهیم. در سایت جاری قبلا در مورد نحوه پیاده سازی آن صحبت شده‌است و میخواهیم از آن در یک پروژه Angular 2 صحبت کنیم.
پروژه دات نت را از طریق این آدرس دریافت کرده  و آن را در حالت اجرا بگذارید.

سپس یک سرویس جدید را در پروژه Angular خود اجرا کنید و متد زیر را به آن اضافه کنید:
login():any{
    let urlSearchParams = new URLSearchParams();
    urlSearchParams.append('username', 'Vahid');
    urlSearchParams.append('password', '1234');
    urlSearchParams.append('grant_type', 'password');
    let body = urlSearchParams.toString();

    let headers = new Headers();
    headers.append('Content-Type', 'application/x-www-form-urlencoded');
    return this._http.post('http://localhost:9577/login', body, { headers: headers })
        .map(res => res.json());
}
در متد بالا ابتدا از کلاس  URLSearchParams  یک شیء میسازیم. این کلاس در ماژول Http قرار دارد و وظیفه آن تبدیل پارامترهای داده شده به شکل زیر میباشد:
username=vahid&password=1234
دلیل اینکه ما در اینجا همانند jquery از JSON.stringify استفاده نکردیم این است که در حالتیکه خصوصیت Content-Type هدر را بر روی application/x-wwww-form-urlencoded قرار میدهیم، شیء ایجاد شده از کلاس Http در اینجا، کل عبارت را تبدیل به کلید بدون مقدار میکند و باعث میشود که پارامترها به درستی به سمت Owin هدایت نشوند. بعد از آن هدری که ذکر شد را در درخواست قرار داده و آن را ارسال میکنیم.
از آنجاکه پروژه انگیولار ساخته شده در آدرسی دیگر و جدا از پروژه‌ی دات نت قرار دارد و همینطور که می‌بینید آدرس کامل آن را به این دلیل قرار دادم، ممکن است خطای CORS را دریافت کنید که میتوانید آن را با نصب یک بسته نیوگتی حل کنید.
 
همچنین برای تست و انجام یک عمل مرتبط با توکن، متد زیر را هم به آن اضافه می‌کنیم:
ApiAdmin(token:any):any{
    let headers = new Headers();
    headers.append('Authorization', 'bearer ' + token);
    return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers })
        .map(res => res.json());
}
در اینجا با استفاده از روش Http Bearer که در اعتبارسنجی در سطح OAuth کاربرد زیادی دارد، توکن تولید شده را که در پارامتر ورودی متد دریافت کرده‌ایم، به هدر اضافه کرده و آن را ارسال میکنیم.

کد کل سرویس،  الان به شکل زیر شده است:
import { Http, Headers, URLSearchParams } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from "rxjs/Observable";
import "rxjs/Rx";

@Injectable()
export class MemberShipService {
    constructor(private _http: Http) { }

    ApiAdmin(token: any): any {
        let headers = new Headers();
        headers.append('Authorization', 'bearer ' + token);
        return this._http.get('http://localhost:9577/api/MyProtectedApi', { headers: headers })
            .map(res => res.json());
    }

    login(): any {
        let urlSearchParams = new URLSearchParams();
        urlSearchParams.append('username', 'Vahid');
        urlSearchParams.append('password', '1234');
        urlSearchParams.append('grant_type', 'password');
        let body = urlSearchParams.toString();

        let headers = new Headers();
        headers.append('Content-Type', 'application/x-www-form-urlencoded');
        return this._http.post('http://localhost:9577/login', body, { headers: headers })
            .map(res => res.json());
    }
}
سپس سرویس ساخته شده را در ngModule معرفی میکنیم. در کامپوننت هدف دو دکمه را قرار میدهیم؛ یکی برای لاگین و دیگری برای دریافت اطلاعاتی که نیاز به اعتبار سنجی دارد. رویداد کلیک دکمه‌ها را به متدهای زیر متصل میکنیم:
Login(){
    this._service.login()
        .subscribe(res => {
            localStorage.setItem('access_token', res.access_token);
            localStorage.setItem('refresh_token', res.refresh_token);
        }
        , error => console.log(error));
}
در اینجا ما اطلاعات لاگین را به سمت سرور فرستاده و در صورتیکه پاسخ صحیحی را دریافت کردیم، متد Subscribe اجرا خواهد شد و مقادیر دریافتی را باید به نحوی در سیستم و بین کامپوننت‌های مختلف، ذخیره و نگهداری کنیم. در اینجا من از روش Local Storage که در سایت جاری قبلا به آن پرداخته شده‌است، استفاده میکنم. access_token و refresh_token جاری و اطلاعات دیگری را چون رول‌ها و ... هر یک را با یک کلید ذخیره میکنیم تا بعدا به آن دسترسی داشته باشیم.
در متد بعدی که قرار است توسط آن صحت اعتبارسنجی مورد آزامایش قرار بگیرد، کدهای زیر را مینویسیم:
CallApi()
{
    let item = localStorage.getItem("access_token");
    if (item == null) {
        alert('please Login First.');
        return;
    }
    this._service.ApiAdmin(item)
        .subscribe(res => {
            alert(res);
        }
        , error => console.log(error));
}
در اینجا ابتدا توکن ذخیره شده را از Local Storage درخواست میکنیم. اگر نتیجه این درخواست نال باشد، به این معنی است که کاربر قبلا لاگین نکرده است؛ در غیر این صورت درخواست را با توکن دریافتی میفرستیم و پاسخ موفق آن را در یک alert می‌بینیم. در صورتیکه توکن ما اعتبار نداشته باشد، خطای بازگشتی در کنسول نمایش خواهد یافت.


اعتبارسنجی در مسیریابی


یکی از روش‌هایی که انگیولار باید بررسی کند این است که کاربر جاری با توجه به نقش‌هایی که ممکن است داشته باشد، یا اعتبار سنجی شده است یا خیر و میزان دسترسی او به کامپوننت‌ها، باید مشخص گردد. برای این مورد خصوصیتی به مسیریابی اضافه شده است به نام CanActivate که از شما یک کلاس را که در آن اینترفیس CanActivate پیاده سازی شده است، درخواست میکند. در اینجا ما یک Guard را با نام LoginGuard ایجاد میکنیم، تا تنها کاربرانی که لاگین کرده‌اند، به این صفحه دسترسی داشته باشند:
import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';

@Injectable()
export class LoginGuard implements CanActivate {
    constructor() { }

    canActivate() {
        let item = localStorage.getItem('access_token');
        if (item == null)
            return false;
        return true;
    }
}
در متد Activate باید خروجی boolean بازگردد. در صورتیکه true بازگشت داده شود، عملیات هدایت به کامپوننت مقصد با موفقیت انجام خواهد شد. در صورتیکه خلاف این موضوع اتفاق بیفتد، کامپوننت هدف نمایش داده نمیشود. در اینجا بررسی کرده‌ایم که اگر توکن مورد نظر در localStorage  قرار داشت، به معنی این است که لاگین شده‌است. ولی این موضوع روشن است که در یک مثال واقعی باید صحت این توکن بررسی شود. این Guard در واقع یک سرویس است که باید در فایل ngModule معرفی شده و آن را در فایل مسیریابی به شکل زیر استفاده کنیم:
 { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard]}
در معرفی یک مسیر به فایل مسیریابی، خصوصیاتی چون مسیری که نوشته میشود و کامپوننت مربوط به آن مسیر ذکر می‌شود. خصوصیت دیگر، CanActivate است که به آن کلاس LoginGuard را معرفی مکنیم. در اینجا شما میتواند به هر تعداد گارد را که دوست دارید، معرفی کنید ولی به یاد داشته باشید که اگر یکی از آن‌ها در اعتبارسنجی ناموفق باشد، دیگر کامپوننت جاری در دسترس نخواهد بود. به معنای دیگر تمامی گاردها باید نتیجه‌ی true را بازگردانند تا دسترسی به کامپوننت هدف ممکن شود.
 { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard]}

در یک گارد ممکن است کاربر لاگین نکرده باشد و شما نیاز دارید او را به صفحه لاگین هدایت کنید. در این صورت گارد لاگین را به شکل زیر بازنویسی میکنیم:
import { Router } from '@angular/router';
import { CanActivate } from '@angular/router';
import { Injectable } from '@angular/core';

@Injectable()
export class LoginGuard implements CanActivate {

    constructor(public router: Router) { }

    canActivate() {
        let item = localStorage.getItem('access_token');
        if (item == null) {
            this.router.navigate(['/app']);
            return false;
        }
        return true;
    }
}
در اینجا ما از سازنده کلاس، شیءایی از نوع Router را ساختیم که امکانات و متدهای خوبی را در باب مسیریابی به همراه دارد و از آن برای انتقال به مسیری دیگر که میتواند صفحه لاگین باشد، استفاده کردیم.

همچنین گارد میتواند اطلاعات مسیر درخواست شده را خوانده و بر اساس آن به شما پاسخ بدهد. به عنوان مثال پارامترهایی را که به سمت مسیر مورد نظر هدایت میشود، بخواند و بر اساس آن، تصمیم گیری کند. به عنوان نمونه کاربر به مسیر ذکرشده دسترسی دارد، ولی با Id که در مسیر ارسال کرده است، دسترسی ندارد:
import { Router } from '@angular/router';
import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';

@Injectable()
export class SecondGuard implements CanActivate {

    constructor(public router: Router) { }

    canActivate(route: ActivatedRouteSnapshot) {
        let id = route.params['id'];
        if (id == 1) {
            return false;
        }
        return true;
    }
}

متد CanActivate میتواند پارامترهای زیادی را به عنوان ورودی دریافت کند که یکی از آن‌ها ActivatedRouteSnapshot است که اطلاعات خوب و مفیدی را در مورد مسیر ارسال شده از طرف کاربر دارد و با استفاده از آن در اینجا میتوانیم پارامترهای ارسالی را دریافت کنیم. در اینجا ذکر کرده‌ایم که اگر پارامتر Id که بر مبنای مسیر زیر است، برابر 1 بود، مقدار برگشتی برابر false خواهد بود و دسترسی به کامپوننت در اینجا ممکن نخواهد بود.
 { path: 'component-one/:id', component: Component1Component,canActivate:[LoginGuard,SecondGuard] }
  • #
    ‫۷ سال و ۴ ماه قبل، شنبه ۹ اردیبهشت ۱۳۹۶، ساعت ۱۴:۱۳
    ممنون بابت مطلب
    با توجه به اینکه شرط لاگین بودن بررسی وجود access_token در localStorage است در صورتیکه توکن منقضی شود همچنان access_token  در localStorage دارای مقدار می‌باشد پس کاربر در حالت لاگین باقی می‌ماند.
    برای این مسئله راهکاری هست؟
    • #
      ‫۷ سال و ۴ ماه قبل، شنبه ۹ اردیبهشت ۱۳۹۶، ساعت ۱۴:۲۷
      - مهم نیست. چون نهایتا با اولین درخواستی که به سمت سرور ارسال می‌کند، برگشت خواهد خورد و سرور این درخواست را پردازش نمی‌کند.
      - می‌توان از متد tokenNotExpired در ابتدای canActivate برای بررسی سمت کلاینت هم استفاده کرد:
      if (tokenNotExpired('currentUser')) return true;
  • #
    ‫۷ سال و ۱ ماه قبل، یکشنبه ۲۲ مرداد ۱۳۹۶، ساعت ۲۲:۰۰
    با عرض سلام؛ من قبلا تو پروژه ام برای رفع خطای CORS از روش   
     public static void Register(HttpConfiguration config)
    {
                var cors = new EnableCorsAttribute("*", "*", "*");
                config.EnableCors(cors);
                config.Formatters.JsonFormatter.SupportedMediaTypes
                                            .Add(new MediaTypeHeaderValue("application/json")); 
    }
    استفاده میکردم و الان برای رفع خطای CORS (برای لاگین)  راه حلی که شما در این مطلب گفتین رو پیاده سازی کردم. که حالا دوباره روش قبلیم خطای  Cross-Origin رو میده. می‌خواستم بدونم چطوری میشه این دو روش رو باهم بدون خطا پیاده سازی کرد
    • #
      ‫۷ سال و ۱ ماه قبل، یکشنبه ۲۲ مرداد ۱۳۹۶، ساعت ۲۲:۲۹
      این روش فعالسازی CORS با تمام نگارش‌های ASP.NET سازگار است و مستقیما توسط IIS مدیریت می‌شود:
      <system.webServer>
          <modules runAllManagedModulesForAllRequests="true"></modules>
          <httpProtocol>
            <customHeaders>
              <add name="Access-Control-Allow-Origin" value="*" />
              <add name="Access-Control-Allow-Headers" value="*" />
              <add name="Access-Control-Allow-Credentials" value="true" />
            </customHeaders>
          </httpProtocol>
          <handlers>
            <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
            <!--<remove name="OPTIONSVerbHandler" />-->
            <remove name="TRACEVerbHandler" />
            <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
          </handlers>
        </system.webServer>
      • #
        ‫۵ سال و ۴ ماه قبل، سه‌شنبه ۲۴ اردیبهشت ۱۳۹۸، ساعت ۰۶:۴۳
        با سلام
         بنده در پروژه Angular 7 ا برای احراز هویت نیز از Owin-JWT استفاده می‌کنم. سمت سرور نیز WebAPI  می‌باشد که در IIS هاست شده است. با استفاده از این روش متاسفانه مشکل حل نشد. مجبور شدم از Attribute به شکل زیر بروی Controller‌ها استفاده کنم :
           [EnableCors(origins: "*", headers: "*", methods: "*")]
            public class ApiSettingsController : ApiBase
            {
            }

         بر روی Controller‌های WebAPI استفاده کردم و به سختی بالاخره مشکل حل شد. همچنین برای Owin به دلیل اینکه Controller خاصی نداره از روش زیر  استفاده کرده ام:
         public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
                {
                     context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
                }

        تا اینجا مشکل حل شده اما برای Refresh Token هر کاری میکنم خطای CORS رو میگیرم. شکل کار نیز به شکل زیر است:
        public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
                {
                    context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
                }
        متاسفانه خیلی وقت گرفته تا الان اما مشکل حل نمیشه. در صورت امکان راهنمایی بفرمایید. 
        درصورت نیاز کدهای بیشتری قرار بدم اینجا.
        باتشکر

  • #
    ‫۴ سال و ۵ ماه قبل، شنبه ۲۳ فروردین ۱۳۹۹، ساعت ۰۰:۱۵
    با سلام؛ در معماری پیاز، ساخت jwt، وظیفه کدوم بخش هست؟ کلاینت؟ دامین سرویس؟ اپلیکیشن سرویس ؟ اینفرا استراکچر؟
    • #
      ‫۴ سال و ۵ ماه قبل، شنبه ۲۳ فروردین ۱۳۹۹، ساعت ۰۳:۳۵
      سلام؛ فکر میکنم قاعده‌ی هر فیچری توی این معماری اینجوریه: تعریف اینترفیس‌ها که در واقع تعریف اون فیچر میشه، در لایه‌ی اپلیکیشن اتفاق میفته. پیاده سازی اون اینترفیس یا فیچر توی اینفراستراکچر اتفاق میفته و استفاده ازش هم توی سرویس‌های لایه اپلیکیشن هست.
    • #
      ‫۴ سال و ۵ ماه قبل، شنبه ۲۳ فروردین ۱۳۹۹، ساعت ۰۶:۱۲
      با دریافت قالب زیر و ایجاد سولوشن جدید ، جایگاه هر فایل با یک نگاه مشخص میشه.
      dotnet new --install Aro.SolutionTemplates::1.0.0