توسعه برنامه‌های وب با ASP.NET Core 2.0 و React (بخش اول)

در این مقاله، شما یک برنامه وب مبتنی بر ASP.NET Core 2.0 و React خواهید ساخت. برای ایجاد ویژگی مدیریت هویت (identity)، این برنامه را با Auth0 ادغام می‌کنید. در بخش اول این مجموعه، قصد استفاده از ASP.NET Core 2.0 برای توسعه APIهای برنامه خود را دارید.

توسعه برنامه‌های وب با ASP.NET Core 2.0 و React (بخش اول)

کد نهایی را می‌توانید در GitHub بیابید.

راه‌اندازی برنامه ASP.NET Core

برنامه‌ای که می‌خواهید پیاده‌سازی کنید، به کاربران اجازه می‌دهد در فهرست کتاب‌های آنلاین بگردند. به دنبال رویکرد توسعه API-First، با ایجاد ASP.NET Core 2.0 Web API شروع به ساخت برنامه خود خواهید کرد. برای انجام این کار شما دو گزینه دارید: اول اینکه می‌توانید برنامه خود را با ویژوال استودیو ایجاد کنید، دوم اینکه می‌توانید برنامه را از خط فرمان ایجاد کنید.

ایجاد پروژه با ویژوال استودیو

اگر از ویژوال استودیو استفاده می‌کنید، می‌توانید پروژه را با قالب ASP.NET Core Web App بسازید، همان‌طور که در تصویر زیر نشان داده شده است:

بعد از اینکه قالب پروژه ASP.NET Core Web App را انتخاب کردید، باید نوع برنامه ASP.NET ای که می‌خواهید بسازید را مشخص کنید. در اینجا، نوع برنامه را Web API انتخاب کنید، همان‌طور که در تصویر زیر است:

اطمینان حاصل کنید که نوعی از احراز هویت را انتخاب نکرده‌اید، زیرا می‌خواهید برنامه را با Auth0 ادغام کنید.

ایجاد برنامه از خط فرمان

اگر ترجیح می‌دهید از خط فرمان استفاده کنید، می‌توانید برنامه خود را با نوشتن دستور زیر بسازید:

dotnet new webapi -n API-Auth0

این دستور یک پوشه ASP.NET Web API ایجاد می کند، که پروژه شما با نام API-Auth0 درون پوشه جاری است.

چه از ویژوال استودیو استفاده کنید چه خط فرمان، در هر حال نتیجه یکی است. یعنی بعد از انجام این مراحل یک برنامه ASP.NET Core 2 Web API دریافت خواهید کرد.

ایجاد کنترلر Books در ASP.NET Core 2.0

حالا که پروژه را ایجاد کردید، می‌توانید آن را برای ارائه قابلیت‌های دلخواه بازسازی کنید. اولین کاری که انجام می‌دهید این است که فایل ValuesController.cs درون پوشه Controllers را حذف کنید. شما در این برنامه به این کنترلر نیاز ندارید.

بعد از حذف آن، می‌توانید فایل BooksController.cs را در همان پوشه، همانند کد زیر، بسازید:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace APIAuth0.Controllers
{
    [Route("api/[controller]")]
    public class BooksController : Controller
    {
        [HttpGet]
        public IEnumerable<Book> Get()
        {
            var currentUser = HttpContext.User;
            var resultBookList = new Book[] {
                new Book { Author = "Ray Bradbury", Title = "Fahrenheit 451", AgeRestriction = false },
                new Book { Author = "Gabriel García Márquez", Title = "One Hundred years of Solitude", AgeRestriction = false },
                new Book { Author = "George Orwell", Title = "1984", AgeRestriction = false },
                new Book { Author = "Anais Nin", Title = "Delta of Venus", AgeRestriction = true }
            };

            return resultBookList;
        }

        public class Book
        {
            public string Author { get; set; }
            public string Title { get; set; }
            public bool AgeRestriction { get; set; }
        }
    }
}

