Manuel Tokenization İle Authentication, WebServisleri Güvenliği ve Otomatik Yenileme Part 2

Selamlar Arkadaşlar,

Bu makalede, bir önceki makalede işlenen, tokenların Session’da tutulması durumu yerine, Redis’de tutulması durumunda, bize ne gibi avantajlar sağlanacağı ve kodda nasıl bir değişikliğe gidileceği incelenecektir. .Net Core üzerinde Redis hakkında daha detaylı bilgiye, bu makaleden erişebilirsiniz. Bir önceki makalede anlatılan kısımlar, bu makalede tekrardan anlatılmayacaktır.

Uygulamada Redis kullanımı için, ServiceStack kütüphanesi aşağıdaki gibi eklenir.

Son olarak, Redis ayarları için appsettings.json aşağıdaki gibi değiştirilir:

İlk değişiklik yapılacak yer, gelen client’ın login olup olmadığının, Session’a bakılarak kontrol edildiği LoginFilter.cs sınıfıdır.

LoginFilter.cs: Tüm session alanların yerini Redis almıştır. Şimdi yapılan değişiklikleri, adım adım inceleyelim:

  • “RedisEndpoint conf = new RedisEndpoint() { Host = “127.0.0.1”, Port = 6379 }”: Redis Config ile alakalı tüm tanımlamalar burada yapılmıştır.
  • “using (IRedisClient client = new RedisClient(conf))” : Redis client’a, using içerisinde erişilecektir.
  • “context.HttpContext.Session.TryGetValue(“UniqueUserName”, out var name)” : Redis’de key olarak, Login olunan UserName, yani “UniqueUserName” kullanılmıştır. İlgili UserName Session’da saklanmıştır. Burada da Redis’i kontrol etmek amacı ile UserName session’dan çekilmiştir.

Not -1: Önceki makalede Session süresi 1dakika olmasına karşın, bu makalede UserName session’da tutulduğu için, “Start.cs/ConfigureServices”‘de süre aşağıdaki gibi 1 saate çıkarılmıştır.

Startup.cs/ConfigureServices():

Not -2: En başta Redis’de key olarak, Session.Id kullanılmak istenmiştir. Ama Mac’de “Https” üzerinden yapılan her request’de Session.Id değişmiştir. Buna henüz bir çözüm bulamadım. Bundan dolayı Redis Key olarak UserName kullandım.

  • “string userName = name != null ? System.Text.Encoding.UTF8.GetString(name) : “”” : En başta, client daha hiç login olmadığı için Session boştur. Burada bu kontrol edilmektedir.
  • ” var result = client.Get<string>(userName)”: Redis üzerinde olması gereken token, session’dan çekilen “userName” ile kontrol edilir.
  • “if (result == null)” : Eğer Redis, ilgili userName’e karşılık olarak boş ise ya token süresi dolmuştur. Ya da client daha hiç login olmadığı için Session[“userName”] boştur.
  • public string IsRefreshToken(string sessionToken, ActionExecutingContext context): Token süresinin 40sn – 60sn arasında ise, değiştirildiği methoddur. Burada Token artık session’dan değil, Redis’den değiştirilmektedir.
    • “string tokenSession = sessionToken.Split(‘æ’)[1]” : Artık gelen token byte[] değil string bir değer olduğu için, direk “Split()” methodu kullanılabilmiştir.
    • “context.HttpContext.Session.TryGetValue(“UniqueUserName”, out var name);” : Eğer token’ın süresi 40sn’den fazla ise, Session’dan login olunan “UniqueUserName” değeri çekilir.
    • “client.Set(userName, token,DateTime.Now.AddMinutes(1))” : Yeni oluşturulan Token, Redis’e sessiondan çekilen ilgili “UniqueUserName” ile atılır. Böylece, süresi dolan Token, server-side tarafta Redis üzerinden güncellenmiş olunur.

Diyelim ki, client Login olmamış yani, Redis ilgili UserName’e karşılık boş. İşte bu durumda Login ekranına düşülür.

