Just My Code برای جاوا اسکریپت
With the new Office Developer Tools for Visual Studio 2013 – March 2014 Update comes a feature called “Just My Code” for JavaScript, which can speed up your debugging experience by allowing you to focus on debugging your own code, and not library or framework code. In this post, I’ll walk through how this works
برپایی پیشنیازها
در اینجا نیز از همان برنامهای که در قسمت 30، برای بررسی مثالهای React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانهی Axios را نیز نصب میکنید. به همین جهت در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
در ادامه میخواهیم در برنامهی React خود، لیست مطالب برنامهی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمههای search و clear نیز به آن اضافه کنیم.
دریافت اطلاعات اولیه از سرور، درون useEffect Hook
پس از نصب پیشنیازها و راه اندازی برنامهی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد میکنیم:
{ "apiUrl": "https://localhost:5001/api" }
import React from "react"; import {apiUrl} from "../../config.json"; export default function App() { return <></>; }
import App from "./components/part03/Search";
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { useEffect(() => { axios .get(apiUrl + "/posts/search?query=") .then(response => console.log(response.data)); }); return <></>; }
اکنون میخواهیم این اطلاعات دریافتی را در برنامهی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقهای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آنرا import کرده و سپس آنرا تعریف و در قسمت then، فراخوانی میکنیم:
import axios from "axios"; import React, { useEffect, useState } from "react"; import { apiUrl } from "../../config.json"; export default function App() { const [results, setResults] = useState([]); useEffect(() => { axios.get(apiUrl + "/posts/search?query=").then(response => { console.log(response.data); setResults(response.data); }); });
همانطور که مشاهده میکنید، یک حلقهی بی پایان در اینجا رخ دادهاست! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامهی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرندهی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا میشود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننتهای کلاسی را با هم پوشش میدهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شدهاست و پس از آن، مجددا effect method فراخوانی میشود و ... این حلقه هیچگاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ میکنند!
در این برنامه فعلا میخواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایهی خالی مقدار دهی میکنیم:
useEffect(() => { // ... }, []);
//... export default function App() { // ... return ( <> <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> </> ); }
استفاده ازAsync/Await برای دریافت اطلاعات، درون یک useEffect Hook
اکنون میخواهیم درون effect method یک useEffect Hook، روش قدیمی استفادهی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آنرا بیشتر بررسی کردیم، جایگزین کنیم.
useEffect(async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); }, []);
Warning: An effect function must not return anything besides a function, which is used for clean-up. It looks like you wrote useEffect(async () => ...) or returned a Promise.
برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
useEffect(() => { getResults(); }, []); const getResults = async () => { const { data } = await axios.get(apiUrl + "/posts/search?query="); console.log(data); setResults(data); };
پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا
تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آنرا از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقهی بینهایت وارد نشود. اکنون این سؤال مطرح میشود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شدهی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آنرا با استفاده از useState Hook و یک مقدار اولیهی دلخواه تنظیم میکنیم:
export default function App() { // ... const [query, setQuery] = useState("Title");
<input type="text" name="query" className="form-control my-3" placeholder="Search..." onChange={event => setQuery(event.target.value)} value={query} />
اکنون که متغیر query دارای مقدار شدهاست، میتوان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` );
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمیشود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get میشود، طوری تنظیم شدهاست که فقط یکبار، آنهم پس از رندر اولیهی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایهای که به عنوان پارامتر دوم متد useEffect تعریف شده، میتوان اجرای مجدد effect function آنرا وابستهی به تغییرات متغیر query در state کامپوننت کرد:
useEffect(() => { getResults(); }, [query]);
دریافت اطلاعات جستجو، تنها با ارسال اطلاعات یک فرم به سمت سرور
تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد میکند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن میتوان یک دکمهی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف میکنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آنرا مجددا به یک آرایهی خالی تنظیم میکنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره میکند، در بالای جدول نتایج مطالب، قرار میدهیم:
<button className="btn btn-primary" type="button" onClick={getResults} > Search </button>
<form onSubmit={handleSearch}> <div className="input-group my-3"> <label htmlFor="query" className="form-control-label sr-only"></label> <input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} /> <div className="input-group-append"> <button className="btn btn-primary" type="submit"> Search </button> </div> </div> </form>
const handleSearch = event => { event.preventDefault(); getResults(); };
افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook
در ادامه میخواهیم یک دکمهی جدید را در کنار دکمهی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شدهی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشتهی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آنرا با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم میکنیم». برای انجام یک چنین کاری در اینجا، Hook ویژهای به نام useRef معرفی شدهاست. بنابراین برای پیاده سازی این نیازمندیها، ابتدا دکمهی Clear را در کنار دکمهی Search قرار میدهیم:
<button type="button" onClick={handleClearSearch} className="btn btn-info" > Clear </button>
import React, { useEffect, useRef, useState } from "react"; // ... export default function App() { // ... const searchInputRef = useRef(); const handleClearSearch = () => { setQuery(""); searchInputRef.current.focus(); };
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آنرا به ویژگی ref المان، انتساب داد:
<input type="text" id="query" name="query" className="form-control" placeholder="Search ..." onChange={event => setQuery(event.target.value)} value={query} ref={searchInputRef} />
نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور
البته در اینجا با هر بار کلیک بر روی دکمهی جستجو، نتیجهی نهایی به سرعت نمایش داده میشود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ میدهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه میکنیم:
const [loading, setLoading] = useState(false);
const getResults = async () => { setLoading(true); const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); setLoading(false); };
{loading ? ( <div className="alert alert-info">Loading results...</div> ) : ( <table className="table"> <thead> <tr> <th>Title</th> </tr> </thead> <tbody> {results.map(post => ( <tr key={post.id}> <td>{post.title}</td> </tr> ))} </tbody> </table> )}
برای آزمایش آن میتوان سرعت اتصال را در برگهی شبکهی ابزارهای توسعه دهندگان مرورگر، تغییر داد:
مدیریت خطاها در حین اعمال async
آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت میگیرد:
// ... export default function App() { // ... const [error, setError] = useState(null); // ... const getResults = async () => { setLoading(true); try { const { data } = await axios.get( `${apiUrl}/posts/search?query=${encodeURIComponent(query)}` ); console.log(data); setResults(data); } catch (err) { setError(err); } setLoading(false); };
{error && <div className="alert alert-warning">{error.message}</div>}
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
6.Visual Studio 2017 15.7 منتشر شد
This release contains the following improvements and enhancements:
- This release now installs Java™ Development Kit 8, Update 181 (JDK version 8u181).
These are the customer-reported issues addressed in 15.7.6:
- Visual C++ Redistributable 2017 does not report the install state in a specific registry location.
- In 15.7 users may see extensions load without all of their assets and cause VS to crash. This has been addressed for 15.7.6.
- Visual Studio 2017 version 15.7.5 crashes when opening a solution.
These are the customer-reported issues addressed in this release:
- The debugger cannot continue running the process. Operation not supported. Unknown error: 0x9233000b.
- Recent Projects and Solutions not populated once executed.
- Full build every time with 15.5.
- Wrong IntelliSense errors are still shown on VS 15.5.2 for Visual Basic projects.
- VS2017 15.5.2 Unresolved references when "Allow parallel project initialization" is enabled.
- Upgrading to 15.5.2: cannot launch nor repair VS.
- VS2017 Installation issue.
- Unable to install because of BSoD.
بررسی 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 وارد کنید.
In this tutorial, you'll build a Nest.js application to get yourself familiar with its building blocks as well as the fundamental principles of building modern web applications. You'll approach this project by separating the application into two different sections: the frontend and the backend. Firstly, you'll concentrate on the RESTful back-end API built with Nest.js. You'll then focus on the frontend, which you will build with Vue.js. Both applications will run on different ports and will function as separate domains.
انتشار Unity 2018.1
The Visual Studio team is excited about the Unity 2018.1 release: It’s the start of a new release cycle packed with great new features like the Scriptable Render Pipeline and the C# Job System. You can read the full blog post by Unity for all the details on what’s new in the 2018.1 release.
Visual Studio 17.8 منتشر شد
The latest update for Visual Studio 2022 is officially out. Offering a comprehensive suite of enhancements that span productivity, programming languages, and enterprise management, this general release is a monumental step forward. Developed with extensive user feedback, this version is fully compatible with .NET 8, which is also generally available as of today.