کتابخانهای جاوااسکریپتی برای ساخت صفحات HTML با کمک Bootstrap و JQuery به صورت کشیدن ورهاکردن به همراه امکانات فراوان
کتابخانه itoc.js
It is a jQuery plugin, A toc tree generator from html document.
کتابخانه dynamic-table
کتابخانه chartify
روش کار :
1- دریافت پارامتر ورودی به صورت رشته2- درج عناوین اعداد، ارزش مکانی اعداد صحیح و اعشاری هرکدام در یک جدول
3- جدا کردن ارقام صحیح و اعشاری
4- جداکردن سه رقم سه رقم اعداد صحیح و انتقال آنها به جدول مربوطه
5- Join جداول عناوین و ارقام جدا شده
6- ارسال ارقام اعشاری به همین تابع
7- مشخص کردن ارزش مکانی رقم اعشار
8- اتصال رشته حروف صحیح و اعشاری
بررسی قسمتهای مختلف کد
برای اینکه محدودیتی در تعداد ارقام صحیح و اعشاری نداشته باشیم، پارامتر ورودی را از نوع VARCHAR میگیریم. پس باید ورودی را بررسی کنیم تا رشته عددی باشد.
بررسی رشته ورودی:
-- @pNumber پارامتر ورودی IF LEN(ISNULL(@pNumber, '')) = 0 RETURN NULL IF (PATINDEX('%[^0-9.-]%', @pNumber) > 0) OR (LEN(@pNumber) -LEN(REPLACE(@pNumber, '-', '')) > 1) OR (LEN(@pNumber) -LEN(REPLACE(@pNumber, '.', '')) > 1) OR (CHARINDEX('-', @pNumber) > 1) RETURN 'خطا' IF PATINDEX('%[^0]%', @pNumber) = 0 RETURN 'صفر' IF (CHARINDEX('.', @pNumber) = 1) SET @pNumber='0'+@pNumber DECLARE @Negative AS VARCHAR(5) = ''; IF LEFT(@pNumber, 1) = '-' BEGIN SET @pNumber = SUBSTRING(@pNumber, 2, 100) SET @Negative = 'منفی ' END
- بررسی رشته ورودی برای پیدا کردن کاراکتر غیر عددی، نقطه و منفی. بررسی تعداد علامت منفی و نقطه که بیشتر از یک مورد نباشند، و در نهایت بررسی اینکه علامت منفی در ابتدای رشته ورودی باشد.
- بررسی صفر بودن ورودی(0)، مقدار ورودی شروع شونده با ممیز(0213. ) و مقدار عددی منفی(21210.0021-).
چیز دیگری به ذهنم نرسید!
درج عناوین در جداول مربوطه:
فکر کنم اینجا به علت وجود کاراکترهای فارسی و انگلیسی کد کمی بهم ریخته نمایش داده میشود.
DECLARE @NumberTitle TABLE (val INT,Title NVARCHAR(100)); INSERT INTO @NumberTitle (val,Title) VALUES(0, ''),(1, 'یک') ,(2, 'دو'),(3, 'سه'),(4, 'چهار') ,(5, 'پنج'),(6, 'شش'),(7, 'هفت'),(8, 'هشت') ,(9, 'نه'),(10, 'ده'),(11, 'یازده'),(12, 'دوازده') ,(13, 'سیزده'),(14, 'چهارده'),(15, 'پانزده'),(16, 'شانزده') ,(17, 'هفده'),(18, 'هجده'),(19, 'نوزده'),(20, 'بیست') ,(30, 'سی'),(40, 'چهل'),(50, 'پنجاه'),(60, 'شصت') ,(70, 'هفتاد'),(80, 'هشتاد'),(90, 'نود'),(100, 'صد') ,(200, 'دویست'),(300, 'سیصد'),(400, 'چهارصد'),(500, 'پانصد') ,(600, 'ششصد'),(700, 'هفتصد'),(800, 'هشتصد'),(900, 'نهصد') DECLARE @PositionTitle TABLE (id INT,Title NVARCHAR(100)); INSERT INTO @PositionTitle (id,title) VALUES (1, ''),(2, 'هزار'),(3, 'میلیون'),(4, 'میلیارد'),(5, 'تریلیون') ,(6, 'کوادریلیون'),(7, 'کوینتیلیون'),(8, 'سیکستیلون'),(9, 'سپتیلیون') ,(10, 'اکتیلیون'),(11, 'نونیلیون'),(12, 'دسیلیون') ,(13, 'آندسیلیون'),(14, 'دودسیلیون'),(15, 'تریدسیلیون') ,(16, 'کواتردسیلیون'),(17, 'کویندسیلیون'),(18, 'سیکسدسیلیون') ,(19, 'سپتندسیلیون'),(20, 'اکتودسیلیوم'),(21, 'نومدسیلیون') DECLARE @DecimalTitle TABLE (id INT,Title NVARCHAR(100)); INSERT INTO @DecimalTitle (id,Title) VALUES( 1 ,'دهم' ),(2 , 'صدم'),(3 , 'هزارم') ,(4 , 'ده-هزارم'),(5 , 'صد-هزارم'),(6 , 'میلیون ام') ,(7 , 'ده-میلیون ام'),(8 , 'صد-میلیون ام'),(9 , 'میلیاردم') ,(10 , 'ده-میلیاردم')
جداسازی رقم صحیح و اعشاری:
عدد ورودی ممکن است حالتهای مختلفی داشته باشد مثل: .00002 , 0.000000 , 234.434400000000 , 123.بنابراین براساس ممیز، قسمت صحیح را از اعشاری جدا میکنیم. برای ورودی که با ممیز شروع شود، در ابتدا تابع بررسی میکنیم و عدد صفر را به رشته اضافه میکنیم.
بعد از ممیز و اعداد بزرگتر از یک، با صفرهای بی ارزش چه کنیم؟ شاید اولین چیزی که به ذهن برسد استفاده از حلقه (WHILE) برای حذف صفرهای بی ارزش قسمت ممیز باشد؛ ولی من ترجیح میدهم که از روش دیگری استفاده کنم :
برعکس کردن رشته قسمت اعشاری، پیدا کردن مکان اولین عدد غیر صفر منهای یک ، و کم کردن عدد بدست آمده از طول رشته اعشاری، قسمت مورد نظر ما را برخواهد گرداند:
اما اگر عدد ورودی 20.0 باشد همچنان صفر بی ارزش بعداز ممیز را خواهیم داشت. برای رفع این مشکل کافی است که کاراکتری غیر از صفر را به اول رشته اعشاری اضافه کنیم. من از علامت '?' استفاده کردم. پس به علت اضافه کردن کاراکتر، استارت را از 2 شروع کرده و دیگر نیازی به -1 نخواهیم داشت. با کد زیر قسمت صحیح و اعشاری را بدست میآوریم:SUBSTRING(@DecimalNumber,1, len(@DecimalNumber )-PATINDEX('%[^0]%', REVERSE (@DecimalNumber))-1)
DECLARE @IntegerNumber NVARCHAR(100), @DecimalNumber NVARCHAR(100), @PointPosition INT =case CHARINDEX('.', @pNumber) WHEN 0 THEN LEN(@pNumber)+1 ELSE CHARINDEX('.', @pNumber) END SET @IntegerNumber= LEFT(@pNumber, @PointPosition - 1) SET @DecimalNumber= '?' + SUBSTRING(@pNumber, @PointPosition + 1, LEN(@pNumber)) SET @DecimalNumber= SUBSTRING(@DecimalNumber,2, len(@DecimalNumber )-PATINDEX('%[^0]%', REVERSE (@DecimalNumber))) SET @pNumber= @IntegerNumber
جداد کردن سه رقم سه رقم :
- بدست آوردن یکان، دهگان و صدگان- برای قسمت دهگان، اگر عددی بین 10 تا 19 باشد به صورت کامل (مثلا 15) و در غیر این صورت فقط رقم دهگان. برای بدست آوردن یکان اگر دو رقم آخر بین 10 و 19 بود صفر و در غیر این صورت یکان برگردانده میشود و در جدول MyNumbers درج میگردد.
DECLARE @Number AS INT DECLARE @MyNumbers TABLE (id INT IDENTITY(1, 1), Val1 INT, Val2 INT, Val3 INT) WHILE (@pNumber) <> '0' BEGIN SET @number = CAST(SUBSTRING(@pNumber, LEN(@pNumber) -2, 3)AS INT) INSERT INTO @MyNumbers SELECT (@Number % 1000) -(@Number % 100), CASE WHEN @Number % 100 BETWEEN 10 AND 19 THEN @Number % 100 ELSE (@Number % 100) -(@Number % 10) END, CASE WHEN @Number % 100 BETWEEN 10 AND 19 THEN 0 ELSE @Number % 10 END IF LEN(@pNumber) > 2 SET @pNumber = LEFT(@pNumber, LEN(@pNumber) -3) ELSE SET @pNumber = '0' END
استفاده از JOIN :
JOIN کردن جدول اعداد با عناوین عددی براساس ارزش آنها و JOIN جدول اعداد با جدول ارزش مکانی براساس ID به صورت نزولی(شماره سطر).DECLARE @Str AS NVARCHAR(2000) = ''; SELECT @Str += REPLACE(REPLACE(LTRIM(RTRIM(nt1.Title + ' ' + nt2.Title + ' ' + nt3.title)),' ',' '),' ', ' و ') + ' ' + pt.title + ' و ' FROM @MyNumbers AS mn INNER JOIN @PositionTitle pt ON pt.id = mn.id INNER JOIN @NumberTitle nt1 ON nt1.val = mn.Val1 INNER JOIN @NumberTitle nt2 ON nt2.val = mn.Val2 INNER JOIN @NumberTitle nt3 ON nt3.val = mn.Val3 WHERE (nt1.val + nt2.val + nt3.val > 0) ORDER BY pt.id DESC
Replace بیرونی: جایگزینی فاصلههای خالی با ' و '
همانطور که در بالا اشاره کردم سطرهایی که val2,val1 و val3 آن صفر باشد برای ما بی ارزش هستند، پس آنها را با شرط نوشته شده حذف میکنیم.
بدست آوردن مقدار اعشاری:
خوب! حالا نوبت به عدد اعشاری میرسد. برای بدست آوردن حروف، مقدار اعشاری بدست آمده را به همین تابع ارسال میکنیم و برای بدست آوردن عنوان ارزش مکانی، براساس طول اعشار (ID) آن را در جدول مربوطه پیدا میکنیم.اگر عدد ورودی مثلا 0.355 باشد، تابع باید صفر اول را شناسایی و قسمت عناوین اعشاری را به آن اضافه کند، که این کار با شرط ذیل انجام میشود.
اگر رشته اعشار بدون مقدار باشد، تابع مقدار NULL بر میگرداند (قسمت بررسی رشته ورودی) و هر رشته ای که با NULL جمع شود برابر با NULL خواهد بود. در این صورت با توجه به کد زیر مقداری به رشته Str به عنوان قسمت اعشاری، اضافه نمیگردد.
IF @IntegerNumber='0' SET @Str=CASE WHEN PATINDEX('%[^0]%', @DecimalNumber) > 0 THEN @Negative ELSE '' END + 'صفر' ELSE SET @Str = @Negative + LEFT (@Str, LEN(@Str) -2) DECLARE @PTitle NVARCHAR(100)=ISNULL((SELECT Title FROM @DecimalTitle WHERE id=LEN(@DecimalNumber)),'') SET @Str += ISNULL(' ممیز '+[dbo].[fnNumberToWord_Persian](@DecimalNumber) +' '+@PTitle,'') RETURN @str
مثال: رشته '5445789240.54678000000000'
پنج میلیارد و چهارصد و چهل و پنج میلیون و هفتصد و هشتاد و نه هزار و
دویست و چهل ممیز پنجاه و چهار هزار و ششصد و هفتاد و هشت صد-هزارم دانلود فایل
Summary
JavaScript is a language written for websites to run in the client’s browser.
AJAX is a way for JavaScript to request data from a server without refreshing the page or blocking the application.
jQuery is a JavaScript library built to automate and simplify common web tasks like AJAX or animation.
Angular is a hip JavaScript framework which is made for building large, single-page web applications.
Node.js allows JavaScript to be run without a browser, and is commonly used to run web servers.
<?xml version="1.0" encoding="utf-8"?> <license id="17d46246-a6cb-4196-98a0-ff6fc08cb67f" expiration="2012-06-12T00:00:00.0000000" type="Trial" prof="EFProf"> <name>MyName</name> <Signature xmlns="http://www.w3.org/2000/09/xmldsig#"> <SignedInfo> <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" /> <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" /> <Reference URI=""> <Transforms> <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" /> </Transforms> <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" /> <DigestValue>b8N0bDE4gTakfdGKtzDflmmyyXI=</DigestValue> </Reference> </SignedInfo> <SignatureValue>IPELgc9BbkD8smXSe0sGqp5vS57CtZo9ME2ZfXSq/thVu...=</SignatureValue> </Signature> </license>
در ادامه به نحوه تولید و استفاده از یک چنین مجوزهای امضاء شدهای در برنامههای دات نتی خواهیم پرداخت.
تولید کلیدهای RSA
برای تهیه امضای دیجیتال یک فایل XML نیاز است از الگوریتم RSA استفاده شود.
برای تولید فایل XML امضاء شده، از کلید خصوصی استفاده خواهد شد. برای خواندن اطلاعات مجوز (فایل XML امضاء شده)، از کلیدهای عمومی که در برنامه قرار میگیرند کمک خواهیم گرفت (برای نمونه برنامه EF Prof این کلیدها را در قسمت Resourceهای خود قرار داده است).
استفاده کننده تنها زمانی میتواند مجوز معتبری را تولید کند که دسترسی به کلیدهای خصوصی تولید شده را داشته باشد.
public static string CreateRSAKeyPair(int dwKeySize = 1024) { using (var provider = new RSACryptoServiceProvider(dwKeySize)) { return provider.ToXmlString(includePrivateParameters: true); } }
تهیه ساختار مجوز
در ادامه یک enum که بیانگر انواع مجوزهای برنامه ما است را مشاهده میکنید:
namespace SignedXmlSample { public enum LicenseType { None, Trial, Standard, Personal } }
using System; using System.Xml.Serialization; namespace SignedXmlSample { public class License { [XmlAttribute] public Guid Id { set; get; } public string Domain { set; get; } [XmlAttribute] public string IssuedTo { set; get; } [XmlAttribute] public DateTime Expiration { set; get; } [XmlAttribute] public LicenseType Type { set; get; } } }
تولید و خواندن مجوز دارای امضای دیجیتال
کدهای کامل کلاس تولید و خواندن یک مجوز دارای امضای دیجیتال را در اینجا مشاهده میکنید:
using System; using System.IO; using System.Security.Cryptography; // needs a ref. to `System.Security.dll` asm. using System.Security.Cryptography.Xml; using System.Text; using System.Xml; using System.Xml.Serialization; namespace SignedXmlSample { public static class LicenseGenerator { public static string CreateLicense(string licensePrivateKey, License licenseData) { using (var provider = new RSACryptoServiceProvider()) { provider.FromXmlString(licensePrivateKey); var xmlDocument = createXmlDocument(licenseData); var xmlDigitalSignature = getXmlDigitalSignature(xmlDocument, provider); appendDigitalSignature(xmlDocument, xmlDigitalSignature); return xmlDocumentToString(xmlDocument); } } public static string CreateRSAKeyPair(int dwKeySize = 1024) { using (var provider = new RSACryptoServiceProvider(dwKeySize)) { return provider.ToXmlString(includePrivateParameters: true); } } public static License ReadLicense(string licensePublicKey, string xmlFileContent) { var doc = new XmlDocument(); doc.LoadXml(xmlFileContent); using (var provider = new RSACryptoServiceProvider()) { provider.FromXmlString(licensePublicKey); var nsmgr = new XmlNamespaceManager(doc.NameTable); nsmgr.AddNamespace("sig", "http://www.w3.org/2000/09/xmldsig#"); var xml = new SignedXml(doc); var signatureNode = (XmlElement)doc.SelectSingleNode("//sig:Signature", nsmgr); if (signatureNode == null) throw new InvalidOperationException("This license file is not signed."); xml.LoadXml(signatureNode); if (!xml.CheckSignature(provider)) throw new InvalidOperationException("This license file is not valid."); var ourXml = xml.GetXml(); if (ourXml.OwnerDocument == null || ourXml.OwnerDocument.DocumentElement == null) throw new InvalidOperationException("This license file is coruppted."); using (var reader = new XmlNodeReader(ourXml.OwnerDocument.DocumentElement)) { var xmlSerializer = new XmlSerializer(typeof(License)); return (License)xmlSerializer.Deserialize(reader); } } } private static void appendDigitalSignature(XmlDocument xmlDocument, XmlNode xmlDigitalSignature) { xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(xmlDigitalSignature, true)); } private static XmlDocument createXmlDocument(License licenseData) { var serializer = new XmlSerializer(licenseData.GetType()); var sb = new StringBuilder(); using (var writer = new StringWriter(sb)) { var ns = new XmlSerializerNamespaces(); ns.Add("", ""); serializer.Serialize(writer, licenseData, ns); var doc = new XmlDocument(); doc.LoadXml(sb.ToString()); return doc; } } private static XmlElement getXmlDigitalSignature(XmlDocument xmlDocument, AsymmetricAlgorithm key) { var xml = new SignedXml(xmlDocument) { SigningKey = key }; var reference = new Reference { Uri = "" }; reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); xml.AddReference(reference); xml.ComputeSignature(); return xml.GetXml(); } private static string xmlDocumentToString(XmlDocument xmlDocument) { using (var ms = new MemoryStream()) { var settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 }; var xmlWriter = XmlWriter.Create(ms, settings); xmlDocument.Save(xmlWriter); ms.Position = 0; return new StreamReader(ms).ReadToEnd(); } } } }
در حین کار با متد CreateLicense، پارامتر licensePrivateKey همان اطلاعاتی است که به کمک متد CreateRSAKeyPair قابل تولید است. توسط پارامتر licenseData، اطلاعات مجوز در حال تولید اخذ میشود. در این متد به کمک provider.FromXmlString، اطلاعات کلیدهای RSA دریافت خواهند شد. سپس توسط متد createXmlDocument، محتوای licenseData دریافتی به یک فایل XML نگاشت میگردد (بنابراین اهمیتی ندارد که حتما از ساختار کلاس مجوز یاد شده استفاده کنید). در ادامه متد getXmlDigitalSignature با در اختیار داشتن معادل XML شیء مجوز و کلیدهای لازم، امضای دیجیتال متناظری را تولید میکند. با استفاده از متد appendDigitalSignature، این امضاء را به فایل XML اولیه اضافه میکنیم. از این امضاء جهت بررسی اعتبار مجوز برنامه در متد ReadLicense استفاده خواهد شد.
برای خواندن یک فایل مجوز امضاء شده در برنامه خود میتوان از متد ReadLicense استفاده کرد. توسط آرگومان licensePublicKey، اطلاعات کلید عمومی دریافت میشود. این کلید دربرنامه، ذخیره و توزیع میگردد. پارامتر xmlFileContent معادل محتوای فایل XML مجوزی است که قرار است مورد ارزیابی قرار گیرد.
مثالی در مورد نحوه استفاده از کلاس تولید مجوز
در ادامه نحوه استفاده از متدهای CreateLicense و ReadLicense را ملاحظه میکنید؛ به همراه آشنایی با نمونه کلیدهایی که باید به همراه برنامه منتشر شوند:
using System; using System.IO; namespace SignedXmlSample { class Program { static void Main(string[] args) { //Console.WriteLine(LicenseGenerator.CreateRSAKeyPair()); writeLicense(); readLicense(); Console.WriteLine("Press a key..."); Console.ReadKey(); } private static void readLicense() { var xml = File.ReadAllText("License.xml"); const string publicKey = @"<RSAKeyValue> <Modulus> mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc= </Modulus> <Exponent> AQAB </Exponent> </RSAKeyValue>"; var result = LicenseGenerator.ReadLicense(publicKey, xml); Console.WriteLine(result.Domain); Console.WriteLine(result.IssuedTo); } private static void writeLicense() { const string rsaData = @"<RSAKeyValue> <Modulus> mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc= </Modulus> <Exponent> AQAB </Exponent> <P> xwPKN77EcolMTD2O2Csv6k9Y4aen8UBVYjeQ4PtrNGz0Zx6I1MxLEFzRpiKC/Ney3xKg0Icwj0ebAQ04d5+HAQ== </P> <Q> w568t0Xe6OBUfCyAuo7tTv4eLgczHntVLpjjcxdUksdVw7NJtlnOLApJVJ+U6/85Z7Ji+eVhuN91yn04pQkAtw== </Q> <DP> svkEjRdA4WP5uoKNiHdmMshCvUQh8wKRBq/D2aAgq9fj/yxlj0FdrAxc+ZQFyk5MbPH6ry00jVWu3sY95s4PAQ== </DP> <DQ> WcRsIUYk5oSbAGiDohiYeZlPTBvtr101V669IUFhhAGJL8cEWnOXksodoIGimzGBrD5GARrr3yRcL1GLPuCEvQ== </DQ> <InverseQ> wIbuKBZSCioG6MHdT1jxlv6U1+Y3TX9sHED9PqGzWWpVGA+xFJmQUxoFf/SvHzwbBlXnG0DLqUvxEv+BkEid2w== </InverseQ> <D> Yk21yWdT1BfXqlw30NyN7qNWNuM/Uvh2eaRkCrhvFTckSucxs7st6qig2/RPIwwfr6yIc/bE/TRO3huQicTpC2W3aXsBI9822OOX4BdWCec2txXpSkbZW24moXu+OSHfAdYoOEN6ocR7tAGykIqENshRO7HvONJsOE5+1kF2GAE= </D> </RSAKeyValue>"; string data = LicenseGenerator.CreateLicense( rsaData, new License { Id = Guid.NewGuid(), Domain = "dotnettips.info", Expiration = DateTime.Now.AddYears(2), IssuedTo = "VahidN", Type = LicenseType.Standard }); File.WriteAllText("License.xml", data); } } }
همانطور که مشاهده میکنید، اطلاعات کامل یک نمونه از آن، در متد writeLicense مورد نیاز است. اما در متد readLicense تنها به قسمت عمومی آن یعنی Modulus و Exponent نیاز خواهد بود (موارد قابل انتشار به همراه برنامه).
سؤال: امنیت این روش تا چه اندازه است؟
پاسخ: تا زمانیکه کاربر نهایی به کلیدهای خصوصی شما دسترسی پیدا نکند، امکان تولید معادل آنها تقریبا در حد صفر است و به طول عمر او قد نخواهد داد!
اما ... مهاجم میتواند کلیدهای عمومی و خصوصی خودش را تولید کند. مجوز دلخواهی را بر این اساس تهیه کرده و سپس کلید عمومی جدید را در برنامه، بجای کلیدهای عمومی شما درج (patch) کند! بنابراین روش بررسی اینکه آیا برنامه جاری patch شده است یا خیر را فراموش نکنید. یا عموما مطابق معمول قسمتی از برنامه که در آن مقایسهای بین اطلاعات دریافتی و اطلاعات مورد انتظار وجود دارد، وصله میشوند؛ این مورد عمومی است و منحصر به روش جاری نمیباشد.
دریافت نسخه جنریک این مثال:
SignedXmlSample.zip
Iterators در ES 6
هر Iterator شیءایی است که دارای متد next میباشد. هر بار که این متد فراخوانی میشود، عضو بعدی مجموعه، بازگشت داده خواهد شد. خروجی هر مرحله، درون یک شیء با دو خاصیت value و done قرار داده میشود. value، مقدار مرحلهی بعد است و done مشخص میکند که آیا به پایان مجموعه رسیدهایم یا خیر (بنابراین در اینجا تعداد اعضای Iterator مشخص نیست).
مثالی از پیمایش یک آرایه با چندین روش مختلف
در مثال زیر، آرایهای از اعداد را داریم که نیاز است جمع اعضای آن محاسبه شود:
let sum = 0; let numbers = [1,2,3,4];
// for loop sum = 0; for(let i =0; i < numbers.length; i++){ sum += numbers[i]; } //sum = 10
// for in sum = 0; for(let i in numbers) { sum += numbers[i]; } //sum = 10
// iterator sum = 0; let iterator = number.values(); let next = iterator.next(); while(!next.done){ sum += next.value; next = iterator.next(); } //sum = 10
مرحلهی بعدی، فراخوانی متد next این Iterator است. این عملیات باید در طی یک حلقه، تا پایان کار Iterator انجام شود. همانطور که در ابتدای بحث نیز عنوان شد، خروجی متد next یک شیء است که دارای خواص value و done میباشد. اگر done مساوی true شد، یعنی به پایان کار پیمایش رسیدهایم.
البته هدف از این مثال، صرفا نمایش سطح پایین کار با Iterators بود. در عمل از حلقهی جدیدی به نام for of برای انجام این پیمایش استفاده میشود.
معرفی حلقهی for of
جاوا اسکریپت سالها است که دارای حلقهی for in میباشد و نمونهای از کاربرد آنرا در مثال قبل مشاهده کردید. اگر این حلقه بر روی آرایهها فراخوانی شود، هربار ایندکس پیمایش شده را بازگشت میدهد و اگر بر روی یک شیء فراخوانی شود، خواص آن شیء را بازگشت میدهد:
var person = { first: "Vahid", last "N" }; for(let i in person) { console.log(person[i]); }
چون این حلقه صرفا ایندکسها و کلیدها را بازگشت میدهد، جهت کار با Iterators که نیاز است به مقادیر اعضاء دسترسی پیدا کنیم، مناسب نیست. به همین جهت در ES 6، حلقهی جدیدی به نام for of برای کار با Iterators معرفی شدهاست:
let numbers = [1,2,3,4]; for(let i of numbers) { console.log(i); }
let sum = 0; let numbers = [1,2,3,4]; for(let n of numbers){ sum += n; }
for of یکی از روشهای پیمایش Iterators است. پارامترهای rest و همچنین Array.from نیز چنین قابلیتی را فراهم میکنند.
امکان پیاده سازی Iterators سفارشی نیز وجود دارد که پیشنیاز آن، درک مبحث جدید Symbols است که به صورت جداگانهای بررسی خواهد شد.
همانطور که در قسمت اول گفته شد، React برای اینکه بتواند تگها را در زمان اجرا و به صورت پویا به روز کند، وضعیت فعلی تگها را دنبال میکند و در صورت وقوع تغییرات، تگها را به روز میکند. به این حالت Stateful گفته میشود. تگهای ساخته شده توسط React دو وضعیت را دارند. یکی وضعیت اولیه که به مرورگر ارسال شده، در حال نمایش است و ثابت، و دیگری در پشت زمینه در فایل جاوااسکریپت، در انتظار وقوع تغییری. React این دو وضعیت را با هم مقایسه میکند و اگر بین آنها تفاوتی وجود داشت، تغییرات را اعمال میکند.
در React.createClass به همراه متدهای داخلی React میتوانیم برای یک کامپوننت، وضعیتی اولیه را مشخص کنیم، تغییرات را دنبال کنیم و وضعیت فعلی را تغییر دهیم. برای روشن شدن نحوه کار، مثال قسمت قبل را که یک منو از نوشیدنیها بود، اینطور تغییر میدهیم که کاربر بتواند با inputها و یک دکمه، به لیست نوشیدنیها، مورد تازهای را اضافه کند:
var hotDrinks = [ { item: "Tea", price: "7000" }, { item: "Espresso", price: "10000" }, { item: "Hot Chocolate", price: "12000" } ]; var MenuItem = React.createClass({ render: function () { return ( <li className="list-group-item"> <span className="badge">{this.props.price}</span> <p>{this.props.item}</p> </li> ) } }); var Menu = React.createClass({ getInitialState: function () { return { menuList: this.props.data }; }, componentDidMount: function () { var component = this; $("#btnAddNewItem").click(function () { component.state.menuList.push( { item: $("#textInputItemName").val(), price: $("#textInputItemPrice").val() }); component.setState({ menuList: component.state.menuList }); }); }, render: function () { return ( <div className="row"> <div className="col-md-4"> <ul className="list-group"> {this.state.menuList.map(item => <MenuItem {...item} />)} </ul> </div> </div> ) } }); ReactDOM.render( <Menu data={hotDrinks} />, document.getElementById("reactTestContainer") );
توضیح کامپوننت Menu
getInitialState، componentDidMount، setState، state و render همگی از کتابخانه React هستند. اگر intelisense و code snippets مخصوص React را در VSCode نصب کرده باشید، دسترسی به سایر متدها و خاصیتهای کتابخانه سادهتر است.
شیء state، وضعیت کنونی کامپوننت است. وقتی دادهای را به state اختصاص میدهیم، آن را به عنوان وضعیت اولیه در نظر میگیرد. با تغییر داده، React وضعیت کامپوننت را تغییر یافته حساب میکند و به صورت خودکار تگها را دوباره با دادههای تازه میسازد. دادههای state همان دادههایی هستند که تگها با آنها ساخته میشوند؛ در بخش render.
getInitialState مثل یک سازنده عمل میکند؛ مقدار ورودی کامپوننت را به یک شیء اختصاص میدهد و آن را برمیگرداند. به کجا؟ به state. یعنی menuList عضوی از شیء state میشود. در مثال بالا و در این متد، لیست نوشیدنیها به menuList اعمال میشود.
componentDidMount باید حتما قبل render تعریف شود، به این دلیل که زمان اجرایش باید حتما بعد از اولین render باشد. این متد وظیفه دارد تغییرات مورد نظر ما را در سطح کد یا رابط کاربری دنبال کند. اگر تغییر دلخواهی به وجود آمد، وضعیت کامپوننت را به روز میکند که بعد از آن React به صورت خودکار تگها را دوباره میسازد. در مثال بالا متد به رویداد کلیک یک دکمه گوش میدهد. اگر کلیک زده شد، نام نوشیدنی جدید و قیمت آن را از inputها میخواند و به عنوان یک آیتم جدید به menuList در state اضافه میکند. اما هنوز یک قدم مانده و بدون آن React، شیء state را تغییر یافته به حساب نمیآورد. در بخش setState وضعیت جاری کامپوننت را با تغییرات اعمال شده، جایگزین میکنیم. در این نقطه React به صورت خودکار به سراغ render میرود و ادامه داستان!
همانطور که قبلا گفته شد، React.createClass و React.Component فقط در Syntax با هم تفاوت دارند. در نتیجه این مثال را میشود در حالت React.Component هم اجرا کرد.
در قسمت بعد موضوع دیگری را به نام Composability شرح میدهیم. مبحثی ساده با مثال که نشان میدهد چطور کامپوننتها را مستقل از هم بسازیم و در عین حال با هم استفاده کنیم.
کلمههای عبور کاربران فعلی سیستم با الگوریتمی متفاوت از الگوریتم مورد استفاده Identity هش شدهاند. برای اینکه کاربرانی که قبلا ثبت نام کرده بودند بتوانند با کلمههای عبور خود وارد سایت شوند، باید الگوریتم هش کردن Identity را با الگوریتم فعلی مورد استفاده Iris جایگزین کرد.
برای تغییر روش هش کردن کلمات عبور در Identity باید اینترفیس IPasswordHasher را پیاده سازی کنید:
public class IrisPasswordHasher : IPasswordHasher { public string HashPassword(string password) { return Utilities.Security.Encryption.EncryptingPassword(password); } public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { return Utilities.Security.Encryption.VerifyPassword(providedPassword, hashedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed; } }
سپس باید وارد کلاس ApplicationUserManager شده و در سازندهی آن اینترفیس IPasswordHasher را به عنوان وابستگی تعریف کنید:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store, IUnitOfWork uow, IIdentity identity, IApplicationRoleManager roleManager, IDataProtectionProvider dataProtectionProvider, IIdentityMessageService smsService, IIdentityMessageService emailService, IPasswordHasher passwordHasher) : base(store) { _store = store; _uow = uow; _identity = identity; _users = _uow.Set<ApplicationUser>(); _roleManager = roleManager; _dataProtectionProvider = dataProtectionProvider; this.SmsService = smsService; this.EmailService = emailService; PasswordHasher = passwordHasher; createApplicationUserManager(); }
برای اینکه کلاس IrisPasswordHasher را به عنوان نمونه درخواستی IPasswordHasher معرفی کنیم، باید در تنظیمات StructureMap کد زیر را نیز اضافه کنید:
x.For<IPasswordHasher>().Use<IrisPasswordHasher>();
پیاده سازی اکشن متد ثبت نام کاربر با استفاده از Identity
در کنترلر UserController، اکشن متد Register را به شکل زیر بازنویسی کنید:
[HttpPost] [ValidateAntiForgeryToken] [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")] public virtual async Task<ActionResult> Register(RegisterModel model) { if (ModelState.IsValid) { var user = new ApplicationUser { CreatedDate = DateAndTime.GetDateTime(), Email = model.Email, IP = Request.ServerVariables["REMOTE_ADDR"], IsBaned = false, UserName = model.UserName, UserMetaData = new UserMetaData(), LastLoginDate = DateAndTime.GetDateTime() }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { var addToRoleResult = await _userManager.AddToRoleAsync(user.Id, "user"); if (addToRoleResult.Succeeded) { var code = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "User", new { userId = user.Id, code }, protocol: Request.Url.Scheme); _emailService.SendAccountConfirmationEmail(user.Email, callbackUrl); return Json(new { result = "success" }); } addErrors(addToRoleResult); } addErrors(result); } return PartialView(MVC.User.Views._Register, model); }
برای این کار متد زیر را به کلاس EmailService اضافه کنید:
public SendingMailResult SendAccountConfirmationEmail(string email, string link) { var model = new ConfirmEmailModel() { ActivationLink = link }; var htmlText = _viewConvertor.RenderRazorViewToString(MVC.EmailTemplates.Views._ConfirmEmail, model); var result = Send(new MailDocument { Body = htmlText, Subject = "تایید حساب کاربری", ToEmail = email }); return result; }
@model Iris.Model.EmailModel.ConfirmEmailModel <div style="direction: rtl; -ms-word-wrap: break-word; word-wrap: break-word;"> <p>با سلام</p> <p>برای فعال سازی حساب کاربری خود لطفا بر روی لینک زیر کلیک کنید:</p> <p>@Model.ActivationLink</p> <div style=" color: #808080;"> <p>با تشکر</p> <p>@Model.SiteTitle</p> <p>@Model.SiteDescription</p> <p><span style="direction: ltr !important; unicode-bidi: embed;">@Html.ConvertToPersianDateTime(DateTime.Now, "s,H")</span></p> </div> </div>
اصلاح پیام موفقیت آمیز بودن ثبت نام کاربر جدید
سیستم IRIS از ارسال ایمیل تایید حساب کاربری استفاده نمیکند و به محض اینکه عملیات ثبت نام تکمیل میشد، صفحه رفرش میشود. اما در سیستم Identity یک ایمیل حاوی لینک فعال سازی حساب کاربری به او ارسال میشود.
برای اصلاح پیغام پس از ثبت نام، باید به فایل myscript.js درون پوشهی Scripts مراجعه کرده و رویداد onSuccess شیء RegisterUser را به شکل زیراصلاح کنید:
RegisterUser.Form.onSuccess = function (data) { if (data.result == "success") { var message = '<div id="alert"><button type="button" data-dismiss="alert">×</button>ایمیلی حاوی لینک فعال سازی، به ایمیل شما ارسال شد؛ لطفا به ایمیل خود مراجعه کرده و بر روی لینک فعال سازی کلیک کنید.</div>'; $('#registerResult').html(message); } else { $('#logOnModal').html(data); } };
[AllowAnonymous] public virtual async Task<ActionResult> ConfirmEmail(int? userId, string code) { if (userId == null || code == null) { return View("Error"); } var result = await _userManager.ConfirmEmailAsync(userId.Value, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); }
@{ ViewBag.Title = "حساب کاربری شما تایید شد"; } <h2>@ViewBag.Title.</h2> <div> <p> با تشکر از شما، حساب کاربری شما تایید شد. </p> <p> @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" }) </p> </div>
اصلاح اکشن متد ورود به سایت
[HttpPost] [ValidateAntiForgeryToken] public async virtual Task<ActionResult> LogOn(LogOnModel model, string returnUrl) { if (!ModelState.IsValid) { if (Request.IsAjaxRequest()) return PartialView(MVC.User.Views._LogOn, model); return View(model); } const string emailRegPattern = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"; string ip = Request.ServerVariables["REMOTE_ADDR"]; SignInStatus result = SignInStatus.Failure; if (Regex.IsMatch(model.Identity, emailRegPattern)) { var user = await _userManager.FindByEmailAsync(model.Identity); if (user != null) { result = await _signInManager.PasswordSignInAsync (user.UserName, model.Password, model.RememberMe, shouldLockout: true); } } else { result = await _signInManager.PasswordSignInAsync(model.Identity, model.Password, model.RememberMe, shouldLockout: true); } switch (result) { case SignInStatus.Success: if (Request.IsAjaxRequest()) return JavaScript(IsValidReturnUrl(returnUrl) ? string.Format("window.location ='{0}';", returnUrl) : "window.location.reload();"); return redirectToLocal(returnUrl); case SignInStatus.LockedOut: ModelState.AddModelError("", string.Format("حساب شما قفل شد، لطفا بعد از {0} دقیقه دوباره امتحان کنید.", _userManager.DefaultAccountLockoutTimeSpan.Minutes)); break; case SignInStatus.Failure: ModelState.AddModelError("", "نام کاربری یا کلمه عبور اشتباه است."); break; default: ModelState.AddModelError("", "در ورود شما خطایی رخ داده است."); break; } if (Request.IsAjaxRequest()) return PartialView(MVC.User.Views._LogOn, model); return View(model); }
اصلاح اکشن متد خروج کاربر از سایت
[HttpPost] [ValidateAntiForgeryToken] [Authorize] public virtual ActionResult LogOut() { _authenticationManager.SignOut(); if (Request.IsAjaxRequest()) return Json(new { result = "true" }); return RedirectToAction(MVC.User.ActionNames.LogOn, MVC.User.Name); }
پیاده سازی ریست کردن کلمهی عبور با استفاده از ASP.NET Identity
مکانیزم سیستم IRIS برای ریست کردن کلمهی عبور به هنگام فراموشی آن، ساخت GUID و ذخیرهی آن در دیتابیس است. سیستم Identity با استفاده از یک توکن رمز نگاری شده و بدون استفاده از دیتابیس، این کار را انجام میدهد و با استفاده از قابلیتهای تو کار سیستم Identity، تمهیدات امنیتی بهتری را نسبت به سیستم کنونی در نظر گرفته است.
برای این کار کدهای کنترلر ForgottenPasswordController را به شکل زیر ویرایش کنید:
using System.Threading.Tasks; using System.Web.Mvc; using CaptchaMvc.Attributes; using Iris.Model; using Iris.Servicelayer.Interfaces; using Iris.Web.Email; using Microsoft.AspNet.Identity; namespace Iris.Web.Controllers { public partial class ForgottenPasswordController : Controller { private readonly IEmailService _emailService; private readonly IApplicationUserManager _userManager; public ForgottenPasswordController(IEmailService emailService, IApplicationUserManager applicationUserManager) { _emailService = emailService; _userManager = applicationUserManager; } [HttpGet] public virtual ActionResult Index() { return PartialView(MVC.ForgottenPassword.Views._Index); } [HttpPost] [ValidateAntiForgeryToken] [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")] public async virtual Task<ActionResult> Index(ForgottenPasswordModel model) { if (!ModelState.IsValid) { return PartialView(MVC.ForgottenPassword.Views._Index, model); } var user = await _userManager.FindByEmailAsync(model.Email); if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.Id))) { // Don't reveal that the user does not exist or is not confirmed return Json(new { result = "false", message = "این ایمیل در سیستم ثبت نشده است" }); } var code = await _userManager.GeneratePasswordResetTokenAsync(user.Id); _emailService.SendResetPasswordConfirmationEmail(user.UserName, user.Email, code); return Json(new { result = "true", message = "ایمیلی برای تایید بازنشانی کلمه عبور برای شما ارسال شد.اعتبارایمیل ارسالی 3 ساعت است." }); } [AllowAnonymous] public virtual ActionResult ResetPassword(string code) { return code == null ? View("Error") : View(); } [AllowAnonymous] public virtual ActionResult ResetPasswordConfirmation() { return View(); } // // POST: /Account/ResetPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> ResetPassword(ResetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { // Don't reveal that the user does not exist return RedirectToAction("Error"); } var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction("ResetPasswordConfirmation", "ForgottenPassword"); } addErrors(result); return View(); } private void addErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError("", error); } } } }
همچنین برای اکشن متدهای اضافه شده، Viewهای زیر را نیز باید اضافه کنید:
- View با نام ResetPasswordConfirmation.cshtml را اضافه کنید.
@{ ViewBag.Title = "کلمه عبور شما تغییر کرد"; } <hgroup> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> کلمه عبور شما با موفقیت تغییر کرد </p> <p> @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" }) </p> </div>
- View با نام ResetPassword.cshtml
@model Iris.Model.ResetPasswordViewModel @{ ViewBag.Title = "ریست کردن کلمه عبور"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ResetPassword", "ForgottenPassword", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() <h4>ریست کردن کلمه عبور</h4> <hr /> @Html.ValidationSummary("", new { @class = "text-danger" }) @Html.HiddenFor(model => model.Code) <div> @Html.LabelFor(m => m.Email, "ایمیل", new { @class = "control-label" }) <div> @Html.TextBoxFor(m => m.Email) </div> </div> <div> @Html.LabelFor(m => m.Password, "کلمه عبور", new { @class = "control-label" }) <div> @Html.PasswordFor(m => m.Password) </div> </div> <div> @Html.LabelFor(m => m.ConfirmPassword, "تکرار کلمه عبور", new { @class = "control-label" }) <div> @Html.PasswordFor(m => m.ConfirmPassword) </div> </div> <div> <div> <input type="submit" value="تغییر کلمه عبور" /> </div> </div> }
همچنین این View و Controller متناظر آن، احتیاج به ViewModel زیر دارند که آن را به پروژهی Iris.Models اضافه کنید.
using System.ComponentModel.DataAnnotations; namespace Iris.Model { public class ResetPasswordViewModel { [Required] [EmailAddress] [Display(Name = "ایمیل")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "کلمه عبور باید حداقل 6 حرف باشد", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "کلمه عبور")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "تکرار کلمه عبور")] [Compare("Password", ErrorMessage = "کلمه عبور و تکرارش یکسان نیستند")] public string ConfirmPassword { get; set; } public string Code { get; set; } } }
حذف سیستم قدیمی احراز هویت
برای حذف کامل سیستم احراز هویت IRIS، وارد فایل Global.asax.cs شده و سپس از متد Application_AuthenticateRequest کدهای زیر را حذف کنید:
var principalService = ObjectFactory.GetInstance<IPrincipalService>(); var formsAuthenticationService = ObjectFactory.GetInstance<IFormsAuthenticationService>(); context.User = principalService.GetCurrent()
فارسی کردن خطاهای ASP.NET Identity
سیستم Identity، پیامهای خطاها را از فایل Resource موجود در هستهی خود، که به طور پیش فرض، زبان آن انگلیسی است، میخواند. برای مثال وقتی ایمیلی تکراری باشد، پیامی به زبان انگلیسی دریافت خواهید کرد و متاسفانه برای تغییر آن، راه سر راست و واضحی وجود ندارد. برای تغییر این پیامها میتوان از سورس باز بودن Identity استفاده کنید و قسمتی را که پیامها را تولید میکند، خودتان با پیامهای فارسی باز نویسی کنید.
راه اول این است که از این پروژه استفاده کرد و کلاسهای زیر را به پروژه اضافه کنید:
public class CustomUserValidator<TUser, TKey> : IIdentityValidator<ApplicationUser> where TUser : class, IUser<int> where TKey : IEquatable<int> { public bool AllowOnlyAlphanumericUserNames { get; set; } public bool RequireUniqueEmail { get; set; } private ApplicationUserManager Manager { get; set; } public CustomUserValidator(ApplicationUserManager manager) { if (manager == null) throw new ArgumentNullException("manager"); AllowOnlyAlphanumericUserNames = true; Manager = manager; } public virtual async Task<IdentityResult> ValidateAsync(ApplicationUser item) { if (item == null) throw new ArgumentNullException("item"); var errors = new List<string>(); await ValidateUserName(item, errors); if (RequireUniqueEmail) await ValidateEmailAsync(item, errors); return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()); } private async Task ValidateUserName(ApplicationUser user, ICollection<string> errors) { if (string.IsNullOrWhiteSpace(user.UserName)) errors.Add("نام کاربری نباید خالی باشد"); else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, "^[A-Za-z0-9@_\\.]+$")) { errors.Add("برای نام کاربری فقط از کاراکترهای مجاز استفاده کنید "); } else { var owner = await Manager.FindByNameAsync(user.UserName); if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id)) errors.Add("این نام کاربری قبلا ثبت شده است"); } } private async Task ValidateEmailAsync(ApplicationUser user, ICollection<string> errors) { var email = await Manager.GetEmailStore().GetEmailAsync(user).WithCurrentCulture(); if (string.IsNullOrWhiteSpace(email)) { errors.Add("وارد کردن ایمیل ضروریست"); } else { try { var m = new MailAddress(email); } catch (FormatException) { errors.Add("ایمیل را به شکل صحیح وارد کنید"); return; } var owner = await Manager.FindByEmailAsync(email); if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id)) errors.Add("این ایمیل قبلا ثبت شده است"); } } }
public class CustomPasswordValidator : IIdentityValidator<string> { #region Properties public int RequiredLength { get; set; } public bool RequireNonLetterOrDigit { get; set; } public bool RequireLowercase { get; set; } public bool RequireUppercase { get; set; } public bool RequireDigit { get; set; } #endregion #region IIdentityValidator public virtual Task<IdentityResult> ValidateAsync(string item) { if (item == null) throw new ArgumentNullException("item"); var list = new List<string>(); if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength) list.Add(string.Format("کلمه عبور نباید کمتر از 6 کاراکتر باشد")); if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit)) list.Add("برای امنیت بیشتر از حداقل از یک کارکتر غیر عددی و غیر حرف برای کلمه عبور استفاده کنید"); if (RequireDigit && item.All(c => !IsDigit(c))) list.Add("برای امنیت بیشتر از اعداد هم در کلمه عبور استفاده کنید"); if (RequireLowercase && item.All(c => !IsLower(c))) list.Add("از حروف کوچک نیز برای کلمه عبور استفاده کنید"); if (RequireUppercase && item.All(c => !IsUpper(c))) list.Add("از حروف بزرک نیز برای کلمه عبور استفاده کنید"); return Task.FromResult(list.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(string.Join(" ", list))); } #endregion #region PrivateMethods public virtual bool IsDigit(char c) { if (c >= 48) return c <= 57; return false; } public virtual bool IsLower(char c) { if (c >= 97) return c <= 122; return false; } public virtual bool IsUpper(char c) { if (c >= 65) return c <= 90; return false; } public virtual bool IsLetterOrDigit(char c) { if (!IsUpper(c) && !IsLower(c)) return IsDigit(c); return true; } #endregion }
سپس باید کلاسهای فوق را به Identity معرفی کنید تا از این کلاسهای سفارشی شده به جای کلاسهای پیش فرض خودش استفاده کند. برای این کار وارد کلاس ApplicationUserManager شده و درون متد createApplicationUserManager کدهای زیر را اضافه کنید:
UserValidator = new CustomUserValidator< ApplicationUser, int>(this) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = false, RequireUppercase = false };
ایجاد SecurityStamp برای کاربران فعلی سایت
سیستم Identity برای لحاظ کردن یک سری موارد امنیتی، به ازای هر کاربر، فیلدی را به نام SecurityStamp درون دیتابیس ذخیره میکند و برای این که این سیستم عملکرد صحیحی داشته باشد، باید این مقدار را برای کاربران فعلی سایت ایجاد کرد تا کاربران فعلی بتوانند از امکانات Identity نظیر فراموشی کلمه عبور، ورود به سیستم و ... استفاده کنند.
برای این کار Identity، متدی به نام UpdateSecurityStamp را در اختیار قرار میدهد تا با استفاده از آن بتوان مقدار فیلد SecurityStamp را به روز رسانی کرد.
معمولا برای انجام این کارها میتوانید یک کنترلر تعریف کنید و درون اکشن متد آن کلیهی کاربران را واکشی کرده و سپس متد UpdateSecurityStamp را بر روی آنها فراخوانی کنید.
public virtual async Task<ActionResult> UpdateAllUsersSecurityStamp() { foreach (var user in await _userManager.GetAllUsersAsync()) { await _userManager.UpdateSecurityStampAsync(user.Id); } return Content("ok"); }
انتقال نقشهای کاربران به جدول جدید و برقراری رابطه بین آنها
در سیستم Iris رابطهی بین کاربران و نقشها یک به چند بود. در سیستم Identity این رابطه چند به چند است و من به عنوان یک حرکت خوب و رو به جلو، رابطهی چند به چند را در سیستم جدید انتخاب کردم. اکنون با استفاده از دستورات زیر به راحتی میتوان نقشهای فعلی و رابطهی بین آنها را به جداول جدیدشان منتقل کرد:
public virtual async Task<ActionResult> CopyRoleToNewTable() { var dbContext = new IrisDbContext(); foreach (var role in await dbContext.Roles.ToListAsync()) { await _roleManager.CreateAsync(new CustomRole(role.Name) { Description = role.Description }); } var users = await dbContext.Users.Include(u => u.Role).ToListAsync(); foreach (var user in users) { await _userManager.AddToRoleAsync(user.Id, user.Role.Name); } return Content("ok"); }