.Net Core’da Merkezi Login Sistemi İle Dağıtık Sistemlere Giriş Yapma
Selamlar,
Bu makalede, farklı DBlere, Redis’e ve benzeri araçlara sahip sistemlere, tek bir yerden Login olma konusunu Backend tarafından inceleyeceğiz. Kısaca amaç, farklı uygulamara tek bir Login Sayfasından bağlanmak.
Öncelikle gelin yeni Ana Login sayfasının, DB ve Servislerini yazalım.
İlk önce DB’den başlıyalım. Aşağıda görüldüğü gibi Companies tablosunun başlıca fieldları, Şirket Adı, Şirket Kodu, CompanyUrl ve ApiUrl’dir. Login olacak kişi, CompanyCode’u elle girmekte, ilgili şirketin UserName ve Password’ünü girerek uzaktan login olmaktadır.
Gerçek Senaryo:
Gerçek senaryoda amaç, birden fazla alt şirketi olan büyük bir şirketin yöneticisinin, istediği şirkete tek bir Ana Login sayfasından girmesidir. İstenildiğinde, ilgili şirketlerin kendine özel Login sayfası da bulunmaktadır.
CopmanyCode, ApiUrl : Gönderilen CompanyCode’a göre, ApiUrl bulunur. Girilen UserName ve Password, bulunan ApiUrl’e Post edilerek Login işlemi yapılır.
CompanyUrl: Eğer Login işlemi başarılı ise, geriye Result olarak dönen UniqueID Key alanı, Fornt-End uygulama (Angular) üzerinde, CompanyUrl adresine => UrlParamtere olarak Redirect edilir.
Companies (Table):
MainLogin adında .Net Core bir WebApi projesi yaratılır.
“Başkalarını bilen kimse bilgili, kendini bilen kimse bilgedir. Başkalarını yönetmek kuvvettir; kendini yönetmek iradedir.” ―Lao Tzu
LoginModel: Öncelikle Request çekilecek, client’ın credentiallarını post edeceği LoginModel, aşağıdaki gibi tanımlanır. Bu model ile request çekilecek servis yolu, client tarafından el ile girilen CompanyCode ile bulunur.
1 2 3 4 5 6 |
public class LoginModel { public string UserName { get; set; } public string Password { get; set; } public string CompanyCode { get; set; } } |
LoginReturnModel: Girilen CompanyCode ile bulunan, ilgili firmanın servisine Login olmak amacı ile post edilen data model aşağıdaki LoginReturnModeldir.
1 2 3 4 5 6 7 8 |
public class LoginReturnModel { public string UserName { get; set; } public string Password { get; set; } public bool IsMobile { get; set; } = false; public string UnqDeviceId { get; set; } } |
MainLoginResultModel: İstenen firma için, Login işlemi gerçekleştikten sonra geriye dönülen data modelidir. Burada en önemli field, “Key” propertysidir.
1 2 3 4 5 6 7 8 |
public class MainLoginResultModel { public int UserID { get; set; } public string Key { get; set; } public string Url { get; set; } public DateTime CreatedTokenTime { get; set; } public string ExceptionMessage { get; set; } } |
Post: Ana sisteme, esas login olunan servisdir.
- “string encryptedPassword = _encryption.EncryptText(model.Password)“: ClientSide’dan gönderilen Password şifrelenir. Amaç güveliğin arttırılmasıdır. Eğer service Trace edilir ise, password’ün okunur bir şekilde görülmesi engellenir. Encryption sınıfının kodlarına, şu makaleden erişebilirsiniz. => https://www.borakasmer.com/net-core-uzerinde-config-redis-ve-elasticsearch-icin-guvenlik/
- “var company = _context.Companies.AsNoTracking().Where(a => a.CompanyCode == model.CompanyCode).FirstOrDefault()“: Girilen CompanyCode’a göre, erişilecek servis yolu, DB’den çekilir. Her şirket için farklı bir servis yolu bulunmaktadır.
- “LoginReturnModel returnModel = new() { UserName = model.UserName, Password = encryptedPassword }“: Bağlanılacak sisteme ait Credential bilgileri, LoginReturnModel’e doldurularak post edilir.
- “var resp = await client.PostAsync($”http://{company.ApiUrl}/api/Login/MainLogin”, requestContent)” : Makalenin devamında anlatılacak olan “MainLogin” servise post atılıp, remote login işlemi gerçekleştirilir.
- “var mainLoginResultModel = JsonSerializer.Deserialize<MainLoginResultModel>(content)”: İlgili servisden geriye, “MainLoginResultModel” tipinde bir sonuç dönülür. Bu modelde client için önemli olan iki alan vardır.Biri “Key“, diğeri de “CompanyUrl“‘dir.
- “mainLoginResultModel.Url = company.CompanyUrl“: Client-Side’da login işlemi başarı ile gerçekleşir ise, Redirect olunacak url bu propertyde tanımlanır. Bu sefer redirect olunacak Url, Remote’da Client-Side (Angular) tarafındaki, Login sayfasının yoludur.
Post():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
[HttpPost()] public async Task<ServiceResponse<MainLoginResultModel>> Post([FromBody] LoginModel model) { ServiceResponse<MainLoginResultModel> response = new(); try { string encryptedPassword = _encryption.EncryptText(model.Password); if (model.Password == "") { response.IsSuccessful = false; response.ExceptionMessage = "Geçersiz şifre!"; return response; } var company = _context.Companies.AsNoTracking().Where(a => a.CompanyCode == model.CompanyCode).FirstOrDefault(); if (company == null) { response.IsSuccessful = false; response.ExceptionMessage = "Şirket bulunamadı!"; return response; } HttpClient client = new(); LoginReturnModel returnModel = new() { UserName = model.UserName, Password = encryptedPassword }; var returnLoginModel = JsonSerializer.Serialize(returnModel); var requestContent = new StringContent(returnLoginModel, Encoding.UTF8, "application/json"); var resp = await client.PostAsync($"http://{company.ApiUrl}/api/Login/MainLogin", requestContent); resp.EnsureSuccessStatusCode(); var content = await resp.Content.ReadAsStringAsync(); var mainLoginResultModel = JsonSerializer.Deserialize<MainLoginResultModel>(content); mainLoginResultModel.Url = company.CompanyUrl; response.Entity = mainLoginResultModel; response.IsSuccessful = true; return response; } catch (Exception ex) { response.IsSuccessful = false; response.ExceptionMessage = ex.Message; return response; } } |
Genel Özet:
Companies tablosuna aşağıdaki gibi bir sorgu çekilir ise CompanyCode, Ana Giriş sayfasında elle girilen koddur. Bu koda bağlı olarak, Server-Side’da “ApiUrl” ile gidilecek Remote şirketin WebApi servisinin yolu belirlenir. İlgili servise, UserName ve Password gönderilerek uzaktan login olunur. Eğer Login işlemi başarılı olunmuş ise, dönen Response modeldeki “KEY” alanı, Url parameter olarak remote şirketteki “CompanyUrl“‘de bulunan Client-Side(Angular) application’a Redirect edilir. Ve otomatik login işlemi gerçekleşmiş olur.
Buraya kadar, seçilen bir şirkete uzaktan Login olunuldu ve bu servisden geriye bir Key döndü (1). Daha sonra bu key alınarak, url parameter şeklinde gene remotedaki Client-Side Login sayfasına redirect edildi (2). Bundan sonra, ilgili şirketin Login sayfasına, alınan bu Key ile gelinince, otomatik login işlemi yapılıp Ana sayfaya yönlendirilecek ve ilgili key, Redis’den silinecekdir. (3)
Şimdi gelin isterseniz, Remote WebApi server’da neler oluyor ona bakalım.
“Geçici güvenlik için özgürlüğünü feda edenler ne özgürlüğü hak eder ne de güvenliği.” ―Benjamin Franklin
Remote LoginController/MainLogin(): Remote Login amaçlı request çekilen servis, aşağıdaki gibidir. Aslında burası, LoginServices’den hemen önceki dışarıya açık olan bir api katmanıdır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[Route("MainLogin")] [HttpPost] [SwaggerOperation(Summary = "", Description = "BORSOFT Ana Login ekranından giriş yapıldığı zaman kullanılan servistir. Geriye Redis'te temp olarak oluşturulan key dönmektedir.")] public IActionResult MainLogin([FromBody] LoginModel model) { var mainLoginResultModel = _loginService.CheckMainLogin(model).Entity; string errorCode = mainLoginResultModel.ExceptionMessage; if (errorCode != null) { mainLoginResultModel.ExceptionMessage = "Şifre işleminde bir problem yaşandı. Lütfen teknik destek alın."; } var response = new ServiceResponse<ActionResult>(HttpContext); return new ObjectResult(mainLoginResultModel); } |
LoginResultModel: Login olununca, client’a dönülecek olan ResultModeldir.
1 2 3 4 5 6 7 8 9 10 11 |
public class LoginResultModel { public int UserId { get; set; } public string Name { get; set; } public string UserName { get; set; } public bool IsAdmin { get; set; } public string Token { get; set; } public string RefreshToken { get; set; } public long CreatedTokenTime { get; set; } public string ExceptionMessage { get; set; } } |
RedisMainLoginModel: Remote Login işleminden sonra, geçici olarak Redis’e set edilecek olan Login modeldir.
1 2 3 4 5 6 7 |
public class RedisMainLoginModel { public int UserId { get; set; } public string Token { get; set; } public string RefreshToken { get; set; } public DateTime CreatedTokenTime { get; set; } } |
CheckMainLogin(): Bu method, kullanıcının girdiği Credentialların kontrol edilip doğruluğun onaylandığı ve buna göre Tokenların üretildiği yerdir.
- “decPassword = _encryption.DecryptText(model.Password)“: Encrypted gönderilen password, decrypt edilir.
- “if (model.Password != “” && decPassword == “”)“: Encrypted olması gereken Password’ün, fake olduğu anlamına gelmektedir. Var olan şifreleme methodu ile şifrelenmediği anlaşılmıştır.
- “var user = IsValidUserAndPasswordCombination(model.UserName, decPassword)“: Girilen UserName ve Password’ün doğru olup olmadığı kontrol edilmektedir. İlgili function, bir sonraki adımda detaylıca incelenecektir.
- “var (encToken, decToken) = _encryption.GenerateToken(user.Email); redisMainLoginModel.Token = encToken“: Girilen bilgiler doğru ise, Client için email bilgisinden yeni bir token üretilir.
- “var createTime = DateTime.Now; var cacheKey = _redisCacheService.GetTokenKey(user.IdUser, isMobile, false, model.UnqDeviceId)“: Doğru Login işleminden sonra, client’ın UserID’si, Platform tipi(Mobile,web) ve Token tipine Göre(Token,RefreshToken), redis’e atanacak bir key oluşturulur.
-
“_redisCacheService.Set(cacheKey, decToken, createTime.AddMinutes(_coreContext.TokenExpireTime));// 1 saatlik Token Açık Atılır.“: Redise yaratılan key ile 1 saat Expire süreli Client’a özel oluşturulan “Token” atılır.
- “var refreshToken = _encryption.GenerateToken(user.Email); redisMainLoginModel.RefreshToken = refreshToken.encToken; _redisCacheService.Set(_redisCacheService.GetTokenKey(user.IdUser, false, true, model.UnqDeviceId), refreshToken.decToken, tokenExpireTime)“: Token için yapılan tüm işlemler RefreshToken için de yapılır.
- *”string tempLoginKey = GenerateKey.GenerateStringKey()“: Burada esas amaç, Remote login olan client’a login olduktan sadece 1 defaya mahsus bir keyin oluşturulmasıdır. Makelenin devamında methoda ait kodlar gösterilmektedir. Bu key sayesinde RemoteLogin olan client, username ve password bilgilerine ihtiyaç duymadan tek sefere mahsus login olabilecektir.
- “_redisCacheService.Set(tempLoginKey, redisMainLoginModel, DateTime.Now.AddMinutes(5))“: 5 dakika için geçerli olan, oluşturulan tempLoginKey ile RemoteLogin yapan Client’ın Credential bilgileri Redis‘e atılır.
- “mainLoginResultModel.Key = _encryption.EncryptText(tempLoginKey); mainLoginResultModel.CreatedTokenTime = createTime; mainLoginResultModel.UserID = user.IdUser“: Uzaktan login olan Client’a özel oluşturulan bu TempKey, UserID ve CreatedTime geri dönülür.
CheckMainLogin():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
public ServiceResponse<MainLoginResultModel> CheckMainLogin(LoginModel model) { string decPassword; try { //Client'dan encrypted olarak gelen Password Decrypt edilir. decPassword = _encryption.DecryptText(model.Password); if (model.Password != "" && decPassword == "") { string message = "PasswordError"; var response = new ServiceResponse<MainLoginResultModel>(null); response.Entity = new MainLoginResultModel { ExceptionMessage = message }; response.IsSuccessful = false; return response; } } catch { string message = "PasswordError"; var response = new ServiceResponse<MainLoginResultModel>(null); response.Entity = new MainLoginResultModel { ExceptionMessage = message }; response.IsSuccessful = false; return response; } var user = IsValidUserAndPasswordCombination(model.UserName, decPassword); if (user != null) { var mainLoginResultModel = new MainLoginResultModel(); var redisMainLoginModel = new RedisMainLoginModel(); //Token var (encToken, decToken) = _encryption.GenerateToken(user.Email); redisMainLoginModel.Token = encToken; var createTime = DateTime.Now; var cacheKey = _redisCacheService.GetTokenKey(user.IdUser, false, false, model.UnqDeviceId); _redisCacheService.Set(cacheKey, decToken, createTime.AddMinutes(_coreContext.TokenExpireTime));// 1 saatlik Token Açık Atılır. DateTime tokenExpireTime = createTime.AddMinutes(_coreContext.RefreshTokenExpireTime); //RefreshToken var refreshToken = _encryption.GenerateToken(user.Email); redisMainLoginModel.RefreshToken = refreshToken.encToken; _redisCacheService.Set(_redisCacheService.GetTokenKey(user.IdUser, false, true, model.UnqDeviceId), refreshToken.decToken, tokenExpireTime); string tempLoginKey = GenerateKey.GenerateStringKey(); redisMainLoginModel.CreatedTokenTime = createTime; redisMainLoginModel.UserId = user.IdUser; _redisCacheService.Set(tempLoginKey, redisMainLoginModel, DateTime.Now.AddMinutes(5)); mainLoginResultModel.Key = _encryption.EncryptText(tempLoginKey); mainLoginResultModel.CreatedTokenTime = createTime; mainLoginResultModel.UserID = user.IdUser; var response = new ServiceResponse<MainLoginResultModel>(null); response.Entity = mainLoginResultModel; response.IsSuccessful = true; return response; } else { var loginResultModel = new MainLoginResultModel(); var response = new ServiceResponse<MainLoginResultModel>(null); loginResultModel.ExceptionMessage = "Hatalı Kullanıcı Adı veya Şifre girdiniz!"; response.Entity = loginResultModel; response.IsSuccessful = false; return response; } } |
“Başlarınız, düşünceler yön değiştirebilsin diye yuvarlaktır.” ―Francis Picabia
Aşağıda Remote Login işlemi başarılı olmuş bir Client’ın, Redis üzerinde 1 kullanımlık ve 5 dakikalık bir süre için geçerli olan, “tempLoginKey” ile atılan örnek bir kaydı bulunmaktadır.
IsValidUserAndPasswordCombination: Remote Login ile uzaktan gönderilen Username ve Password’ün, kontrol edildiği methoddur.
- “var user = _userRepository.Table.FirstOrDefault(k => k.UserName.Equals(username))“: Gönderilen unique UserName’e göre, DB’den user bilgisi çekilir.
- “var encryptKey = user.PasswordHash“: Gönderilen Username’e göre bulunan User’ın Password’ü, Hash’li olarak DB’den alınır.
- “string getEncryptKey = encryptKey.Split(‘æ’)[0]“: Alınan Password içinde, key ve salt bulunmaktadır. İkisi de ayrı ayrı bulunur.
- “bool isSuccess = _encryption.ValidateHash(password, getSalt, getEncryptKey)“: Gönderilen Password aynı salt key ile Hashlenir. DB’deki kullanıcının gerçek Hashli password ile Client tarafından gönderilen sorgulanacak Password aynı key ile Hashlenerek karşılaştırılır. Aynı ise, başarılı sonucu geri dönülür.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
private DbUserViewModel IsValidUserAndPasswordCombination(string username, string password) { DbUserViewModel userData = null; if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) { var user = _userRepository.Table.FirstOrDefault(k => k.UserName.Equals(username)); if (user != null) { var encryptKey = user.PasswordHash; string getEncryptKey = encryptKey.Split('æ')[0]; string getSalt = encryptKey.Split('æ')[1]; bool isSuccess = _encryption.ValidateHash(password, getSalt, getEncryptKey); if (isSuccess) { userData = _userService.GetById(user.IdUser).Entity; } } return userData; } return null; } |
GenerateStringKey(): Remote Login olan Client’a, tek seferlik verilen keyin oluşturulduğu methoddur.
Neden Tokenlar geri dönülmüyor da, bu oluşturulan Unique key geri dönülüyor ?
Nedeni tamamen güvenlik. Remote bir sistemden Login olunduktan sonra, geri dönülen key ile ilgili firmanin Login sayfasına gidilecek ve bu tek kullanımlık key ile login sayfası geçilerek Ana sayfaya gelinecek. Bu arada sistemler arası iletilen tek şey, Encrypt edilmiş olan bir key olacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public static string GenerateStringKey() { int length = 8; // creating a StringBuilder object() StringBuilder str_build = new StringBuilder(); Random random = new Random(); char letter; for (int i = 0; i < length; i++) { double flt = random.NextDouble(); int shift = Convert.ToInt32(Math.Floor(25 * flt)); letter = Convert.ToChar(shift + 65); str_build.Append(letter); } return str_build.ToString(); } |
Aşağıdaki Angular client-side kodlarda da görüldüğü gibi, dönen encrypted unique-key, remote daki Client-Side taraftaki Login sayfasına, URL parameter olarak gönderilmektedir.
Aşağıda görüldüğü gibi, remote ilgili şirket üzerinde çalışan Client-Side Angular Login Component Constructor’ında, eğer Url içerisinde “key” parametresi var ise, authService üzerindeki loginGate() methodu çağrılmaktadır.
Son olarak aşağıda görüldüğü gibi “loginGate()” servisinde, server-side tarafdaki LoginGate methoduna, ilgili “key” ile post işlemi yapılmaktadır. Böylece Remote olarak sadece bir key ile Username ve Password gönderilmeden uzaktan login olunması sağlanmıştır. İlk sefer gönderilen UserName ve Password ile tek seferlik unique bir key oluşturulmuş ve bu key, Encoded bir şekilde network ağında kullanılmıştır. İlgili key, Ana-Login sayfasına Url Parameter olarak gönderilmiş ve makalenin devamında anlatılacak LoginGate() servisi ile sanki username password girilmiş gibi ilgili Token ve RefreshToken client’a gönderilmiştir. Böylece tek bir ana noktadan Login olunarak, var olan sistemin Login sayfası atlanmıştır.
LoginGate(): Aşağıda görüldüğü gibi, bu method normal Login() methoduna karşın Remote bir sistemden Login işlemi gerçekleştirildikten sonra çağrılmaktadır. İlgili CheckLoginRedisKey() servisinden doğrulama yapılmaktadır.
1 2 3 4 5 6 7 8 9 |
[Route("LoginGate")] [HttpPost] [SwaggerOperation(Summary = "", Description = "MainLogin ile giriş yapıldıktan sonra alınan Redis Key ile login olunan kullanıcı bilgilerini dönmektedir. sorgulama yapıldıktan sonra Redis'ten bu key silinmektedir.")] public IActionResult LoginGate([FromBody] string tempKey) { var loginResultModel = _loginService.CheckLoginRedisKey(tempKey).Entity; return new ObjectResult(loginResultModel); } |
CheckLoginRedisKey(): Gönderilen encoded key’e göre, credential bilgilerinin geri dönüldüğü servisdir.
- “decryptKey = _encryption.DecryptText(key)“: Encrpted gönderilen key, decrypt edilerek okunur hale getirilir.
- “RedisMainLoginModel redisMainLoginModel = _redisCacheService.Get<RedisMainLoginModel>(decryptKey)“: İlgili key ile Redis’den, “RedisMainLoginModel” çekilir.
- “if(redisMainLoginModel!= null)” : Kayıt var mı yok mu diye kontrol edilir. Her key sadece 1 kere kullanılabilir. Aynı key 2.kere kullanılmaya çalışıldığında, hata alınır.
- “var userData = _userService.GetById(redisMainLoginModel.UserId).Entity“: Redis’den çekilen UserID değerine göre, kullanıcı bilgileri Redis’den yok ise DB’den çekilir.(_userServices’de, ilgili kullanıcı bilgileri önce Redis’de var mı diye kontrol edilir, yok ise DB’den çekilerek ilgili Redis doldurulur.)
- “var loginResultModel = new LoginResultModel“: Client’a geri dönülecek, Token, RefreshToken ve UserID gibi bilgileri içeren modeldir.
- “_redisCacheService.Remove(decryptKey)”: Redis’den ilgili credential bilgileri alındıktan sonra, silinir. Kısacası, her key ile sadece bir giriş hakkı sağlanmış olunur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public ServiceResponse<LoginResultModel> CheckLoginRedisKey(string key) { ServiceResponse<LoginResultModel> response = new(null); string decryptKey = string.Empty; try { decryptKey = _encryption.DecryptText(key); } catch { response.Entity = new LoginResultModel { UserId = -2, ExceptionMessage = "RedisKey Error!" }; response.IsSuccessful = false; return response; } RedisMainLoginModel redisMainLoginModel = _redisCacheService.Get<RedisMainLoginModel>(decryptKey); if(redisMainLoginModel!= null) { var userData = _userService.GetById(redisMainLoginModel.UserId).Entity; var loginResultModel = new LoginResultModel { UserName = userData.UserName, Name = userData.Name, UserId = userData.IdUser, IsAdmin = (bool)userData.IsAdmin, Token = redisMainLoginModel.Token, RefreshToken = redisMainLoginModel.RefreshToken, CreatedTokenTime = redisMainLoginModel.CreatedTokenTime.GetTotalMilliSeconds() }; response.Entity = loginResultModel; response.IsSuccessful = true; _redisCacheService.Remove(decryptKey); } else { var loginResultModel = new LoginResultModel(); loginResultModel.ExceptionMessage = "Giriş başarısız!"; response.Entity = loginResultModel; response.IsSuccessful = false; return response; } return response; } |
Böylece tek bir yerden, farklı sistemlere Login olunmuş olunur. Amaç kendi içinde Login sistemi olan birden fazla sisteme, dışarıdan tek bir yerden login olunulmasıdır. Bu makalede anlatılmayan bir de, mikroservis işi vardır. O da yeni bir şirketin kurulması durumunda, Global tanımlanan Companies tablosuna, ilgili şirketin Backend ve Client-Side Url adresi ile birlikte yaratılması ve oluşan yeni CompanyCode’un ilgili kullanıcılara çeşitli yollar ile bildirilmesidir.(Mail, Sms gibi..)
İlk Login işleminden sonra geriye Token yerine, oluşturulan Unique keyin encrypted dönülmesi, tamamen güvenlik sebebiyledir. 2 sistem arasında Tokenlar yerine tek seferlik, Redis’de oluşturulan bu key kullanılmaktadır. Redis bu yaklaşımda, zorunlu bir araç olarak kullanılmakta ve maalesef bağımlılığı arttırmaktadır.
Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
ya sen nasil bir kralsin normalde claim mantigini mvc yapisinda hic kullanmamistim ve jwt mantigini kafamda tam oturtamamistim tesekkurler :)