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.
1 |
<span class="crayon-e">dotnet </span><span class="crayon-e">add </span><span class="crayon-t">package</span> <span class="crayon-v">ServiceStack</span><span class="crayon-sy">.</span><span class="crayon-v">Redis</span><span class="crayon-sy">.</span><span class="crayon-v">Core</span> <span class="crayon-o">--</span><span class="crayon-i">version</span> <span class="crayon-cn">5.1.0</span> |
Son olarak, Redis ayarları için appsettings.json aşağıdaki gibi değiştirilir:
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "RedisConfig": { "host": "127.0.0.1", "port": 6379, "name": "master" } } |
İ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():
1 2 3 4 5 |
services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(60); options.Cookie.HttpOnly = true; }); |
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.
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 73 74 75 76 77 78 |
using System; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Filters; using ServiceStack.Redis; using token.Controllers; public class LoginFilter : IActionFilter { RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; public void OnActionExecuting(ActionExecutingContext context) { string actionName = (string)context.RouteData.Values["action"]; string controllerName = (string)context.RouteData.Values["controller"]; var controller = (HomeController)context.Controller; if (HasIgnoreAttribute(context)) { return; } //context.HttpContext.Session.TryGetValue("token", out var result); //Prefer Redis using (IRedisClient client = new RedisClient(conf)) { context.HttpContext.Session.TryGetValue("UniqueUserName", out var name); string userName = name != null ? System.Text.Encoding.UTF8.GetString(name) : ""; var result = client.Get<string>(userName); if (result == null) { context.Result = controller.RedirectToAction("Login", "Home"); } else { string refreshToken = IsRefreshToken(result, context); if (!String.IsNullOrWhiteSpace(refreshToken)) { context.Result = controller.RedirectToAction(actionName, controllerName, new { token = refreshToken }); } } } } public void OnActionExecuted(ActionExecutedContext context) { } public bool HasIgnoreAttribute(ActionExecutingContext context) { foreach (var filterDescriptors in ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.CustomAttributes) { if (filterDescriptors.AttributeType == typeof(IgnoreAttribute)) { return true; } } return false; } public string IsRefreshToken(string sessionToken, ActionExecutingContext context) { //string tokenSession = System.Text.Encoding.UTF8.GetString(sessionToken).Split('æ')[1]; string tokenSession = sessionToken.Split('æ')[1]; DateTime sessionCreateTime = DateTime.Parse(tokenSession); TimeSpan remainingTime = DateTime.Now - sessionCreateTime; if (remainingTime.TotalSeconds >= 40) { using (IRedisClient client = new RedisClient(conf)) { string token = Guid.NewGuid().ToString() + "æ" + DateTime.Now; //context.HttpContext.Session.Set("token", System.Text.Encoding.UTF8.GetBytes(token)); context.HttpContext.Session.TryGetValue("UniqueUserName", out var name); string userName = System.Text.Encoding.UTF8.GetString(name); client.Set(userName, token,DateTime.Now.AddMinutes(1)); return token; } } return string.Empty; } } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
[IgnoreAttribute] [HttpPost] public ActionResult LogIn(string name, string password) { //Static UserName Password Check if ((name == "bora" && password == "1234") || (name=="duru" && password=="4321")) { string token = Guid.NewGuid().ToString() + "æ" + DateTime.Now; HttpContext.Session.Set("UniqueUserName", System.Text.Encoding.UTF8.GetBytes(name)); //HttpContext.Session.Set("token", System.Text.Encoding.UTF8.GetBytes(token)); //Prefer Redis using (IRedisClient client = new RedisClient(conf)) { client.Set(name, token,DateTime.Now.AddMinutes(1)); } ViewBag.Token = token; return Content(token); } else { return Content("Error"); } } |
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.
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 |
public async Task<IActionResult> Gazete(string token) { if (token != null) { ViewBag.Token = token; } HttpContext.Session.TryGetValue("UniqueUserName", out var name); string userName = name != null ? System.Text.Encoding.UTF8.GetString(name) : ""; ViewBag.UserName = userName; //return View(); /*using (var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true })*/ var handler = new HttpClientHandler(); handler.ServerCertificateCustomValidationCallback = System.Net.Http.HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; using (var client = new HttpClient(handler)) { //HttpContext.Session.TryGetValue("token", out var tokenValue); //Prefer Redis using (IRedisClient redisClient = new RedisClient(conf)) { string tokenValue=redisClient.Get<string>(userName); string tokenSession = tokenValue.Split('æ')[0]; var response = await client.GetAsync($"https://localhost:1923/api/news/{userName}/{tokenSession}"); response.EnsureSuccessStatusCode(); string content = await response.Content.ReadAsStringAsync(); var data = JsonConvert.DeserializeObject<List<Kategoris>>(content); return View(data); } } } |
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.
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
@model List<Kategoris> <script src="~/lib/jquery/dist/jquery.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" /> <script> $(document).ready(function () { $("#commandDiv").hide(); $("#detailDiv").hide(); $("#comboCategory").change(function(){ fillNews($( this ).val()); }); $("#comboNews").change(function(){ fillNewsDetail($('option:selected', this).attr('detail')); fillCommands($('option:selected', this).val()); }); var token=localStorage.getItem('token').split("æ")[0]; function fillNews(categoryID){ $("#detailDiv").hide(); $("#commandDiv").hide(); var token=localStorage.getItem('token').split("æ")[0]; $.getJSON("/api/news/"+categoryID+"/@ViewBag.UserName/"+token).done(function (data) { if(data.length==0) { window.location.href = 'https://localhost:1923'; } if(data[0].refreshToken!=null) { alert(data[0].refreshToken); localStorage.setItem("token", data[0].refreshToken); } var items = []; items.push("<option>Haber Seçin</option>"); for (i = 0; i < data.length; i++) { items.push("<option detail='" + data[i].detay + "' value=" + data[i].id + ">" + data[i].baslik + "</option>"); } $("#comboNews").html(items.join(' ')); }); $("#comboNews").show(); } function fillNewsDetail(detail){ $("#newsDetail").html(detail); $("#detailDiv").show(); } function fillCommands(newsID) { $("#commandDiv").hide(); var token=localStorage.getItem('token').split("æ")[0]; $.getJSON("/api/news/command/"+newsID+"/@ViewBag.UserName/"+token).done(function (data) { console.log("Data:"+JSON.stringify(data)); if(data.length==0) { window.location.href = 'https://localhost:1923'; } if(data[0].refreshToken!=null) { alert(data[0].refreshToken); localStorage.setItem("token", data[0].refreshToken); } var items = []; for (i = 0; i < data.length; i++) { items.push("<tr><td> [<b>" + data[i].isim + "</b>]:" + data[i].icerik+ "</td><td>"+ data[i].tarih +"</td></tr>"); } $("#tableCommand").html(items.join(' ')); }); $("#commandDiv").show(); } }); </script> @if(@ViewBag.Token!=null) { <script> alert('@Html.Raw(@ViewBag.Token)'); localStorage.setItem("token", '@Html.Raw(@ViewBag.Token)'); </script> } <div class="container"> <table class="table"> <thead> <tr> <th>Kategoryler</th> <th>Haberler</th> </tr> </thead> <tbody> <tr> <td style="width: 10%"> <select id="comboCategory"> <option>Kategory Seçin</option> @foreach(var item in Model) { <option value="@item.id"> @item.Ad</option> } </select> </td> <td style="width: 10%"> <select id="comboNews"> </select> </td> <td style="width: 10%"></td> </tr> </tbody> </table> <div class="form-group" id="detailDiv"> <label for="comment">Haber Detay:</label> <textarea class="form-control" rows="5" id="newsDetail"></textarea> </div> <div class="form-group" id="commandDiv"> <label for="comment">Yorumlar:</label> <table class="table"> <tbody id="tableCommand"> <tr> <td> </td> </tr> </tbody> </table> </div> </div> |
Ş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.
- “public string IsRefreshToken(string userName,string token)”: Parametre olarak yine tekil kullanıcı adı (userName) ve kontrol edilecek token parametre olarak alını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.
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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Mvc; using ServiceStack.Redis; [Route("api/[controller]")] [ApiController] public class NewsController : ControllerBase { RedisEndpoint conf = new RedisEndpoint() { Host = "127.0.0.1", Port = 6379 }; [HttpGet("{userName}/{token}")] public List<Kategoris> Category(string userName, string token) { //var session = HttpContext.Session; //if (session != null) //{ using (IRedisClient client = new RedisClient(conf)) { var result = client.Get<string>(userName); //HttpContext.Session.TryGetValue("token", out var result); if (result != null) { string tokenRedis = result.Split('æ')[0]; if (tokenRedis == token) { Console.WriteLine("Geçerli token :" + token); using (GazeteContext context = new GazeteContext()) { return context.Kategoris.ToList(); } } else { Console.WriteLine("Geçerli bir token değil:" + token); //Response.Redirect("https://localhost:5001"); return new List<Kategoris>(); } } else { //Response.Redirect("https://localhost:5001"); return new List<Kategoris>(); } } //} //Console.WriteLine("Geçerli bir session yok"); //Response.Redirect("https://localhost:5001"); //return new List<Kategoris>(); } [HttpGet("{id}/{userName}/{token}")] public List<Habers> CategoryNews(int id, string userName, string token) { //var session = HttpContext.Session; //if (session != null) //{ using (IRedisClient client = new RedisClient(conf)) { var result = client.Get<string>(userName); //HttpContext.Session.TryGetValue("token", out var result); if (result != null) { string refreshToken = IsRefreshToken(userName,result); string tokenRedis = result.Split('æ')[0]; if (tokenRedis == token) { Console.WriteLine("CategoryNews:" + token); using (GazeteContext context = new GazeteContext()) { var listNews = context.Habers.Where(ha => ha.Kategori_id == id).ToList(); if (!String.IsNullOrWhiteSpace(refreshToken)) { listNews.ForEach(command => { command.RefreshToken = refreshToken; }); } return listNews; } } else { Console.WriteLine("Geçerli bir token değil:" + token); //Response.Redirect("https://localhost:5001"); return new List<Habers>(); } } else { //Response.Redirect("https://localhost:5001"); return new List<Habers>(); } } //} //Console.WriteLine("Geçerli bir session yok"); //Response.Redirect("https://localhost:5001"); //return new List<Habers>(); } [HttpGet("command/{id}/{userName}/{token}")] public List<Yorums> CommandNews(int id, string userName, string token) { //var session = HttpContext.Session; //if (session != null) //{ //HttpContext.Session.TryGetValue("token", out var result); using (IRedisClient client = new RedisClient(conf)) { var result = client.Get<string>(userName); if (result != null) { string refreshToken = IsRefreshToken(userName,result); string tokenSession = result.Split('æ')[0]; if (tokenSession == token) { Console.WriteLine("CommandNews:" + token); using (GazeteContext context = new GazeteContext()) { var listCommand = context.Yorums.Where(yor => yor.Haber_id == id).ToList(); if (!String.IsNullOrWhiteSpace(refreshToken)) { listCommand.ForEach(command => { command.RefreshToken = refreshToken; }); } return listCommand; } } else { Console.WriteLine("Geçerli bir token değil:" + token); //Response.Redirect("https://localhost:5001"); return new List<Yorums>(); } } else { //Response.Redirect("https://localhost:5001"); return new List<Yorums>(); } } //} //Console.WriteLine("Geçerli bir session yok"); //Response.Redirect("https://localhost:5001"); //return new List<Yorums>(); } public string IsRefreshToken(string userName,string token) { string tokenSession = token.Split('æ')[1]; DateTime sessionCreateTime = DateTime.Parse(tokenSession); TimeSpan remainingTime = DateTime.Now - sessionCreateTime; if (remainingTime.TotalSeconds >= 40) { using (IRedisClient client = new RedisClient(conf)) { string newToken = Guid.NewGuid().ToString() + "æ" + DateTime.Now; client.Set(userName, token,DateTime.Now.AddMinutes(1)); return token; } } return string.Empty; } } |
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
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.
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.
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ı?
4) TwoFactor kontrolünü hemen her işlem onayı için kullanmak istersem TwoFactor kontrolünü bir modül haline nasıl çevirebilrim.