همانطور که توسعه نرم افزار به معماری های Service-Oriented تکامل می یابد، چارچوب ها و روش های اساسی مورد استفاده نیز باید تغییر کنند. در این مقاله نحوه ایجاد سرویسی برای مدیریت احراز هویت در بین کامپوننت ها و سرورها را نشان می دهد.
اخیراً، نرمافزار بیشتر به سمت سیستمهای Component-Based، مانند SOA ( معماری سرویسگرا - Service-Oriented Architecture ) و بهویژه میکروسرویسها متمایل شده است. در حالی که این رویکرد برای حل مسائل مقیاس پذیری و قابلیت استفاده مجدد در اکثر سیستم ها استفاده می شود، چند چالش جدید را نیز برای توسعه دهندگان ایجاد می کند.
یکی از این چالش ها، تأیید هویت کاربر در multiple components است. در معماری SOA یا میکروسرویس، هر یک از Component ها ممکن است در یک سرور جداگانه مستقر شوند، بنابراین رویکرد یکپارچه ایجاد یک جلسه ( Session ) برای هر کاربر دیگر کار نخواهد کرد. حتی اگر برنامههایی که کاربر با آنها تعامل دارد از یک Session برای اعتبارسنجی هویت کاربران استفاده کنند، این اعتبار در سایر Component ها معنایی ندارد.
برای حل این مشکل باید یک فیلتر بین اپلیکیشن های فرانت اند و سرویس ها قرار داد. این فیلتر که API Gateway نامیده می شود به منظور هدایت هر درخواست به Component ای که مسئول رسیدگی به آن است، آن هم تنها در صورتی که کاربر به انجام آن عمل خاص دسترسی داشته باشد، عمل می کند. و برای سبک نگه داشتن API Gateway، باید از یک Component خاص برای اعتبارسنجی هویت کاربران استفاده شود.
یک جریان کاری معمولی برای چنین سیستمی به این صورت خواهد بود:
1. کاربر از یک برنامه وب یا تلفن وارد می شود. اعتبارنامه ها ( Credentials ) از طریق API Gateway به Component مسئول ارسال می شوند.
2. اگر اعتبارنامه ها درست باشد، یک توکن صادر می شود و به کاربر بازگردانده می شود.
3. هر درخواست بعدی حاوی این توکن است که توسط API Gateway از طریق همان Component ای که آن را صادر کرده است تأیید می شود.
در این مقاله بر ایجاد سرویسی که وظیفه صدور و تایید هویت کاربران را بر عهده دارد تمرکز خواهم کرد. من این را با ASP.NET Core WebAPI توسعه خواهم داد، اما این روش مستقل از زبان و ابزار بوده و میتوان آن را با تکنولوژی های دیگری نظیر Node.JS، Java یا Python با توجه به فضای و ابزار هایشان نیز پیاده کرد.
توکنهایی که من استفاده خواهم کرد، از نوع JSON Web Tokens هستند (JWT، که «وسیلهای فشرده و ایمن برای نشان دادن ادعاها ( Claims ) برای انتقال بین دو طرف است.») اساساً، یک JWT یک شی JSON کدگذاری شده است با یک کلید مخفی ( Secret Key ) یا یک جفت کلید عمومی/خصوصی ( Public/Private Key ) امضا میشود.
یک JWT از سه بخش مختلف تشکیل شده است: هدر ( Header )، محموله ( Payload ) و امضا ( Signature ).
قسمت Header معمولاً از دو بخش تشکیل شده است: نوع توکن (JWT) و الگوریتم هش مورد استفاده (مانند HMAC SHA256).
قسمت Payload حاوی "Claim"های توکن است که بیانگر اظهاراتی در مورد یک موجودیت (به عنوان مثال کاربر) است. سه نوع Claim وجود دارد، ثبت شده ( Registered )، عمومی ( Public ) و خصوصی ( Private ). مهمترین آنها Claim ها خصوصی هستند که برای به اشتراک گذاشتن اطلاعات بین طرفینی که در مورد استفاده از JWT توافق کرده اند استفاده می شود. اینها میتوانند شامل نام کاربر یا نقشها ( Roles ) مانند مدیر یا ناشر باشند.
پس از اینکه دو قسمت اول با استفاده از Base64Url کدگذاری شدند، امضا باید ایجاد شود. این شامل Header و Payload است که با استفاده از الگوریتم مشخص شده در هدر هش می شوند. هدف از امضا تأیید هویت فرستنده و اطمینان از عدم تغییر پیام است.
در تصویر زیر، قسمت های گفته شده برای فهم آسان تر به ترتیب با سه رنگ مختلف جداسازی شده اند.
همانطور که گفتم پروژه در این مقاله با استفاده از ASP.NET Core WebAPI توسعه خواهد یافت. برای شروع، ویژوال استودیو را باز کنید و یک پروژه جدید ایجاد کنید. پروژه ای از نوع ASP.NET Core Web API را مطابق تصویر انتخاب کنید. از صفحه بعد من Net Core 5. را انتخاب کردم.
اولین کاری که باید انجام دهید این است که یک کلاس User بسیار ساده ایجاد کنید که از Username و Password تشکیل شده است. من این کلاس را در پوشه جدید به نام Domain ساختم. از آنجایی که این آموزش فقط بر جنبه JWT تمرکز می کند، برای سادگی کار از پیاده سازی دسترسی به پایگاه داده یا هش رمز عبور صرف نظر میکنیم، بنابراین فقط آن را به صورت plain text ذخیره می کنم – بدیهی است که در پروژه های واقعی هرگز چنین کاری انجام نمی شود.
public class User { public string Username { get; set; } public string Password { get; set; } }
یکی دیگر از property هایی که می توانید به کاربر اضافه کنید یک نقش ( Role ) است. ساده ترین راه برای انجام این کار ایجاد یک enum به نام UserRole است، مانند این:
public enum UserRole { NORMAL, ADMIN }
فقط برای اینکه بتوانم با آن کار کنم، یک کلاس UserRepository در یک پوشه جدید به نام Repositories ایجاد می کنم که دسترسی به پایگاه داده را شبیه سازی می کند. این فقط شامل یک لیست است که من در Constructor پر می کنم، و Method ی که می تواند داده ها را از آن لیست بر اساس نام کاربری بازیابی کند.
public class UserRepository { public List<User> TestUsers; public UserRepository() { TestUsers = new List<User>(); TestUsers.Add(new User() { Username = "Test1", Password = "Pass1" }); TestUsers.Add(new User() { Username = "Test2", Password = "Pass2" }); } public User GetUser(string username) { try { return TestUsers.First(user => user.Username.Equals(username)); } catch { return null; } } }
اطمینان حاصل کنید که using مربوط به کلاس user را به UserRepository اضافه کرده باشید.
برای شروع کار توکن واقعی، یک کلاس جدید در پوشه Services ایجاد می کنم که TokenService نام دارد. این کار هم از ایجاد و هم اعتبار سنجی توکن ها مراقبت می کند. برای استفاده از قابلیت JWT، باید بسته ای را نصب کنید که دسترسی به JWT را ارائه دهد. فقط روی پروژه در Solution Explorer کلیک راست کرده و Manage NuGet Packages را انتخاب کنید. مطمئن شوید که Browse انتخاب شده است. سپس JWT را در نوار جستجو جستجو کنید و بسته System.IdentityModel.Tokens.Jwt را نصب کنید. من نسخه ی 6.15.0 این پکیج را نصب کردم.
اولین چیزی که در کلاس TokenService جدید ایجاد می شود، فیلدی به نام Secret است که قرار است به عنوان کلید مخفی توکن ها عمل کند. می توانید از یک Online Generator برای ایجاد یک Secret استفاده کنید یا می توانید با اجرای کد زیر در یک پروژه جداگانه و کپی کردن نتیجه ی کد، آن را در سی شارپ ایجاد کنید.
HMACSHA256 hmac = new HMACSHA256(); string key = Convert.ToBase64String(hmac.Key);
یا می توانید موردی را که من برای این کار استفاده می کنم کپی کنید:
private static string Secret = "XCAP05H6LoKvbRRa/QkqLNMI7cOHguaRyHzyg7n5qEkGjQmtBhz4SzYh4Fqwjyi3KJHlSXKPwVu2+bXr6CtpgQ=="
در این کلاس، به using های زیر نیاز خواهید داشت:
using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims;
اولین متدی که بخشی از کلاس TokenService خواهد بود متد GenerateToken است. نسخه من یک نام کاربری را به عنوان پارامتر انتخاب می کند، زیرا توکن ها قرار است هویت کاربران را تضمین کنند، اما شما می توانید هر پارامتر دیگری را که در مورد شما مناسب است را انتخاب کنید.
public static string GenerateToken(string username) { byte[] key = Convert.FromBase64String(Secret); SymmetricSecurityKey securityKey = new SymmetricSecurityKey(key); SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) }), Expires = DateTime.UtcNow.AddMinutes(30), SigningCredentials = new SigningCredentials( securityKey, SecurityAlgorithms.HmacSha256Signature) }; JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); JwtSecurityToken token = handler.CreateJwtSecurityToken(descriptor); return handler.WriteToken(token); }
اولین کاری که این روش انجام می دهد ایجاد یک شی SymmetricSecurityKey با استفاده از Secret است
که قبلا با HMACSHA256 آن را ایجاد کرده بودیم. پس از آن، شروع به ایجاد SecurityTokenDescriptor می کنیم. این شی نشان دهنده محتوای اصلی JWT است، مانند claim ها، تاریخ انقضا و اطلاعات امضا. سپس، توکن ایجاد می شود و یک نسخه رشته ای از آن برگردانده می شود.
در مثال، من فقط یک claim نام کاربری اضافه می کنم، اما لیست انواع claim هایی که می توان اضافه کرد بسیار زیاد است. همچنین میتوانید پس از ایجاد توکن، با دسترسی به ویژگی Payload، دادههای سفارشی را به آن اضافه کنید، مانند این:
token.Payload["favouriteFood"] = "cheese"
برای جمعبندی این بخش از عملکرد، Controller و Action ی را ایجاد میکنیم که توکنها را تولید میکند. روی پوشه Controllers در Solution Explorer کلیک راست کرده و Add -> Controller را انتخاب کنید. ما به هیچ الگوی برای آن نیاز نداریم، بنابراین فقط کنترلر API Controller - Empty را انتخاب کنید. من اسمش را LoginController گذاشتم.
این Controller شامل دو Action است: یکی برای فرآیند ورود و دیگری برای فرآیند اعتبار سنجی. موارد زیر using های مورد استفاده در این کلاس هستند:
using LoginService.Models; using LoginService.Repositories; using System.Net; using System.Net.Http; using System.Web.Http;
در Action اول یک نام کاربری و یک رمز عبور گرفته شده، بررسی می کند که آیا آنها معتبر هستند و سپس یک توکن بر اساس نام کاربری ایجاد می کند.
[HttpPost] public HttpResponseMessage Login(User user) { User u = new UserRepository().GetUser(user.Username); if (u == null) return Request.CreateResponse (HttpStatusCode.NotFound,"The user was not found."); bool credentials = u.Password.Equals(user.Password); if (!credentials) return Request.CreateResponse( HttpStatusCode.Forbidden, "The username/password combination was wrong."); return Request.CreateResponse( HttpStatusCode.OK, TokenService.GenerateToken(user.Username)); }
من این Action را به عنوان POST قرار دادم، بنابراین میتوانم دادههای شی را از طریق بدنه درخواست ( Request’s Body ) به جای پارامترهای URL ارسال کنم. از آنجایی که ما با رمزهای عبور سروکار داریم، بهتر است چنین اطلاعاتی را پنهان کنیم زیرا برای مثال پارامترهای URL در تاریخچه مرورگر ذخیره می شوند. من یک شی User را به عنوان پارامتر در نظر میگیرم، زیرا فقط نام کاربری و رمز عبور را در آن کلاس نگه میدارم. اگر شیء شما پیچیدهتر است، پیشنهاد من این است که یک کلاس جداگانه ایجاد کنید که فقط حاوی دادههایی باشد که میخواهید ارسال شوند.
در این Action، اولین کاری که من انجام می دهم بررسی وجود کاربر است. اگر تهی باشد، یک پاسخ NotFound (404) را برمی گردم. اگر کاربر واقعاً پیدا شد، در حال بررسی صحیح بودن رمز عبور هستم و اگر درست نیست، پیام مربوطه را برگردانم. در نهایت، اگر همه چیز خوب باشد، یک پیام OK را برمی گردانم که حاوی یک نشانه جدید بر اساس نام کاربری ارائه شده است.
اکنون که نیمی از عملکرد به پایان رسیده است، ایده خوبی است که آن را در این مرحله آزمایش کنید. من قصد دارم از Postman برای این منظور استفاده کنم، اما جایگزین های زیادی وجود دارد که می توانید از آنها استفاده کنید، مانند Fiddler.
پروژه را اجرا کنید، اما اگر پیغام خطای 403 ظاهر شد نگران نباشید. از آنجایی که به پوشه ریشه دسترسی دارید و هیچ منبعی در آن وجود ندارد، این طبیعی است. اما پروژه شما باید خوب اجرا شود. پس از اجرای آن، URL را از مرورگر در نرم افزاری که برای آزمایش استفاده می کنید کپی کنید. برای من به این شکل است :
http://localhost:61225/
برای دسترسی به اکشنی که ایجاد شده است، /api/login/ را به URL اضافه کنید تا شبیه به نمونه ی زیر شود :
http://localhost:61225/api/login/
خب حالا درخواست را به عنوان POST علامت گذاری کردم و محتوای درخواست را به عنوان داده JSON تنظیم کردم. در Postman این کار با انتخاب گزینه Raw در تب Body و سپس انتخاب گزینه JSON از منوی کشویی انجام می شود:
سپس یک شی JSON متشکل از نام کاربری و رمز عبور در متن درخواست ایجاد کنید. فیلدهای شی JSON باید مانند Property های کلاس سی شارپ نامگذاری شوند تا بتوان آن را به درستی نگاشت ( Mapped ) کرد. این چیزی است که شی من به نظر می رسد:
اکنون درخواست را ارسال کنید و بدنه پاسخ ( Response Body ) را بررسی کنید. باید شامل یک رشته باشد که از یک توکن جدید تشکیل شده است. این توکنی است که من دریافت کردم:
با کار تولید توکن، زمان شروع عملکرد اعتبار سنجی فرا رسیده است. به کلاس TokenService برمی گردیم و متدی به نام GetPrincipal ایجاد می کنیم، این متد میخواهد توکن را بخواند، اعتبارسنجی کند و یک شی ClaimsPrincipal ایجاد کند که هویت کاربر را نگه میدارد.
public static ClaimsPrincipal GetPrincipal(string token) { try { JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token); if (jwtToken == null) return null; byte[] key = Convert.FromBase64String(Secret); TokenValidationParameters parameters = new TokenValidationParameters() { RequireExpirationTime = true, ValidateIssuer = false, ValidateAudience = false, IssuerSigningKey = new SymmetricSecurityKey(key) }; SecurityToken securityToken; ClaimsPrincipal principal = tokenHandler.ValidateToken( token, parameters, out securityToken); return principal; } catch (Exception e) { return null; } }
این method توکن را در قالب رشته می خواند و در صورت امکان آن را به JwtSecurityToken تبدیل می کند. پس از آن، لیستی از پارامترها ایجاد می شود که در طول فرآیند اعتبار سنجی استفاده می گردد و شامل ایجاد مجدد کلید، با استفاده از همان رمزی است که در طول تولید توکن وجود داشت. سپس شی ClaimsPrincipal ایجاد و برگردانده می شود. بلوک try-catch مواردی را که در آنها قالب توکن اشتباه است و نمی توان آن را تأیید کرد، رسیدگی می کند.
اکنون method دیگری برای استخراج داده ها از شی Principal ایجاد کنید. این method جایی است که میخواهید بسته به اینکه چه دادههایی در توکنها ارسال میکنید، چیزهایی را اضافه یا تغییر دهید.
public static string ValidateToken(string token) { string username = null; ClaimsPrincipal principal = GetPrincipal(token); if (principal == null) return null; ClaimsIdentity identity = null; try { identity = (ClaimsIdentity)principal.Identity; } catch (NullReferenceException) { return null; } Claim usernameClaim = identity.FindFirst(ClaimTypes.Name); username = usernameClaim.Value; return username; }
این متد شی Principal را با استفاده از توکن ایجاد می کند و سپس شی Identity را از آن استخراج می کند. این شی شامل تمام Claim های توکن، بر اساس Claim Type است.
از آنجایی که توکن فقط شامل یک نام کاربری است، من روشی را برای برگرداندن آن نام کاربری طراحی کردم و هرگونه بررسی بیشتر در کنترلر انجام خواهد شد. برای مثال، اگر فیلدهای بیشتری در توکن دارید که نیاز به تأیید بیشتر از پایگاه داده خود دارند، می توانید این کار را در این method انجام دهید و آن را با یک Boolean برگردانید.
خب، Action اعتبارسنجی توکن در همان login controller، از نوع GET است که نام کاربری و رمز را به عنوان ورودی می گیرد. می توانید کد آن را در پائین مشاهده کنید:
[HttpGet] public HttpResponseMessage Validate(string token, string username) { bool exists = new UserRepository().GetUser(username) != null; if (!exists) return Request.CreateResponse( HttpStatusCode.NotFound, "The user was not found."); string tokenUsername = TokenService.ValidateToken(token); if (username.Equals(tokenUsername)) return Request.CreateResponse(HttpStatusCode.OK); return Request.CreateResponse(HttpStatusCode.BadRequest); }
در مرحله اول، وجود نام کاربری در Repository را بررسی می کند، زیرا هیچ دلیلی برای تأیید اعتبار یک توکن برای یک کاربر غیر موجود وجود ندارد. سپس توکن را با استفاده از method ایجاد شده قبلی تأیید می کند و یک پاسخ HTTP مناسب را برمی گرداند.
آخرین کار آزمایش این بخش از عملکرد کد است. یک بار دیگر دیباگ کردن را شروع کنید.
از آنجایی که این یک درخواست GET است، من رمز و نام کاربری را به عنوان پارامترهای URL پر می کنم. روی دکمه Params در سمت راست URL کلیک کنید تا آنها را پر کنید.
مثل اینکه مشکلی نیست و من پاسخ 200 را دریافت کردم.
زمانی که نام کاربری یا توکن اشتباه باشد، پاسخ 400 دریافت می شود.
در این مقاله، من فقط موردی را در نظر گرفتم که از یک سرویس خاص برای اهداف احراز هویت استفاده شود. با این حال، میتوان از همین فناوری در سناریویی استفاده کرد که در آن توکن باید برای تأیید هویت کاربر برای دسترسی به همان Component استفاده شود. در آن صورت، راهحل سادهتر ایجاد یک filter احراز هویت و استفاده از آن برای action هایی است که نیاز به احراز هویت دارند. این سناریویی است که در آن Role Claim مفید خواهد بود، زیرا درخواستها میتوانند توسط سطوح مختلف مجوز فیلتر شوند. آموزش نحوه ایجاد فیلترهای احراز هویت در Web API را می توانید در اینجا بیابید.
با تشکر از شما دوست عزیز بابت مطالعه ی این مقاله.
در این ریپازیتوری بنده در گیتهاب هم نمونه ای از پیاده سازی و استفاده از JWT در میکروسرویس همراه با استفاده از Api Gatway رو میتونین مشاهده بفرمائید.
ترجمه ای هرچند ناقص اما کار راه انداز با اندکی دخل و تصرف از :