[RequireHttps] public class AccountController : Controller { public IActionResult Login() { return Content("Login Page"); } }
$ openssl genrsa -out key.pem 2048 $ openssl req -new -sha256 -key key.pem -out csr.csr $ openssl req -x509 -sha256 -days 365 -key key.pem -in csr.csr -out certificate.pem openssl pkcs12 -export -out localhost.pfx -inkey key.pem -in certificate.pem
$ dotnet add package Microsoft.AspNetCore.Server.Kestrel.Https
namespace testingSSL { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { options.Listen(IPAddress.Any, 8080); options.Listen(IPAddress.Any, 443, listenOptions => listenOptions.UseHttps("localhost.pfx", "qwe123456")); }) .UseStartup<Startup>() .Build(); } }
البته تا اینجا، هدف بررسی ویژگی RequireHttps بود؛ طبیعتاً به SSL در حین Development نیازی نخواهید داشت. در نتیجه میتوانیم به صورت Global تمامی کنترلرها را در زمان Production به ویژگی گفته شده مزین کنیم:
private readonly IHostingEnvironment _env; public Startup(IConfiguration configuration, IHostingEnvironment env) { Configuration = configuration; _env = env; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(); if (!_env.IsDevelopment()) services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute())); }
(Http Strict Transport Security (HSTS
هدایت کردن خودکار درخواست از حالت HTTP به HTTPS، توسط خیلی از سایتها انجام میشود:
البته این روش بهتر از استفاده نکردن از SSL است؛ اما در نظر داشته باشید که همیشه اولین درخواست به صورت رمزنگاری نشده ارسال خواهد شد. فرض کنید در یک محیط پابلیک از طریق WiFi به اینترنت متصل شدهایم. شخصی (هکر) که بر روی مودم کنترل دارد، طوری WiFi را پیکربندی کردهاست که به جای آدرس اصلی که در تصویر مشاهده میکنید، یک نسخه جعلی از سایت باز شود؛ به طوریکه URL همانند URL اصلی باشد. در اینحالت کاربر به جای اینکه نامکاربری و کلمهعبور را وارد سایت اصلی کند، آن را درون سایت جعلی وارد خواهد کرد. برای حل این مشکل میتوانیم وبسایتمان را طوری تنظیم کنیم که هدر Strict-Transport-Security را به هدر اولین responseی که توسط مرورگر دریافت میشود اضافه کند:
Strict-Transport-Security: max-age=31536000
بنابراین مرورگر وبسایت را درون یک لیست internal به مدت یکسال (مقدار max-age) نگهداری خواهد کرد؛ در طول این زمان به هیچ درخواست ناامنی اجازه داده نخواهد شد. به این قابلیت HSTS گفته میشود. البته ASP.NET Core به صورت توکار روشی را جهت اضافه کردن این هدر ارائه نداده است؛ اما میتوانیم خودمان یک Middleware سفارشی را به pipeline اضافه کنیم تا اینکار را برایمان انجام دهد:
namespace testingSSL.Middleware { public class HstsMiddleware { private readonly RequestDelegate _next; public HstsMiddleware(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext context) { if (!context.Request.IsHttps) return _next(context); if (IsLocalhost(context)) return _next(context); context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000"); return _next(context); } private bool IsLocalhost(HttpContext context) { return string.Equals(context.Request.Host.Host, "localhost", StringComparison.OrdinalIgnoreCase); } } }
یا اینکه میتوانیم از کتابخانه NWebSec استفاده کنیم:
$ dotnet add package NWebsec.AspNetCore.Middleware
برای استفاده از آن نیز خواهیم داشت:
app.UseHsts(h => h.MaxAge(days: 365));
اما هنوز یک مشکل وجود دارد؛ هنوز مشکل اولین درخواست را برطرف نکردهایم. زیرا مرورگر برای دریافت این هدر نیاز به مراجعه به سایت دارد. برای حل این مشکل میتوانید آدرس وبسایت خود را در سایت hstspreload ثبت کنید، سپس متد PreLoad را به کد فوق اضافه کنید:
app.UseHsts(h => h.MaxAge(days: 365).Preload());
در اینحالت حتی اگر کسی به وبسایت شما مراجعه نکند، مرورگر میداند که باید از HTTPS استفاده کند. زیرا این لیست به صورت توکار درون مرورگر تعبیه شدهاست. بنابراین در اینحالت مطمئن خواهیم شد که تمامی connectionها به سایتمان امن میباشند.
دریافت کدهای مطلب جاری (+)
مراحل تنظیم Let's Encrypt در IIS
در مقاله قبلی ، درباره دلایل استفاده از Xamarin ، کمی صحبت کردیم. حال وقت آن رسیده که به سراغ نصب و راه اندازی اولین پروژه زمارین برویم.
اگر شما از Visual Studio 2015 به بعد استفاده کنید، موقع نصب قادر خواهید بود که با انتخاب گزینه Mobile Development و انتخاب Xamarin ، آن را نصب و همچنین تمامی فایلهای مربوط به آن را دریافت کنید. در نظر داشته باشید که در طول این پروسه شما باید از ف.یل.تر شکن استفاده کنید، چرا که بعضی از فایلها مانند Android Sdk بر روی سرور گوگل قرار دارد و گوگل آپیهای ایران را برای برنامه نویسان فیلتر کرده است (البته اگر بستهی کامل VS 2017 را دریافت کرده باشید، تمام این پیش نیازها، به همراه این بستهی 21~ گیگابایتی موجود است و این مشکلات را نخواهید داشت).
حال اگر در حال حاضر Visual Studio را بر روی سیستم خود نصب دارید و زمارین را نصب نکرده اید میتوانید از طریق خود سایت زمارین اقدام به دانلود آن بکنید. در نظر داشته باشید، وقتیکه فایل نصبی را از سایت دریافت میکنید، برنامه شروع به نصب خودکار زمارین و تمامی موارد مورد نیاز خود میکند که در این مورد هم به ف.یل.تر شکن نیاز دارید (منهای بستهی کامل VS 2017 که پیشتر عنوان شد). همانطور که بالا گفته شد فایل هایی مانند Android Sdk را نمیتوان از آی پی ایران دانلود کرد (مگر اینکه از سایتهای واسط استفاده کنید).
پس از موافقت با حقوق کپی رایت زمارین، سیستم شما
را جستجو کرده و تمامی موارد مورد نیاز را که نیاز به نصب دارند، به شما نشان
داده و نصب خواهد کرد.
اگر همه مراحل با موفقیت به پایان برسد، پیغامی مبنی بر موفقیت آمیز بودن عملیات را دریافت خواهید کرد. اما اگر به هر دلیلی فایل نصبی نتواند فایلهای مورد نیاز را دانلود کند، پیغامی مبنی بر شکست عملیات دانلود فایل مورد نظر را دریافت میکنید. در این صورت میتوانید صفحهای را مشاهده کنید که درون آن تمامی فایلهای دانلود نشده و لینک دانلود مستقیم آنها قرار دارند و شما میتوانید فایلهای offline آنها را دانلود کرده و به صورت دستی بر روی سیستم نصب نمایید.
اگر تمامی فایلهای مورد نظر را دانلود کرده باشید، قادر خواهید بود اولین پروژهی خود را شروع کنید. قبل از اینکه کار با زمارین را شروع کنیم، اجازه دهید از اینکه ویژوال استودیو تمامی فایلهای نصب شده را تشخیص داده یا خیر، مطمئن شویم. ویژوال استودیوی خود را باز کرده و از منوی Tools، گزینهی Options را انتخاب و بعد از باز شدن صفحه مورد نظر، در انتهای لیست، گزینه Xamarin را انتخاب کنید.
اگر تمامی مراحل با موفقیت انجام شده باشند، ویژوال استودیو تمامی فایلها را به صورت خودکار تشخیص خواهد داد. اما اگر ویژوال استودیو به هر دلیلی هر کدام از اینها را تشخیص نداد، ابتدا از نصب بودن آنها اطمینان حاصل نمایید و سپس به صورت دستی مسیر آنها را مشخص کنید.
نکته : در نظر داشته باشید فایلهای زیر برای اجرای برنامه بر روی اندروید میباشند و اگر شما تمایلی به اجرای نرم افزار خود را بر روی اندروید ندارید، میتوانید از آنها صرف نظر کنید.
در این مرحله میخواهیم اولین برنامه زمارین خود را شروع کنیم. همانطور که در مقاله قبلی توضیح داده شد، شما میتوانید از xamarin forms و یا xamarin native استفاده کنید. Xamarin forms ساختاری برای code sharing میباشد که توسط شرکت زمارین توسعه داده شدهاست. به این ترتیب شما قادر خواهید بود که با استفاده از Xamarin Forms با نوشتن کمترین کد، نرم افزار خود را بر روی پلتفرمهای پشتیبانی شدهی توسط زمارین اجرا کنید. استفاده از Xamarin Forms برای یک پروژهی تجاری بسیار مناسب میباشد؛ اما در برخی موارد شما باید از Xamarin Native هم استفاده نمایید که در این صورت هم زمارین دست شما را نخواهد بست و قادر خواهید بود که به صورت ترکیبی از Xamarin Forms و Xamarin Native از آن استفاده نمایید.
حال به سراغ ساخت اولین پروژه زمارین برویم. در ابتدا برای ایجاد یک پروژه، از منوی File گزینه New و سپس گزینه Project را انتخاب کنید. در پنجره باز شده از سمت چپ گزینه C# را انتخاب و از زیر منوی Cross Platform گزینه Cross Platform App را انتخاب کنید.
پس از تایید با صفحه زیر مواجه خواهید شد.
در این قسمت همانطورکه مشاهده میکنید میتوانید از دو نوع قالب استفاده کنید و همانطور که از نام آنها مشخص است، Master Detail دو پروژه خواهد ساخت که میتوانید از آن برای سناریوهای خاص استفاده کنید. در قسمت پایین، UI Technology مشخص کنندهی شیوه پیاده سازی ظاهر برنامه است. زمارین فرم برای پیاده سازی ظاهر برنامه از Xaml استفاده میکند و شما میتوانید از طریق آن ظاهر تمامی پلت فرمها را پیاده کنید. همچنین قادر خواهید بود بجای استفاده از Xaml، از زبانهای مخصوص هر پلتفرم مانند Xml برای اندروید استفاده کرده و ظاهر نرم افزار خود را با استفاده از آن پیاده کنید.
زمارین فرم برای Code Sharing از دو روش استفاده میکند.
Shared Project
مزایا:
· شما به تمامی قسمتهای یک پلتفرم دسترسی دارید.
· قادر خواهید بود کدهایی مخصوص به هر پلتفرم را بنویسید.
· میتوانید با استفاده از Conditional Compilation، بر اساس نوع پلتفرم، شروطی را اعمال نمایید.
معایب:
· شما با این روش نمیتوانید DLL ایی را تولید کنید و از آن در پروژههای دیگر استفاده نمایید.
· نوشتن Unit Testing نسبت به روش Portable پیچیدهتر است.
Portable Class Library
مزایا:
· شما قادر خواهید بود از پروژه خود DLL تهیه کرده و در مابقی پروژهها از آن استفاده کنید.
· نوشتن Unit Test به سادگی امکان پذیر است.
معایب:
· در این روش شما نمیتوانید از کدهای مخصوص به هر پلتفرم ( Platform-Specific ) استفاده کنید (البته با استفاده از Dependency Injection امکان پذیر است).
· فقط قادر خواهید بود که از زیر مجموعه .Net استفاده نمایید.
در ادامه پس از انتخاب Xamarin Forms و Shared Project، بر روی Ok کلیک کنید. همانطور که میبینید ویژوال استودیو شروع به ساخت پروژههای برنامه میکند. اگر تمامی Sdk های مورد نیاز را نصب کرده باشید، ویژوال استدیو 5 پروژه را برای شما میسازد (در نظر داشته باشید که اگر SDK مربوط به Universal Windows Platform را هم نصب کرده باشید، پروژهی مربوط به آن را نیز خواهید دید).
اولین پروژه، پروژه Shared است که کدهای برنامه در این پروژه نوشته خواهد شد.
دومین پروژه همانطور که از پسوند آن مشخص است، پروژه مربوط به اندروید است. در نظر داشته باشید که پروژههایی که مربوط به پلتفرمها میباشند، برای نوشتن کدهای مخصوص به هر پلتفرم در دسترس میباشند و کدهای نوشته شده در این پروژهها و پروژه Shared ترکیب شده و بعد کامپایل میشوند.
در زمارین برنامهها از فایلی به نام App
اجرا میشوند. این فایل علاوه بر دسترسی به Life Cycle اپلیکیشن، وظیفهی دیگر آن مشخص کردن اولین
صفحهی برنامه نیز میباشد. این فایل درون پروژه Shared قرار دارد.
همانطور که مشاهده میکنید پس از باز کردن فایل App، در متد سازنده آن صفحه اصلی، به صفحهای به نام MainPage که یک فایل Xaml میباشد، تنظیم شدهاست. پس از باز کردن فایل MainPage با کدهای زیر مواجه میشویم.
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:PreviewerTest" x:Class="PreviewerTest.MainPage"> <Label Text="Welcome to Xamarin Forms!" VerticalOptions="Center" HorizontalOptions="Center" /> </ContentPage>
که مشخص کننده یک لیبل با متن Welcome to Xamarin Forms! میباشد. پس از اجرای برنامه میتوانیم کد اجرا شده را مشاهده کنیم.
شروع کار با OpenAI و ChatGPT با #C
در شروع کار با تکنولوژیهای جدید ممکنه همیشه اولش کمی پیچیده بنظر بیاد و زمان بر باشه. توی این مخزن(Repository) گیتهاب سعی کردم چند مثال از کار با OpenAI API با دات نت و به زبان سی شارپ بزارم.
از سادهترین حالت فراخوانی API تا مدلهای یکم پیچیده مثل یک ربات تلگرامی برای چت با ChatGPT و همچنین تولید عکسهای خلاقانه بر اساس promptهای ورودی کاربر با استفاده از DALL-E.
به مرور سعی میکنم مثالهای بیشتر و پیشرفتهتری رو هم اضافه کنم تا برای اونایی که در شروع این راه هستند با حداقل اصطکاک یادگیری و در کمترین زمان بتوانند ایدههای خود را بر اساس ChatGPT پیاده سازی کنند.
در تولید کدها و همچنین مستندسازی هم تا حد امکان سعی کردم از خود ChatGPT استفاده کنم!
کدهای سمت سرور دریافت فایل PDF
using Microsoft.AspNetCore.Mvc; namespace BlazorWasmShowBinaryFiles.Server.Controllers { [ApiController] [Route("api/[controller]")] public class ReportsController : ControllerBase { [HttpGet("[action]")] public IActionResult GetPdfReport() { //TODO: create the `sample.pdf` report file on the server return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامههای Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آنرا در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه میکنیم:
window.JsBinaryFilesUtils = { createBlobUrl: function (byteArray, contentType) { // The byte array in .NET is encoded to base64 string when it passes to JavaScript. const numArray = atob(byteArray) .split("") .map((c) => c.charCodeAt(0)); const uint8Array = new Uint8Array(numArray); const blob = new Blob([uint8Array], { type: contentType }); return URL.createObjectURL(blob); }, downloadFromUrl: function (fileName, url) { const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); }, downloadBlazorByteArray: function (fileName, byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.downloadFromUrl(fileName, blobUrl); URL.revokeObjectURL(blobUrl); }, printFromUrl: function (url) { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = url; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } }, printBlazorByteArray: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.printFromUrl(blobUrl); URL.revokeObjectURL(blobUrl); }, showUrlInNewTab: function (url) { window.open(url); }, showBlazorByteArrayInNewTab: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.showUrlInNewTab(blobUrl); URL.revokeObjectURL(blobUrl); }, };
- زمانیکه در برنامههای Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایهای از بایتهای یک فایل باینری را دریافت میکنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشتهی base64 شده صورت میگیرد (JS Interop اینکار را به صورت خودکار انجام میدهد). به همین جهت در متد createBlobUrl روش تبدیل این رشتهی base64 دریافتی را به آرایهای از بایتها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده میکنید. این Blob Url اکنون آدرس موقتی دسترسی به آرایهای از بایتهای دریافتی توسط مرورگر است. به همین جهت میتوان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آنرا انجام میدهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آنرا دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگیهای آن و سپس کلیک بر روی آنرا به صورت خودکار انجام میدهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین میکند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شدهاست، Url یک منبع را دریافت کرده، آنرا به یک iframe اضافه میکند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحهی پیشنمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین میکند.
تهیهی متدهایی الحاقی جهت کار سادهتر با JsBinaryFilesUtils
پس از تهیهی JsBinaryFilesUtils فوق، میتوان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایلهای باینری را سادهتر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorWasmShowBinaryFiles.Client.Utils { public static class JsBinaryFilesUtils { public static ValueTask<string> CreateBlobUrlAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType); } public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url); } public static ValueTask DownloadBlazorByteArrayAsync( this IJSRuntime JSRuntime, string fileName, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray", fileName, byteArray, contentType); } public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url); } public static ValueTask PrintBlazorByteArrayAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType); } public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url); } public static ValueTask ShowBlazorByteArrayInNewTabAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType); } } }
اصلاح Content Security Policy سمت سرور جهت ارائهی محتوای blob
پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL میتوان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرسهایی به صورت blob:http تولید میشوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob: default-src 'self' blob: object-src 'self' blob:
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و نمایش آن در صفحهی جاری
در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه میکنید:
@page "/" @using BlazorWasmShowBinaryFiles.Client.Utils @inject IJSRuntime JSRuntime @inject HttpClient HttpClient <h1>Display PDF Files</h1> <button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button> <button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button> <button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button> @if(!string.IsNullOrWhiteSpace(PdfBlobUrl)) { <section class="card mb-5 mt-3"> <div class="card-header"> <h4>using iframe</h4> </div> <div class="card-body"> <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using object</h4> </div> <div class="card-body"> <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using embed</h4> </div> <div class="card-body"> <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%"> </div> </section> } @code { private const string ReportUrl = "/api/Reports/GetPdfReport"; private const string PdfContentType = "application/pdf"; private string PdfBlobUrl; private async Task handlePrintPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `PrintFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType); } private async Task handleDownloadPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `DownloadFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType); } private async Task handleShowPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType); } // Tips: // 1- How do I enable/disable the built-in pdf viewer of FireFox // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709). }
- پس از تهیهی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شدهی توسط یک اکشن متد را به صورت آرایهای از بایتها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایهای از بایتهای دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
- Visual Studio Express 2013 RC for Web یا Visual Studio 2013 RC
- یک حساب کاربری در Windows Azure. میتوانید یک حساب رایگان بسازید.
یک مدیر کلی به حساب کاربری Active Directory خود اضافه کنید
- وارد Windows Azure Portal شوید.
- یک حساب کاربری (Windows Azure Active Directory (AD انتخاب یا ایجاد کنید. اگر قبلا حساب کاربری ساخته اید از همان استفاده کنید در غیر اینصورت یک حساب جدید ایجاد کنید. مشترکین Windows Azure یک AD پیش فرض با نام Default Directory خواهند داشت.
- در حساب کاربری AD خود یک کاربر جدید در نقش Global Administrator بسازید. اکانت AD خود را انتخاب کنید و Add User را کلیک کنید. برای اطلاعات کاملتر به Managing Windows Azure AD from the Windows Azure Portal 1– Sign Up with an Organizational Account مراجعه کنید.
یک نام کاربری انتخاب کرده و به مرحله بعد بروید.
نام کاربری را وارد کنید و نقش Global Administrator را به آن اختصاص دهید. مدیران کلی به یک آدرس ایمیل متناوب هم نیاز دارند. به مرحله بعد بروید.
بر روی Create کلیک کنید و کلمهی عبور موقتی را کپی کنید. پس از اولین ورود باید کلمه عبور را تغییر دهید.
یک اپلیکیشن ASP.NET بسازید
گزینه Organizational Accounts را انتخاب کنید. نام دامنه خود را وارد کنید و سپس گزینه Single Sign On, Read directory data را انتخاب کنید. به مرحله بعد بروید.
نکته: در قسمت More Options می توانید قلمرو اپلیکیشن (Application ID URI) را تنظیم کنید. تنظیمات پیش فرض برای اکثر کاربران مناسب است اما در صورت لزوم میتوانید آنها را ویرایش کنید، مثلا از طریق Windows Azure Portal دامنههای سفارشی خودتان را تنظیم کنید.
اگر گزینه Overwrite را انتخاب کنید اپلیکیشن جدیدی در Windows Azure برای شما ساخته خواهد شد. در غیر اینصورت فریم ورک سعی میکند اپلیکیشنی با شناسه یکسان پیدا کند (در پست متدهای احراز هویت در VS 2013 به تنظیمات این قسمت پرداخته شده).
اطلاعات مدیر کلی دامنه در Active Directory خود را وارد کنید (Credentials) و پروژه را با کلیک کردن بر روی Create Project بسازید.
با کلیدهای ترکیبی Ctrl + F5، اپلیکیشن را اجرا کنید. مرورگر شما باید یک اخطار SSL Certificate به شما بدهد. این بدین دلیل است که مدرک استفاده شده توسط IIS Express مورد اعتماد (trusted) نیست. این اخطار را بپذیرید و اجازه اجرا را به آن بدهید. پس از آنکه اپلیکیشن خود را روی Windows Azure منتشر کردید، این پیغام دیگر تولید نمیشود؛ چرا که Certificateهای استفاده شده trusted هستند.
با حساب کاربری سازمانی (organizational account) که ایجاد کردهاید، وارد شوید.
همانطور که مشاهده میکنید هم اکنون به سایت وارد شده اید.
توزیع اپلیکیشن روی Windows Azure
اپلیکیشن را روی Windows Azure منتشر کنید. روی پروژه کلیک راست کرده و Publish را انتخاب کنید. در مرحله تنظیمات (Settings) مشاهده میکنید که احراز هویت حسابهای سازمانی (organizational accounts) فعال است. همچنین سطح دسترسی به خواندن تنظیم شده است. در قسمت Database رشته اتصال دیتابیس را تنظیم کنید.
حال به وب سایت Windows Azure خود بروید و توسط حساب کاربری ایجاد شده وارد سایت شوید. در این مرحله دیگر نباید خطای امنیتی SSL را دریافت کنید.
خواندن اطلاعات پروفایل کاربران توسط Graph API
[Authorize] public async Task<ActionResult> UserProfile() { string tenantId = ClaimsPrincipal.Current.FindFirst(TenantSchema).Value; // Get a token for calling the Windows Azure Active Directory Graph AuthenticationContext authContext = new AuthenticationContext(String.Format(CultureInfo.InvariantCulture, LoginUrl, tenantId)); ClientCredential credential = new ClientCredential(AppPrincipalId, AppKey); AuthenticationResult assertionCredential = authContext.AcquireToken(GraphUrl, credential); string authHeader = assertionCredential.CreateAuthorizationHeader(); string requestUrl = String.Format( CultureInfo.InvariantCulture, GraphUserUrl, HttpUtility.UrlEncode(tenantId), HttpUtility.UrlEncode(User.Identity.Name)); HttpClient client = new HttpClient(); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl); request.Headers.TryAddWithoutValidation("Authorization", authHeader); HttpResponseMessage response = await client.SendAsync(request); string responseString = await response.Content.ReadAsStringAsync(); UserProfile profile = JsonConvert.DeserializeObject<UserProfile>(responseString); return View(profile); }
کلیک کردن لینک UserProfile اطلاعات پروفایل کاربر جاری را نمایش میدهد.
اطلاعات بیشتر
تمامی پاسخهای دریافتی از Web API توسط Client، باید در قالب کدهای وضعیت HTTP باشند. دو کلاس جدید با نامهای HttpResponseMessage و HttpResponseException همراه با ASP.NET MVC 4 معرفی شده اند که ارسال کدهای وضعیت پردازش درخواست به Client را آسان میسازند. به عنوان مثال، ارسال وضعیت برای چهار عمل اصلی بازیابی، ایجاد، آپدیت و حذف رکورد را بررسی میکنیم.
بازیابی رکورد
بر اساس مستندات پروتوکل HTTP، در صورتی که منبع درخواستی Client پیدا نشد، باید کد وضعیت 404 برگشت داده شود. این حالت را در متد ذیل پیاده سازی کرده ایم.
public Product GetProduct(int id) { Product item = repository.Get(id); if (item == null) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); } return item; }
ایجاد رکورد
برای ایجاد رکورد، Client درخواستی از نوع POST را همراه با دادههای رکورد در بدنهی درخواست به Server ارسال میکند. در ذیل، پیاده سازی ساده ای از این حالت را مشاهده میکنید.
public Product PostProduct(Product item) { item = repository.Add(item); return item; }
- کد وضعیت پردازش درخواست: به طور پیش فرض، Web API، کد 200 را در پاسخ ارسال میکند، اما بر اساس مستندات پروتوکل HTTP، زمانی که یک درخواست از نوع POST منجر به تولید منبعی می-شود، Server باید کد وضعیت 201 را به Client برگشت بدهد.
- آدرس منبع جدید ایجاد شده: بر اساس مستندات پروتوکل HTTP، زمانی که منبعی بر روی Server ایجاد میشود، باید آدرس منبع جدید ایجاد شده از طریق هدر Location به Client ارسال شود.
public HttpResponseMessage PostProduct(Product item) { item = repository.Add(item); var response = Request.CreateResponse(HttpStatusCode.Created, item); string uri = Url.Link("DefaultApi", new { id = item.Id }); response.Headers.Location = new Uri(uri); return response; }
آپدیت رکورد
آپدیت با استفاده از درخواستهای از نوع PUT انجام میگیرد. یک مثال ساده در این مورد.
public void PutProduct(int id, Product product) { product.Id = id; if (!repository.Update(product)) { throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.NotFound)); } }
حذف یک رکورد
حذف یک رکورد، با استفاده از درخواستهای از نوع DELETE انجام میگیرد. یک مثال ساده در این مورد.
public HttpResponseMessage DeleteProduct(int id) { repository.Remove(id); return new HttpResponseMessage(HttpStatusCode.NoContent); }
فراخوانی متدها و مدیریت کدهای وضعیت HTTP در سمت Client
حال ببینیم چگونه میتوان از متدهای قبل در سمت Client استفاده و خطاهای احتمالی آنها را مدیریت کرد.
بهتر است مثال را برای حالتی که در آن رکوردی آپدیت میشود بررسی کنیم. کدهای مورد نیاز برای فراخوانی متد PutProduct در سمت Client به صورت ذیل است.
var id = $("#myTextBox").val(); $.ajax({ url: "/api/Test/" + id, type: 'PUT', data: { Id: "1", Name: "Tomato Soup", Category: "Groceries", Price: "1.39M" }, cache: false, statusCode: { 200: function (data) { alert("آپدیت انجام شد"); }, 404: function () { alert("خطا در آپدیت"); } } });
در حالتی که آپدیت با موفقیت همراه باشد، بدنهی پاسخ به شکل ذیل است.
و در صورتی که خطایی رخ دهد، بدنهی پاسخ دریافتی به صورت ذیل خواهد بود.
ASP.NET MVC #17
فیلترهای امنیتی ASP.NET MVC
ASP.NET MVC به همراه تعدادی فیلتر امنیتی توکار است که در این قسمت به بررسی آنها خواهیم پرداخت.
بررسی اعتبار درخواست (Request Validation) در ASP.NET MVC
ASP.NET MVC امکان ارسال اطلاعاتی را که دارای تگهای HTML باشند، نمیدهد. این قابلیت به صورت پیش فرض فعال است و جلوی ارسال انواع و اقسام اطلاعاتی که ممکن است سبب بروز حملات XSS Cross site scripting attacks شود را میگیرد. نمونهای از خطای نمایش داده:
A potentially dangerous Request.Form value was detected from the client (Html="<a>").
بنابراین تصمیم گرفته شده صحیح است؛ اما ممکن است در قسمتی از سایت نیاز باشد تا کاربران از یک ویرایشگر متنی پیشرفته استفاده کنند. خروجی این نوع ویرایشگرها هم HTML است. در این حالت میتوان صرفا برای متدی خاص امکانات Request Validation را به کمک ویژگی ValidateInput غیرفعال کرد:
[HttpPost]
[ValidateInput(false)]
public ActionResult CreateBlogPost(BlogPost post)
از ASP.NET MVC 3.0 به بعد راه حل بهتری به کمک ویژگی AllowHtml معرفی شده است. غیرفعال کردن ValidateInput ایی که معرفی شد، بر روی تمام خواص شیء BlogPost اعمال میشود. اما اگر فقط بخواهیم که مثلا خاصیت Text آن از مکانیزم بررسی اعتبار درخواست خارج شود، بهتر است دیگر از ویژگی ValidateInput استفاده نشده و به نحو زیر عمل گردد:
using System;
using System.Web.Mvc;
namespace MvcApplication14.Models
{
public class BlogPost
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
[AllowHtml]
public string Text { set; get; }
}
}
در اینجا فقط خاصیت Text مجاز به دریافت محتوای HTML ایی خواهد بود. اما خاصیت Title چنین مجوزی را ندارد. همچنین دیگر نیازی به استفاده از ویژگی ValidateInput غیرفعال شده نیز نخواهد بود.
به علاوه همانطور که در قسمتهای قبل نیز ذکر شد، خروجی Razor به صورت پیش فرض Html encoded است مگر اینکه صریحا آنرا تبدیل به HTML کنیم (مثلا استفاده از متد Html.Raw). به عبارتی خروجی Razor در حالت پیش فرض در مقابل حملات XSS مقاوم است مگر اینکه آگاهانه بخواهیم آنرا غیرفعال کنیم.
مطلب تکمیلی
مقابله با XSS ؛ یکبار برای همیشه!
فیلتر RequireHttps
به کمک ویژگی یا فیلتر RequireHttps، تمام درخواستهای رسیده به یک متد خاص باید از طریق HTTPS انجام شوند و حتی اگر شخصی سعی به استفاده از پروتکل HTTP معمولی کند، به صورت خودکار به HTTPS هدایت خواهد شد:
[RequireHttps]
public ActionResult LogOn()
{
}
فیلتر ValidateAntiForgeryToken
نوع دیگری از حملات که باید در برنامههای وب به آنها دقت داشت به نام CSRF یا Cross site request forgery معروف هستند.
برای مثال فرض کنید کاربری قبل از اینکه بتواند در سایت شما کار خاصی را انجام دهد، نیاز به اعتبار سنجی داشته باشد. پس از لاگین شخص و ایجاد کوکی و سشن معتبر، همین شخص به سایت دیگری مراجعه میکند که در آن مهاجمی بر اساس وضعیت جاری اعتبار سنجی او مثلا لینک حذف کاربری یا افزودن اطلاعات جدیدی را به برنامه ارائه میدهد. چون سشن شخص و کوکی مرتبط به سایت اول هنوز معتبر هستند و شخص سایت را نبسته است، «احتمال» اجرا شدن درخواست مهاجم بالا است (خصوصا اگر از مرورگرهای قدیمی استفاده کند).
بنابراین نیاز است بررسی شود آیا درخواست رسیده واقعا از طریق فرمهای برنامه ما صادر شده است یا اینکه شخصی از طریق سایت دیگری اقدام به جعل درخواستها کرده است.
برای مقابله با این نوع خطاها ابتدا باید داخل فرمهای برنامه از متد Html.AntiForgeryToken استفاده کرد. کار این متد ایجاد یک فیلد مخفی با مقداری منحصربفرد بر اساس اطلاعات سشن جاری کاربر است، به علاوه ارسال یک کوکی خودکار تا بتوان از تطابق اطلاعات اطمینان حاصل کرد:
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
در مرحله بعد باید فیلتر ValidateAntiForgeryToken جهت بررسی مقدار token دریافتی به متد ثبت اطلاعات اضافه شود:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateBlogPost(BlogPost post)
در اینجا مقدار دریافتی از فیلد مخفی فرم :
<input name="__RequestVerificationToken" type="hidden" value="C0iPfy/3T....=" />
با مقدار موجود در کوکی سایت بررسی و تطابق داده خواهند شد. اگر این مقادیر تطابق نداشته باشند، یک استثنا صادر شده و از پردازش اطلاعات رسیده جلوگیری میشود.
علاوه بر اینها بهتر است حین استفاده از متد و فیلتر یاد شده، از یک salt مجزا نیز به ازای هر فرم، استفاده شود:
[ValidateAntiForgeryToken(Salt="1234")]
@Html.AntiForgeryToken(salt:"1234")
به این ترتیب tokenهای تولید شده در فرمهای مختلف سایت یکسان نخواهند بود.
به علاوه باید دقت داشت که ValidateAntiForgeryToken فقط با فعال بودن کوکیها در مرورگر کاربر کار میکند و اگر کاربری پذیرش کوکیها را غیرفعال کرده باشد، قادر به ارسال اطلاعاتی به برنامه نخواهد بود. همچنین این فیلتر تنها در حالت HttpPost قابل استفاده است. این مورد هم در قسمتهای قبل تاکید گردید که برای مثال بهتر است بجای داشتن لینک delete در برنامه که با HttpGet ساده کار میکند، آنرا تبدیل به HttpPost نمود تا میزان امنیت برنامه بهبود یابد. از HttpGet فقط برای گزارشگیری و خواندن اطلاعات از برنامه استفاده کنید و نه ثبت اطلاعات.
بنابراین استفاده از AntiForgeryToken را به چک لیست اجباری تولید تمام فرمهای برنامه اضافه نمائید.
مطلب مشابه
Anti CSRF module for ASP.NET
فیلتر سفارشی بررسی Referrer
یکی دیگر از روشهای مقابله با CSRF، بررسی اطلاعات هدر درخواست ارسالی است. اگر اطلاعات Referrer آن با دومین جاری تطابق نداشت، به معنای مشکل دار بودن درخواست رسیده است. فیلتر سفارشی زیر میتواند نمونهای باشد جهت نمایش نحوه بررسی UrlReferrer درخواست رسیده:
using System.Web.Mvc;
namespace MvcApplication14.CustomFilter
{
public class CheckReferrerAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext != null)
{
if (filterContext.HttpContext.Request.UrlReferrer == null)
throw new System.Web.HttpException("Invalid submission");
if (filterContext.HttpContext.Request.UrlReferrer.Host != "mysite.com")
throw new System.Web.HttpException("This form wasn't submitted from this site!");
}
base.OnAuthorization(filterContext);
}
}
}
و برای استفاده از آن:
[HttpPost]
[CheckReferrer]
[ValidateAntiForgeryToken]
public ActionResult DeleteTask(int id)
نکتهای امنیتی در مورد آپلود فایلها در ASP.NET
هر جایی که کاربر بتواند فایلی را به سرور شما آپلود کند، مشکلات امنیتی هم از همانجا شروع خواهند شد. مثلا در متد Upload قسمت 11 این سری، منعی در آپلود انواع فایلها نیست و کاربر میتواند انواع و اقسام شلها را جهت تحت کنترل گرفتن سایت و سرور آپلود و اجرا کند. راه حل چیست؟
از همان روش امنیتی مورد استفاده توسط تیم ASP.NET MVC استفاده میکنیم. فایل web.config قرار گرفته در پوشه Views را باز کنید (نه فایل وب کانفیگ ریشه اصلی سایترا). چنین تنظیمی را میتوان مشاهده کرد:
برای IIS6 :
<system.web>
<httpHandlers>
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
</system.web>
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
تنظیم فوق، موتور اجرایی ASP.NET را در این پوشه خاص از کار میاندازد. به عبارتی اگر شخصی مسیر یک فایل aspx یا cshtml یا هر فایل قرار گرفته در پوشه Views را مستقیما در مرورگر خود وارد کند، با پیغام HttpNotFound مواجه خواهد شد.
این روش هم با ASP.NET Web forms سازگار است و هم با ASP.NET MVC؛ چون مرتبط است به موتور اجرایی ASP.NET که هر دوی این فریم ورکها برفراز آن معنا پیدا میکنند.
بنابراین در پوشه فایلهای آپلودی به سرور خود یک web.config را با محتوای فوق ایجاد کنید (و فقط باید مواظب باشید که این فایل حین آپلود فایلهای جدید، overwrite نشود. مهم!). به این ترتیب این مسیر دیگر از طریق مرورگر قابل دسترسی نخواهد بود (با هر محتوایی). سپس برای ارائه فایلهای آپلودی به کاربران از روش زیر استفاده کنید:
public ActionResult Download()
{
return File(Server.MapPath("~/Myfiles/test.txt"), "text/plain");
}
مزیت مهم روش ذکر شده این است که کاربران مجاز به آپلود هر نوع فایلی خواهند بود و نیازی نیست لیست سیاه تهیه کنید که مثلا فایلهایی با پسوندهای خاص آپلود نشوند (که در این بین ممکن است لیست سیاه شما کامل نباشد ...).
علاوه بر تمام فیلترهای امنیتی که تاکنون بررسی شدند، فیلتر دیگری نیز به نام Authorize وجود دارد که در قسمتهای بعدی بررسی خواهد شد.