در اینجا، شما یک Web API برای بازگشت لیست کتاب‌ها تعریف کرده‌اید. برای سادگی، لیست کتاب‌ها را در یک آرایه ذخیره می‌کنید. با این حال، در موارد واقعی، باید در یک پایگاه داده پایدار ذخیره شود. URL مربوط به API، api/books/ خواهد بود و هر HTTP کلاینتی می‌تواند لیست کتاب‌ها را توسط یک درخواست HTTP GET ساده دریافت کند.

# run the application in the background
dotnet run &

# issue a get request
curl -D - http://localhost:5000/api/books

البته شما نمی‌خواهید هر کلاینتی بدون فرآیند احراز هویت بتواند به کتاب‌فروشی شما دسترسی پیدا کند. شما می‌خواهید فقط کلاینت‌های مجاز بتوانند لیست کتاب‌های مدیریت شده توسط برنامه شما را دریافت کنند. این جایی است که Auth0 به شما کمک می‌کند: مجموعه‌ای از راه‌حل‌های شناسایی را فراهم می‌کند که امنیت درون برنامه را برقرار می‌سازد.

ادغام ASP.NET Core 2.0 با Auth0

در اولین قدم، نیاز به یک اکانت Auth0 دارید. اگر هنوز عضو نیستید، می‌توانید از اینجا به صورت رایگان ثبت نام کنید.

در طول ثبت‌نام، باید نام دامنه خود، منطقه سرویس هاست و یک سری جزئیات در مورد شرکت و خودتان را ارائه دهید. نام دامنه بسیار مهم است، زیرا بخش ریشه (روت) API endpointها که توسط Auth0 به کلاینت‌های مجاز شما نمایش داده می‌شوند را مشخص می‌کند. وقتی نام دامنه را ارائه می‌دهید، دیگر نمی‌توانید آن را تغییر دهید. با این حال، می‌توانید دامنه‌های بسیاری که نیاز دارید را ایجاد کنید. وقتی مرحله ثبت‌نام تکمیل می شود، می‌توانید به داشبورد Auth0 دسترسی داشته باشید.

ایجاد Auth0 API

همان‌طور که یک backend API ایجاد کردید که کاربران را قادر می‌سازد در کتاب‌فروشی آنلاین گشت و گذار کنند، باید یک Auth0 API برای نمایش backend خود ایجاد کنید. برای انجام این کار، به بخش APIهای داشبورد Auth0 بروید و روی دکمه Create API کلیک کنید. پس از آن سه سؤال از شما پرسیده می‌شود:

1. نام (Name) API شما، می‌توانید آن را روی Online Bookstore تنظیم کنید.

2. شناسه‌ای (Identifier) برای API شما، می‌توانید آن را با https://onlinebookstore.mycompany.com تنظیم کنید.

3. و تنظیمات الگوریتم (Signing Algorithm)، در این فیلد می‌توانید RS256 را انتخاب کنید.

وقتی این فرم را کامل کردید، می‌توانید روی دکمه Create کلیک کنید.

ایجاد برنامه Auth0

هدف شما این است که سطح دسترسی API را توسط مجوز (authorizing) فقط برای کلاینت‌های معتبر کنترل کنید. برای انجام این کار، همچنین باید پیکربندی application روی Auth0 را داشته باشید. معمولا، نیاز به ایجاد یک برنامه جدید برای نمایش برنامه front-end خود دارید. نوع برنامه front-end به شما کمک می‌کند تا برای انتخاب نوع برنامه Auth0 تصمیم بگیرید. با این حال، چون در بخش اول هنوز front-end را ایجاد نکرده‌اید، می‌توانید از برنامه‌ای که به طور خودکار برای Auth0 API شما ایجاد شده بود استفاده کنید.

اگر Online Bookstore را به عنوان نام API خود انتخاب کرده‌اید، بنابراین در بخش Application داشبورد Auth0 برنامه‌ای به نام Online Bookstore (برنامه تست) مشاهده خواهید کرد. روی این برنامه کلیک کنید و به تب Settings بروید. در این تب، سه ویژگیی که نیاز دارید را مشاهده خواهید کرد:

1. دامنه Auth0 خود (Domain)

2. کلید شناسه کلاینت (Client ID)

3. کلید رمزی کلاینت (Client Secret)

شما به زودی از این سه مقدار استفاده خواهید کرد.

پیکربندی Auth0 روی برنامه‌های ASP.NET Core

بعد از ایجاد Auth0 API، باید برنامه خود را تغییر دهید تا مدیریت شناسایی را به Auth0 بدهید. در اولین قدم، باید بخش Auth0 را در فایل پیکربندی appsettings.json اضافه کنید، همان‌طور که در زیر نشان داده شده است:

{
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "Auth0": {
    "Authority": "<YOUR_AUTH0_DOMAIN>",
    "Audience": "<YOUR_AUTH0_AUDIENCE>"
  }
}

باید <YOUR_AUTH0_DOMAIN> و <YOUR_AUTH0_AUDIENCE> را با مقادیری که در قسمت‌های قبلی تعریف کرده‌اید جایگزین کنید. مثلا اگر دامنه را https://dotnet2-react.auth0.com تعریف کرده باشید، همان چیزی است که باید در قسمت <YOUR_AUTH0_DOMAIN> استفاده کنید. در قسمت <YOUR_AUTH0_AUDIENCE> نیز مقداری که برای Identifier در Auth0 API تعریف کردید (مثلا https://onlinebookstore.mycompany.com)  را استفاده خواهید کرد.

سپس باید متد ConfigureServices() در فایل Startup.cs را تغییر دهید، که همانند زیر است:

using Microsoft.AspNetCore.Authentication.JwtBearer;

// ... other using statements
// ... namespace definition
// ... class definition
// ... etc

public void ConfigureServices(IServiceCollection services) {
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
        options.Authority = Configuration["Auth0:Authority"];
        options.Audience = Configuration["Auth0:Audience"];
    });

    services.AddMvc();
}

// ... etc

همان‌طور که می‌بینید، با تعیین ساختار حامل JWT و ارائه مقادیر authority و  audience گرفته شده از فایل پیکربندی appsetting.json، سرویس احراز هویت را اضافه کردید.

بعد از آن، باید یک فراخوانی از app.UseAuthentication() در بدنه متد Configure() اضافه کنید، همانند دستور زیر:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseAuthentication();

    app.UseMvc();
}

پس از فعال کردن پشتیبانی از احراز هویت، می‌توانید دسترسی عمومی به API خود را توسط افزودن اتربیوت Authorize به endpoint در فایل BooksController.cs مسدود کنید:

[HttpGet, Authorize]
public IEnumerable<Book> Get()
{
    var currentUser = HttpContext.User;
    var resultBookList = new Book[] {
        new Book { Author = "Ray Bradbury", Title = "Fahrenheit 451", AgeRestriction = false },
        new Book { Author = "Gabriel García Márquez", Title = "One Hundred years of Solitude", AgeRestriction = false },
        new Book { Author = "George Orwell", Title = "1984", AgeRestriction = false },
        new Book { Author = "Anais Nin", Title = "Delta of Venus", AgeRestriction = true }
    };

    return resultBookList;
}

حالا اگر سعی کنید به endpoint مربوط به api/books/ دسترسی پیدا کنید، کد وضعیت 401 HTTP را دریافت خواهید کرد (به این معنی که شما مجوز دسترسی را ندارید). می‌توانید با استفاده از هر HTTP کلاینتی، مانند مرورگر، curl یا Postman آن را بررسی کنید.

# run the application in the background
dotnet run &

# issue a get request
curl -D - http://localhost:5000/api/books

آخرین دستور پاسخی همانند زیر را تولید می‌کند:

# HTTP/1.1 401 Unauthorized
# Date: Wed, 24 Jan 2018 16:28:09 GMT
# Server: Kestrel
# Content-Length: 0
# WWW-Authenticate: Bearer

دریافت توکن (رمز) دسترسی

حالا که endpointها را با Auth0 امن کردید، به شما یاد خواهیم داد که چگونه دستگاهی که می‌تواند لیست کتاب‌ها را دوباره دریافت کند اعتبارسنجی کنید. می‌توانید از هر HTTP کلاینتی استفاده کنید، اما در این بخش، استفاده از curl را مشاهده خواهید کرد.

در مرحله اول یک توکن مجوز را از Auth0 دریافت می‌کنید. می‌توانید این کار را با ارسال یک درخواست POST به oauth/token/ دامنه Auth0 خود انجام دهید، مانند دستور زیر:

AUTH0_CLIENT_ID=<YOUR_AUTH0_CLIENT_ID>
AUTH0_CLIENT_SECRET=<YOUR_AUTH0_CLIENT_SECRET>
AUTH0_AUDIENCE=<YOUR_AUTH0_AUDIENCE>
AUTH0_DOMAIN=<YOUR_AUTH0_DOMAIN>

curl -X POST -H 'content-type: application/json' -d '{
    "client_id": "'$AUTH0_CLIENT_ID'",
    "client_secret": "'$AUTH0_CLIENT_SECRET'",
    "audience": "'$AUTH0_AUDIENCE'",
    "grant_type":"client_credentials"
}' https://$AUTH0_DOMAIN/oauth/token

توجه داشته باشید که باید <YOUR_AUTH0_CLIENT_ID>، <YOUR_AUTH0_CLIENT_SECRET>، <YOUR_AUTH0_AUDIENCE> و <YOUR_AUTH0_DOMAIN> را در دستورات بالا با مقادیر مربوط به Auth0 API و برنامه‌ای که قبلا ایجاد کردید، جایگزین کنید.

پاسخ به این درخواست چیزی شبیه به دستور زیر می‌شود:

{
    "access_token": "eyJ0eXAiO...pJmNFPA",
    "expires_in": 86400,
    "token_type": "Bearer"
}

حالا می‌توانید از access_tokenای که از Auth0 دریافت کردید تا لیست کتاب‌ها را درخواست کنید، استفاده کنید، مانند دستور زیر:

ACCESS_TOKEN=eyJ0eXAiO...pJmNFPA

curl -H 'Authorization: Bearer '$ACCESS_TOKEN -D - http://localhost:5000/api/books

این درخواست پاسخ زیر را تولید می‌کند:


# HTTP/1.1 200 OK
# Date: Wed, 24 Jan 2018 17:35:26 GMT
# Content-Type: application/json; charset=utf-8
# Server: Kestrel
# Transfer-Encoding: chunked

# [{"author":"Ray Bradbury","title":"Fahrenheit 451","ageRestriction":false},{"author":"Gabriel García Márquez","title":"One Hundred years of Solitude","ageRestriction":false},{"author":"George Orwell","title":"1984","ageRestriction":false},{"author":"Anais Nin","title":"Delta of Venus","ageRestriction":true}]

ایجاد ادغام تست‌ها به عنوان دستگاه برای برنامه‌های دستگاه (Machine Application)

تست انجام شده با curl در بخش قبلی فقط باید بررسی کند که داده‌های پیکربندی Auth0 و API اجرا شده برنامه ما به خوبی با هم کار می‌کنند. در حقیقت نوع برنامه‌ای که Auth0 به صورت خودکار ایجاد می‌کند دستگاهی برای Machine Application بود. این نوع برنامه‌ها برای سرور در برابر سرور یا تعامل بدون نظارت تعیین شده‌اند. هرگز نباید Client Secret را در سمت کلاینت ذخیره کنید، زیرا امنیت برنامه را به خطر می‌اندازد.

پیاده‌سازی تست یکپارچه‌سازی ممکن است موردی باشد که می‌توانید از Client Secret استفاده کنید، زیرا این اطلاعات به کلاینت نمایش داده نخواهند شد اما در محیط توسعه باقی می‌مانند.

اکنون زمان آن رسیده است که تست پکپارچه‌سازی را بنویسید که درخواست را برای گرفتن لیست کتاب‌ها بدون استفاده از فایل‌های دسترسی توکن تأیید کند.