[Post]HomeController/Login(): Aşağıda görüldüğü gibi static olarak beklenen 2 user’dan biri ile giriş yapıldıktan sonra, yeni oluşturulan TOKEN girilen userName adı ile Session yerine Redis’e atılmaktadır.

  • “if ((name == “bora” && password == “1234”) || (name==”duru” && password==”4321″))” : Static olarak 2 user’a bakılmıştır. İstenir ise DB’den kullanıcı girişi için doğrulama yapılabilir. Ayrıca aynı UserName’in girilmemesi için kontrol edilmelidir.
  • ” HttpContext.Session.Set(“UniqueUserName”, System.Text.Encoding.UTF8.GetBytes(name))”: Girilen tekil kullanıcı adı, Session’a “UniqueUserName” key’i ile kaydedilir.
  • “using (IRedisClient client = new RedisClient(conf))” : Using içinde Redis client oluşturulur.
  • “client.Set(name, token,DateTime.Now.AddMinutes(1))”: Login olmuş client’ın token’ı, bu örnekde 1 dakikalık bir süre için girmiş olduğu UserName’i ile birlikte Session yerine Redis’de saklanmaktadır.

LoginView’da, login başarı ile gerçekleşir ise Index sayfasına yönlenilir.

Home/Gazete(): Bu örnek de Gazete() Action’ında  “List<Kategoris>” view model’i , Client-Side yerine Server-Side tarafta doldurulup ilgili view’a gönderilmektedir.

