پروژه ویراستار را از Ruby به سی شارپ تبدیل کردم. سورس نهایی کامل، فایلهای باینری، به همراه unit tests و راهنمای کتابخانه، از آدرس زیر قابل دریافت هستند:
خلاصه کارهایی را که انجام میدهد:
[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ها به سایتمان امن میباشند.
دریافت کدهای مطلب جاری (+)
<title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" /> <link href="../../plupload/js/jquery.plupload.queue/css/jquery.plupload.queue.css" rel="stylesheet" /> <script src="@Url.Content("~/Scripts/jquery-1.7.1.min.js")" type="text/javascript"></script> <script type="text/javascript" src="http://bp.yahooapis.com/2.4.21/browserplus-min.js"></script> <script src="../../plupload/js/plupload.full.js"></script> <script src="../../plupload/js/jquery.plupload.queue/jquery.plupload.queue.js"></script> <script src="../../plupload/js/i18n/fa.js"></script>
@{ ViewBag.Title = "Uploading Files using PlUpload"; } <h2>@ViewBag.Message</h2> @using (Html.BeginForm("Post", "home", FormMethod.Post, new { enctype = "multipart/form-data" })) { <div id="uploader"> <p>You browser doesn't have Flash, Silverlight, Gears, BrowserPlus or HTML5 support.</p> </div> } <script> $(function () { $("#uploader").pluploadQueue({ // General settings runtimes: 'html5,gears,flash,silverlight,browserplus,html4', url: '@Url.Action("Upload" , "Home")', max_file_size: '10mb', chunk_size: '1mb', unique_names: true, // Resize images on clientside if we can resize: { width: 320, height: 240, quality: 90 }, // Specify what files to browse for filters: [ { title: "Image files", extensions: "jpg,gif,png" }, { title: "Zip files", extensions: "zip" } ], // Flash settings flash_swf_url: '/plupload/js/plupload.flash.swf', // Silverlight settings silverlight_xap_url: '/plupload/js/plupload.silverlight.xap' }); }); </script>
[HttpPost] public ActionResult Upload(int? chunk, string name) { var fileUpload = Request.Files[0]; var uploadPath = Server.MapPath("~/App_Data"); chunk = chunk ?? 0; using (var fs = new FileStream(Path.Combine(uploadPath, name), chunk == 0 ? FileMode.Create : FileMode.Append)) { if (fileUpload != null) { var buffer = new byte[fileUpload.InputStream.Length]; fileUpload.InputStream.Read(buffer, 0, buffer.Length); fs.Write(buffer, 0, buffer.Length); } } return Content("chunk uploaded", "text/plain"); }
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } }
[ServiceContract] public interface ISampleService { [OperationContract] IEnumerable<Book> GetAll(); [OperationContract] void Save( Book book ); }
public class SampleService : ISampleService { public List<Book> ListOfBook { get; private set; } public SampleService() { ListOfBook = new List<Book>(); } public IEnumerable<Book> GetAll() { ListOfBook.AddRange( new Book[] { new Book(){Code=1 , Name="Book1"}, new Book(){Code=2 , Name="Book2"}, } ); return ListOfBook; } public void Save( Book book ) { ListOfBook.Add( book ); } }
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string Author { get; set; } }
[DataMember( IsRequired = true )] public string Author { get; set; }
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember( IsRequired = true )] public string Author { get; set; } [OnDeserializing] private void OnDeserializing( StreamingContext context ) { if ( string.IsNullOrEmpty( Author ) ) { Author = "Masoud Pakdel"; } } }
روش بعدی استفاده از اینترفیس IExtensibleDataObject است. بعد از اینکه کلاس Book این اینترفیس را پیاده سازی کرد مشکل Versioning Round Trip حل میشود. به این صورت که سرویس یا کلاینتی که نسخه قدیمی را میشناسد اگر نسخه جدید را دریافت کند خصوصیاتی را که نمیشناسد مثل Author در خاصیت ExtensionData ذخیره میشود و هنگامی که کلاس Book برای سرویس یا کلاینتی که نسخه جدید را میشناسد DataContractSerializer اطلاعات مورد نظر را از خصوصیت ExtensionData بیرون میکشد و کلاس Book جدید را باز سازی میکند. بررسی کلاس ExtensionData توسط خود DataContractSreializer انجام میشود و نیاز به هیچ گونه ای کد نویسی ندارد.
[DataContract] public class Book : IExtensibleDataObject { [DataMember] public int Code { get; set; } [DataMember] public string Name { get; set; } [DataMember] public string Author { get; set; } public virtual ExtensionDataObject ExtensionData { get { return _extensionData; } set { _extensionData = value; } } private ExtensionDataObject _extensionData; }
public IEnumerable<Book> GetAll() { ListOfBook.AddRange( new Book[] { new Book(){Code=1 , Name="Book1", Author="Masoud Pakdel"}, new Book(){Code=2 , Name="Book2" }, } ); return ListOfBook; }
همان طور که میبینید این نسخه از کلاینت هیچ گونه اطلاعی از وجود یک خاصیت به نام Author ندارد ولی از طریق ExtensionData متوجه میشود یک خاصیت به نام Author به مدل سمت سرور اضافه شده است.
اما در صورتی که قصد داشته باشیم که یک سرویس خاص از همان نسخه قدیمی کلاس Book استفاده کند و نیاز به نسخه جدید آن نداشته باشد میتوانیم این کار را از طریق مقدار دهی True به خاصیت IgnoreExtensionDataObject در ServiceBehaviorAttribute انجام داد. بدین شکل
[ServiceBehavior( IgnoreExtensionDataObject = true )] public class SampleService : ISampleService
DateTime.Now.ToUniversalTime().ToString("r")
<language>fa-IR</language>
<feed xml:lang="fa">