[Fact]
public async Task UnAuthorizedAccess()
{
    var response = await _client.GetAsync("/api/books");

    Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

این آزمون ساده بررسی می‌کند که چنین درخواستی یک کد وضعیت Unauthorized HTTP401 را دریافت کند. بعد از آن می‌توانید بررسی کنید که داده پیکربندی که در فایل appsetting.json گذاشته‌اید، به شما اجازه می‌دهد یک توکن دسترسی معتبر دریافت کنید:

[Fact]
public async Task TestGetToken()
{
    var auth0Client = new HttpClient();
    var bodyString = $@"{{""client_id"":""{_configuration["Auth0:ClientId"]}"", ""client_secret"":""{_configuration["Auth0:ClientSecret"]}"", ""audience"":""{_configuration["Auth0:Audience"]}"", ""grant_type"":""client_credentials""}}";
    var response = await auth0Client.PostAsync($"{_configuration["Auth0:Authority"]}oauth/token", new StringContent(bodyString, Encoding.UTF8, "application/json"));

    Assert.Equal(HttpStatusCode.OK, response.StatusCode);

    var responseString = await response.Content.ReadAsStringAsync();
    var responseJson = JObject.Parse(responseString);
    Assert.NotNull((string)responseJson["access_token"]);
    Assert.Equal("Bearer", (string)responseJson["token_type"]);
}

در اینجا، همان کد درخواستی که با curl ساختید تکرار می‌شود. شرط Assert بررسی می‌کند که توکن‌هایی که خالی نیستند و توکن‌های از نوع Bearer دریافت شوند.

آخرین تست تضمین می‌کند که درخواست با یک توکن دسترسی معتبر لیستی از کتاب‌های بازگشت‌داده شده توسط API را دریافت کند:

[Fact]
public async Task GetBooks()
{
    var token = await GetToken();

    var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/books");
    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
    var booksResponse = await _client.SendAsync(requestMessage);

    Assert.Equal(HttpStatusCode.OK, booksResponse.StatusCode);

    var bookResponseString = await booksResponse.Content.ReadAsStringAsync();
    var bookResponseJson = JArray.Parse(bookResponseString);
    Assert.Equal(4, bookResponseJson.Count);
}

این کد از یک متد به نام GetToken() استفاده می‌کند که یک توکن معتبر را از endpoint احراز هویت Auth0 درخواست می‌کند:

public async Task<string> GetToken()
{
    var auth0Client = new HttpClient();
    string token = "";
    var bodyString = $@"{{""client_id"":""{_configuration["Auth0:ClientId"]}"", ""client_secret"":""{_configuration["Auth0:ClientSecret"]}"", ""audience"":""{_configuration["Auth0:Audience"]}"", ""grant_type"":""client_credentials""}}";
    var response = await auth0Client.PostAsync($"{_configuration["Auth0:Authority"]}oauth/token", new StringContent(bodyString, Encoding.UTF8, "application/json"));

    if (response.IsSuccessStatusCode)
    {
        var responseString = await response.Content.ReadAsStringAsync();
        var responseJson = JObject.Parse(responseString);
        token = (string)responseJson["access_token"];

    }

    return token;
}

این تست اعتبارسنجی برنامه ASP.NET Core 2.0 شما را با Auth0 تکمیل می‌کند. در صورت نیاز می‌توانید کد سورس کامل برنامه را از GitHub دانلود کنید.

خلاصه

در این مقاله یک برنامه ASP.NET Core 2 Web API را ساختید و با Auth0 ادغام کردید تا API را فقط به کلاینت‌های معتبر نمایش دهید. این اولین گام در مجموعه مقالات است که نشان می‌دهد چگونه یک برنامه مدرن کامل بسازید. این برنامه بر پایه ASP.NET Core API است که شما فقط آن را در یک برنامه React Single Page ایجاد کردید. در مقاله بعدی این برنامه Single Page را می‌سازید تا به کاربران اجازه دهید در کتاب فروشی گشت و گذار کنند. با ما همراه باشید.