Not: Burada esas gösterilmek istenen, önceki bölümde bu işlemin yapılması durumunda, WebApi servislerinin stateless olmasından dolayı, ilgili token’ın session’dan çekilemeyip, geriye hiçbir data döndürülemiyordu. Ama ilgili token’ların Redis’de tutulması durumunda, bu işlem WebApi seviyesinde rahatlıkla yapılabilmektedir.

  • “HttpContext.Session.TryGetValue(“UniqueUserName”, out var name)”: Login olan Client’ın UniqueUserName’ı, Session’dan alınır.
  • “var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = System.Net.Http.HttpClientHandler.DangerousAcceptAnyServerCertificateValidator” : Geldik şu meşhur koda :) Bu kod sadece development zamanı kullanıp, daha sonra kaldırılması gerekmektedir. Local Mac makinasında, “Https” olarak yayımlanan bir WebApi servisine, server-side taraftan erişilmek istendiğinde SSL ve TLS sertifika uyumsuzluğu alınmıştır. Bu sorunun önüne geçmek için sertifika kontrolü iptal edilmiştir.
    • Not: Yayına çıkılırken ilgili kodun kaldırılması gerekmektedir.
  • “using (var client = new HttpClient(handler))”: HttpClient, hemen yukarıdaki tanımlama ile oluşturulur.
  • “using (IRedisClient redisClient = new RedisClient(conf))” : Session yerine, RedisClient oluşturulur.
  • “string tokenValue=redisClient.Get<string>(userName)”: İlgili token, session’dan çekilen Username ile Redis’den alınır.
  • “string tokenSession = tokenValue.Split(‘æ’)[0]” : Çekilen token’dan, zaman kısmı çıkarılır.
  • “var response = await client.GetAsync($”https://localhost:1923/api/news/{userName}/{tokenSession}”)”: İlgili WebApi servisine asenkron get işlemi, çekilen token ile yapılır.
  • “response.EnsureSuccessStatusCode()” : Request’in başarılı bir şekilde sonlanması beklenir.
  • “string content = await response.Content.ReadAsStringAsync()”: Dönen Kategori Listesi string bir content’e atanır.
  • “var data = JsonConvert.DeserializeObject<List<Kategoris>>(content)”: İlgili List<Kategoris> data’sı, NewtonSoft ile Deserialize edilip “data” değişkenine aktarılır.
  • “return View(data)” : İlgili data View’a geri dönülür.

Gazete.cshtml: Burada önceki makaleye göre en büyük fark, view’un “@model List<Kategoris>” şeklinde Server-Side tarafından doldurulan bir model beklemesidir. Böylece WebApi servisinden, hem client side hem de server side tarafından yapılan isteklere, redisdeki token ile kontrol edilerek cevap verilmektedir.

  • “<select id=”comboCategory”> <option>Kategory Seçin</option> @foreach(var item in Model) { <option value=”@item.id”> @item.Ad</option> } </select>” : İlk bölümden farklı olan html kısmı burasıdır. Gelen Mvc ViewModel’in kullanıldığı yer, seçilecek olan kategori kombosudur. İlgili model gezilerek “select itemlar” tek tek komboya doldurulur.
  • “fillNews()/ $.getJSON(“/api/news/”+categoryID+”/@ViewBag.UserName/”+token).done(function (data) {“: Seçilen kategoriye ait haberleri çekmek için, categoryID, “ViewBag” aracılı ile girilen UserName ve localStorage’dan çekilen token, parametre olarak ilgili WebApi servisine gönderilir.
  • “fillCommands()/$.getJSON(“/api/news/command/”+newsID+”/@ViewBag.UserName/”+token).done(function (data) {” : Seçilen habere ait yorumların çekilmesi için static “command/” tanımlamasının ardından HaberID(newsID)/UserName(@ViewBag.UserName) ve güvenlik amaçlı token değeri ilgili WebApi servisine gönderilir.

Şimdi sıra geldi WebApi servisindeki değişikliklere:

WebApi/NewsController :  2.Bölümde, güvenlik amaçlı gönderilen token sessiondaki bir tokendan değil de, Redis’deki bir tokendan, parametre olarak gönderilen UniqueUserName ile çekilerek kontrol edilir.

  • “public List<Kategoris> Category(string userName, string token)” : Backend taraftan çekilen Kategori Listesi için kullanılan bir servisidir. Parametre olarak login olunan UserName ve güvenlik amaçlı kontrol edilecek token beklenmektedir.
    • “using (IRedisClient client = new RedisClient(conf))”:Bu örnekde session yerine Redis bir client oluşturulur.
    • “var result = client.Get<string>(userName)” : Girilen UniqueUserName ile Redis’den kontrol edilecek token değeri çekilir.
    • “if (tokenRedis == token)” :  Redisdeki token ile parametre olarak gönderilen token aynı ise, herşey yolunda demektir.
    • “return new List<Kategoris>()”: Redis boş ise, ya da tokenlar aynı değil ise geriye boş bir Kategori Listesi dönülür.
  • “public List<Habers> CategoryNews(int id, string userName, string token)” : Seçilen kategoriye ait haberler bu method ile yine Redisdeki token ile kontrol edilip, geri dönülür.
    • “using (IRedisClient client = new RedisClient(conf)) { var result = client.Get<string>(userName)”: Redis’den girilen UserName’e göre varsa token çekilir.
    • “string refreshToken = IsRefreshToken(userName,result)”: Redis’deki token’ın süresi bitmeye az kalmış mı? Yani 40sn ile 60sn arasında mı diye bakılır.
      • “public string IsRefreshToken(string userName,string token)”: Parametre olarak yine tekil kullanıcı adı (userName) ve kontrol edilecek token parametre olarak alınır.
        • “string tokenSession = token.Split(‘æ’)[1]; DateTime sessionCreateTime = DateTime.Parse(tokenSession)” : Gelen tokendaki zaman kısmı alınır.
        • “TimeSpan remainingTime = DateTime.Now – sessionCreateTime; if (remainingTime.TotalSeconds >= 40)” : Şimdiki zamandan çıkarılıp fark 40sn’den büyük mü diye bakılır.
        • “using (IRedisClient client = new RedisClient(conf)) { string newToken = Guid.NewGuid().ToString() + “æ” + DateTime.Now”: Büyük ise yeni bir Redis Client oluşturulur. Ve [“Guid” + “Şimdiki Zaman”] birleşiminden yeni bir token oluşturulur.
        • “client.Set(userName, token,DateTime.Now.AddMinutes(1))” : İlgili token Redis’e 1 dakika süre kısıtlaması ile atılır.
    • “if (tokenRedis == token)”: Redisden çekilen token ve gönderilen token eşit ise, ilgili kategori’ye ait Haber Listesi çekilip, varsa “RefreshToken” değerleri atanıp geri dönülür.
  • “public List<Yorums> CommandNews(int id, string userName, string token)” : Parametre olarak seçilen HaberID, tekil girilen userName ve güvenlik amaçlı token beklemektedir. Amaç seçilen habere ait yorumları getirmektir.
    • “using (IRedisClient client = new RedisClient(conf)) { var result = client.Get<string>(userName)” : Yine diğerlerinde olduğu gibi burada da session yerine Redis kullanılmış ve redis client oluşturulup ilgili parametre olarak gönderilen UserName karşılık gelen token çekilmiştir.
    • “string refreshToken = IsRefreshToken(userName,result)” : Bir öncekinde olduğu gibi bunda da token süresi bitmeye yakın ise(40sn-60sn) Redis’den ilgili token, yenisi ile değiştirilir.
    • “if (tokenSession == token) {” : Gönderilen ve Redisden çekilen tokenlar, doğru ise seçilen Habere ait yorumlar çekilir ve var ise RefreshTokenları doldurulup, tüm liste geri dönülür.

Böylece WebApi servisinde Session yerine Redis üzerinde Token tutularak, güvenlik sağlanmıştır. Artık istenen her yerden, yani hem Backend hem de Frontend’den WebApi servisine ilgili token ve UserName parametre olarak gönderilerek güvenli bir sorgu çekilebilmektedir. Süresi dolmaya yakın olan Tokenlar, “IsRefreshToken()” methodu ile Backend tarafta Redisde yenilenmektedir. Client Side tarafda da LocalStrogeda ilgili Token güncellenmektedir.

Geldik bir makalenin daha sonuna. Yeni bir makalede görüşmek üzere hepinize hoşçakalın.

Source Code: http://www.borakasmer.com/projects/token_redis.zip

Herkes Görsün:

Bunlar da hoşunuza gidebilir...

4 Cevaplar

  1. Yusuf dedi ki:

    Merhaba abi. Ben niğdeden yusuf. “Manuel Tokenization İle Authentication, WebServisleri Güvenliği ve Otomatik Yenileme Part 2″ videonu çok yararlı buldum. Bir önce ki videon da çok güzeldi. Projelerini indirdim göz gezdirirken birşey fark ettim. VS community 15.7.6v kullanıyorum ve projelerini çalıştırmak istediğimde netcore uyumsuzluğunu gördüm. Bende ki sürüm 2.0.9 olduğu için önce baya bi hata aldım. Sertifika doğrulama sorununun kaynağı kullandığın PackageReference Include=”Microsoft.AspNetCore.App” Version=”2.1.0-preview1-final” sürümünden galiba. Ben varsayılan olarak 2.0.9 sürümü ile oluşturunca bir web sorunsuz çalışıyor. Bir deneme için güncelleme yaptım. Microsoft.AspNetCore.App 2.1.1 olarak oluşturduklarımda da sertifika hatası veriyor ve kendim oluşturup tarayıcıya kabul ettirince bağlanıyor. Bana göre tarayıcıların şuan 2.0.9 sürümünde sorunsuz çalıştığını düşünüyorum.

    • borsoft dedi ki:

      Selamlar Yusuf,

      .NET Core’da şu aralar versiyon sorunları çok oluyor. Sanırım yakın bir zamanda son bir 2.xxx versiyonu ile bu sorun çözülecek.
      O zamana kadar versiyonları aynı tutalım :)

      İyi çalışmalar.

  2. Celal dedi ki:

    Merhaba hocam.
    Size birkaç sorum olacak.
    1) Oluşan tokenların belirlenen Timeout süresi içinde kopyala/yapıştır ile farklı browserlarda çalışmasını engellemek için nasıl bir yöntem uygulayabiliriz.
    2) Multilogini nasıl engelleyebiliriz. Örneğin kullanıcı cep telefonundan login oldu. Peşinden bilgisayardan da login olduğunda cep telefonundaki oturumun kapanması gerekiyor.
    3) Register,Login veya diğer işlemlerde TwoFactor(SMS veya GoogleAuth) kullanmak istersem senaryo nasıl olmalı sizce. Örneğin; Register sayfasında bilgileri grip post ettiğimde gerekli validation kontrolleri okey ise veri tabanına yazma işlemi olmadan SMS Kodu kontrol ekranına yönlendirmek, kodu doğru girerse de register ekranında girilen bilgileri veritabanına kaydedip kullanıcıyı login etmek istiyorum. Yada sizin önerebileceğiniz farklı ve daha basit bir yöntem var mı?

    • Celal dedi ki:

      4) TwoFactor kontrolünü hemen her işlem onayı için kullanmak istersem TwoFactor kontrolünü bir modül haline nasıl çevirebilrim.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak.