اشتراکها
اشتراکها
راهنمای جدید NIST در مورد اعتبارسنجی
نظرات مطالب
یافتن مقادیر نال در Entity framework
TOTPیک الگوریتمی است که از ساعت برای تولید رمزهای یکبارمصرف استفاده میکند. به این صورت که در هر لحظه یک کد منحصر به فرد تولید خواهد شد. اگر با برنامه Google Authenticator کار کرده باشید این مفهوم برایتان اشناست.
در این مطلب میخواهیم سناریویی را پیاده سازی کنیم که برای فراخوانی APIها باید یک رمز منحصر به فرد همراه توکن ارسال کنند. برای انجام این کار هر کاربر و یا کلاینتی که بخواهد از API استفاده کند در ابتدا باید لاگین کند و بعد از لاگین یک ClientSecret به طول 16 کاراکتر به همراه توکن به او ارسال میکنیم. از ClientSecret برای رمزنگاری کد ارسال شده ( TOTP ) استفاده میکنیم. مانند Public/Private key یک Key ثابت در سمت سرور و یک Key ثابت در برنامه موبایل وجود دارد و هنگام رمزنگاری TOTP علاوه بر Key موجود در موبایل و سرور از ClientSecret خود کاربر هم استفاده میکنیم تا TOTP بوجود آمده کاملا منحصر به فرد باشد. ( مقدار TOTP را با استفاده از دو کلید Key و ClientSecret رمزنگاری میکنیم ).
در این مثال تاریخ فعلی را ( UTC ) قبل از فراخوانی API در موبایل میگیریم و Ticks آن را در یک مدل ذخیره میکنیم. سپس مدل که شامل تایم فعلی میباشد را سریالایز میکنیم و به string تبدیل میکنیم سپس رشته بدست آمده را با استفاده از الگوریتم AES رمزنگاری میکنیم با استفاده از Key ثابت و ClientSecret. سپس این مقدار بدست آمده را در هدر Request-Key قرار میدهیم. توجه داشته باشید باید ClientSecret هم در یک هدر دیگر به سمت سرور ارسال شود زیرا با استفاده از این ClientSecret عملیات رمزگشایی مهیا میشود. به عنوان مثال ساعت فعلی به صورت Ticks به این صورت میباشد "637345971787256752 ". این عدد از نوع long است و با گذر زمان مقدار آن بیشتر میشود. در نهایت مدل نهایی سریالایز شده به این صورت است :
{"DateTimeUtcTicks":637345812872371593}
g/ibfD2M3uE1RhEGxt8/jKcmpW2zhU1kKjVRC7CyrHiCHkdaAmLOwziBATFnHyJ3
var dateTimeNow = DateTime.UtcNow; var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks; var expireTimeTo = dateTimeNow.Ticks; string clientSecret = httpContext.Request.Headers["ClientSecret"].ToString(); string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret); var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader); if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
در ادامه اگر رشتهی رمزنگاری شده در کش موجود باشد، مجددا پیغام "Forbidden: You don't have permission to call this api" را به کاربر نمایش میدهیم؛ زیرا به این معناست که رشتهی رمزنگاری شده قبلا ارسال شده است. سپس رشته رمزنگاری شده را رمزگشایی میکنیم و به مدل ApiLimiterDto دیسریالایز میکنیم و بررسی میکنیم که مقدار Ticks ارسال شده از طرف موبایل، از یک دقیقه قبل بیشتر بوده و از زمان حال کمتر باشد. اگر در بین این دو بازه باشد، یعنی درخواست معتبر هست و اجازه فراخوانی API را دارد؛ در غیر این صورت پیغام 403 را به کاربر نمایش میدهیم.
مدل ApiLimiterDto :
public class ApiLimiterDto { public long DateTimeUtcTicks { get; set; } }
public class ApiLimiterMiddleware { private readonly RequestDelegate _next; private readonly IDistributedCache _cache; public ApiLimiterMiddleware(RequestDelegate next, IDistributedCache cache) { _next = next; _cache = cache; } private const string requestKey = "Request-Key"; private const string clientSecretHeader = "ClientSecret"; public async Task InvokeAsync(HttpContext httpContext) { if (!httpContext.Request.Headers.ContainsKey(requestKey) || !httpContext.Request.Headers.ContainsKey(clientSecretHeader)) { await WriteToReponseAsync(); return; } var requestKeyHeader = httpContext.Request.Headers[requestKey].ToString(); string clientSecret = httpContext.Request.Headers[clientSecretHeader].ToString(); if (string.IsNullOrEmpty(requestKeyHeader) || string.IsNullOrEmpty(clientSecret)) { await WriteToReponseAsync(); return; } //اگر کلید در کش موجود بود یعنی کاربر از کلید تکراری استفاده کرده است if (_cache.GetString(requestKeyHeader) != null) { await WriteToReponseAsync(); return; } var dateTimeNow = DateTime.UtcNow; var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks; var expireTimeTo = dateTimeNow.Ticks; string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret); var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader); if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo) { //ذخیره کلید درخواست در کش برای جلوگیری از استفاده مجدد از کلید await _cache.SetAsync(requestKeyHeader, Encoding.UTF8.GetBytes("KeyExist"), new DistributedCacheEntryOptions { AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2) }); await _next(httpContext); } else { await WriteToReponseAsync(); return; } async Task WriteToReponseAsync() { httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; await httpContext.Response.WriteAsync("Forbidden: You don't have permission to call this api"); } } }
برای رمزنگاری و رمزگشایی، یک کلاس را به نام AesProvider ایجاد کردهایم که عملیات رمزنگاری و رمزگشایی را فراهم میکند.
public static class AesProvider { private static byte[] GetIV() { //این کد ثابتی است که باید در سمت سرور و موبایل موجود باشد return encoding.GetBytes("ThisIsASecretKey"); } public static string Encrypt(string plainText, string key) { try { var aes = GetRijndael(key); ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV); byte[] buffer = encoding.GetBytes(plainText); string encryptedText = Convert.ToBase64String(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length)); return encryptedText; } catch (Exception) { throw new Exception("an error occurred when encrypting"); } } private static RijndaelManaged GetRijndael(string key) { return new RijndaelManaged { KeySize = 128, BlockSize = 128, Padding = PaddingMode.PKCS7, Mode = CipherMode.CBC, Key = encoding.GetBytes(key), IV = GetIV() }; } private static readonly Encoding encoding = Encoding.UTF8; public static string Decrypt(string plainText, string key) { try { var aes = GetRijndael(key); ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV); byte[] buffer = Convert.FromBase64String(plainText); return encoding.GetString(AESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length)); } catch (Exception) { throw new Exception("an error occurred when decrypting"); } } }
متد Decrypt و Encrypt یک ورودی به نام key دریافت میکنند که از هدر ClientSecret دریافت میشود. در سمت سرور عملا عمل Decrypt انجام میشود و Encrypt برای این مثال در سمت سرور کاربردی ندارد.
برای رمزنگاری با استفاده از روش AES، چون از 128 بیت استفاده کردهایم، باید طول متغییر key برابر 16 کاراکتر باشد و IV هم باید کمتر یا برابر 16 کاراکتر باشد.
در نهایت برای استفاده از این میان افزار میتوانیم از MiddlewareFilter استفاده کنیم که برای برخی از apiهای مورد نظر از آن استفاده کنیم.
کلاس ApiLimiterPipeline :
public class ApiLimiterPipeline { public void Configure(IApplicationBuilder app) { app.UseMiddleware<ApiLimiterMiddleware>(); } }
نحوه استفاده از میان افزار برای یک اکشن خاص :
[Route("api/[controller]/[action]")] [ApiController] public class ValuesController : ControllerBase { [MiddlewareFilter(typeof(ApiLimiterPipeline))] public async Task<IActionResult> Get() { return Ok("Hi"); } }
نظرات اشتراکها
لاگ کردن شروع، خاتمه و به خطا برخورد کردن فراخوانی یک متد
دوره کامل Aspect oriented programming در همین سایت
اشتراکها
یادداشتهای حرفهای و بینظیر
اشتراکها
برنامه نویسی هم روند در .net core
اشتراکها