Asp.NET Core – SignalR Serisi #10 – Derinlemesine Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)

Merhaba,

Klasik web uygulamalarında olduğu gibi SignalR mimarisiyle real time çalışan uygulamalarda da kullanıcı doğrulama ve yetkilendirme ihtiyacı hissedebilir ve eşzamanlı veri akışı esnasında tanımlı kullanıcılar dışında etkileşime müdahale olunmasını istemeyebiliriz. Bu içeriğimizde, bu ihtiyaca istinaden tüm detaylarıyla bir izahatte bulunacak ve SignalR mimarisinde Authentication ve Authorization işlemlerini ele alacağız.

Başlarken

İçeriğimizin derinliklerine girmeden önce izleyeceğimiz yolu genel hatlarıyla gösterebilmek için klavuz niteliğinde açıklama yapmakta fayda var. SignalR ile authentication ve authorization yapılanmasını inceleyebilmek için JWT’den faydalanacak ve tüm detaylarıyla bir Asp.NET Core Web API uygulamasına JWT entegrasyonunun nasıl yapıldığını inceleyeceğiz. Ardından SignalR’ın bildiğimiz yapılanması ile basit bir uygulama geliştirecek ve bunun yanında kullanıcı ekleme ve login olma gibi işlemleri Hub’lar üzerinden gerçekleştireceğiz. Kullanıcı doğrulama ardından yetkilendirme(authorization) konusuna değineceğiz. Süreci daha da efektif bir hale getirebilmek için Refresh Token stratejisinden faydalanacağız. Tüm süreç; real time bir şekilde, gerekli kritikleri gerektiği noktalarda yaparak adım adım bir şekilde tasarlanacaktır.

Şimdi bir Asp.NET Core Web API uygulaması oluşturup, hali hazırda içeriğe devam edebiliriz.

JWT Mekanizmasının İnşası

Konumuz her ne kadar SignalR’da olsa JWT ile authentication ve authorization işlemleri gerçekleştireceksek eğer illaki öncelikle JWT üretebilecek mekanizmayı inşa etmemiz gerekmektedir. Normalde bu mekanizmayı Asp.NET Core 3.1 ile Token Bazlı Kimlik Doğrulaması ve Refresh Token Kullanımı(JWT) başlıklı makaleyi referans ederek, inşa edildiğini varsayıp geçebilirdik. Lakin sizlerin ilgili konuyu pekiştirmeniz ve daha da hakim olabilmeniz için burada da tekrardan entegrasyon sürecine değinmeyi ve ele almayı uygun görmekteyim.

Hadi başlayalım 🙂

  • Adım 1
    Her şeyden önce uygulamada kullanıcılara karşılık gelecek olan ‘User’ modelini ve context nesnesini oluşturarak başlayalım.

        public class User
        {
            public int Id { get; set; }
            public string Username { get; set; }
            public string Password { get; set; }
            public string RefreshToken { get; set; }
            public DateTime RefreshTokenEndDate { get; set; }
        }
    

    Burada dikkat ederseniz eğer kullanıcı oturumunun sürekliliğine dair refresh token stratejisini uygulayacağımızdan dolayı ‘RefrestToken’ ve ‘RefreshTokenEndDate’ propertyleri tanımlanmıştır. Konuya dair daha detaylı bilgi için bknz : Asp.NET Core 3.1 ile Token Bazlı Kimlik Doğrulaması ve Refresh Token Kullanımı(JWT)

    Tasarlayacağınız context nesnesi üzerinden oluşturduğumuz ‘User’ entity’sini DbSet olarak belirtiniz ve bu işlem neticesinde gerekli migration işlemlerini ve migrate etmeyi gerçekleştiriniz. Konuyu fazla dağıtmamak adına bu kısımların icra edildiğini varsayıyorum. Bknz : Asp.NET Core 2 MVC’de Migrations İle Veritabanı İşlemleri

  • Adım 2
    ‘Startup.cs’ dosyasında JWT konfigürasyonunu gerçekleştirelim. Bunun için aşağıdaki gibi ‘Authentication’ ve ‘JwtBearer’ servisini uygulamaya dahil edelim.

           services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateAudience = true,
                            ValidateIssuer = true,
                            ValidateLifetime = true,
                            ValidateIssuerSigningKey = true,
                            ValidIssuer = _configuration["JWT:Issuer"],
                            ValidAudience = _configuration["JWT:Audience"],
                            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SecurityKey"])),
                            ClockSkew = TimeSpan.Zero
                        };
                    });
    

    Burada JWT konfigürasyonu için uygulamaya Microsoft.AspNetCore.Authentication.JwtBearer kütüphanesinin yüklenmesi gerekmektedir. Tanımlama sürecinde propertylerin ne olduğuna dair detaylı bilgi için Asp.NET Core 3.1 ile Token Bazlı Kimlik Doğrulaması ve Refresh Token Kullanımı(JWT) adresindeki makaleye göz atabilirsiniz. Ayrıca ‘appsettings.json’ dosyasındaki statik değerler için aşağıdaki gibi tanımlama yapabilirsiniz.

    {
      .
      .
      .
      "JWT": {
        "Audience": "localhost",
        "Issuer": "www.gencayyildiz.com",
        "SecurityKey": "sebepsiz boş yere ayrılacaksann. ... ."
      }
    }
    
  • Adım 3
    Web tarayıcısı üzerinden client’ların erişimini sağlıklı bir şekilde sağlayabilmek için ‘Startup.cs’ üzerinden CORS politikasını ayarlayalım. Tabi ki de client’lar sadece web tabanlı olmak zorunda değil. Console, Windows, Mobile, Akıllı ütü vs. olabilir 🙂

            public void ConfigureServices(IServiceCollection services)
            {
                .
                .
                .
                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                 {
                     options.TokenValidationParameters = new TokenValidationParameters
                     {
                         ValidateAudience = true,
                         ValidateIssuer = true,
                         ValidateLifetime = true,
                         ValidateIssuerSigningKey = true,
                         ValidIssuer = _configuration["JWT:Issuer"],
                         ValidAudience = _configuration["JWT:Audience"],
                         IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SecurityKey"])),
                         ClockSkew = TimeSpan.Zero
                     };
                 });
                .
                .
                .
            }
    

    ‘UseCors’ middleware’ini çağıralım.

                .
                .
                .
                app.UseCors();
                .
                .
                .
    
  • Adım 4
    Uygulamada authentication ve authorization işlevlerini kullanabilmek için ‘UseAuthentication’ ve ‘UseAuthorization’ middleware’lerini çağıralım.

            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {
                .
                .
                .
                app.UseRouting();
                app.UseCors();
    
                app.UseAuthentication();
                app.UseAuthorization();
    
                app.UseEndpoints...
                .
                .
                .
            }
    

    Bu middleware’ler sayesinde tasarladığımız uygulamaya tüm kullanıcı denetimi ve rol yönetimi işlevleri kazandırılmaktadır.

  • Adım 5
    Üretilecek token ve refresh token değerlerini taşıyacak model’i tasarlayalım.

        public class Token
        {
            public string AccessToken { get; set; }
            public DateTime Expiration { get; set; }
            public string RefreshToken { get; set; }
        }
    
  • Adım 6
    Tüm bu inşalardan sonra artık token üretiminden sorumlu Token Handler sınıfını oluşturabiliriz.

        public class TokenHandler
        {
            readonly IConfiguration _configuration;
            public TokenHandler(IConfiguration configuration)
            {
                _configuration = configuration;
            }
            public Token CreateAccessToken(int duration)
            {
                Token tokenInstance = new Token();
                SymmetricSecurityKey securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SecurityKey"]));
                SigningCredentials signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
                tokenInstance.Expiration = DateTime.Now.AddMinutes(duration);
                JwtSecurityToken securityToken = new JwtSecurityToken
                    (
                    issuer: _configuration["JWT:Issuer"],
                    audience: _configuration["JWT:Audience"],
                    expires: tokenInstance.Expiration,
                    notBefore: DateTime.Now,
                    signingCredentials: signingCredentials
                );
                JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
                tokenInstance.AccessToken = tokenHandler.WriteToken(securityToken);
                tokenInstance.RefreshToken = CreateRefreshToken();
                return tokenInstance;
            }
            public string CreateRefreshToken()
            {
                byte[] number = new byte[32];
                using RandomNumberGenerator random = RandomNumberGenerator.Create();
                random.GetBytes(number);
                return Convert.ToBase64String(number);
            }
        }
    

Evet, bu adımlar neticesinde uygulamaya JWT entegrasyonunu başarıyla sağlamış bulunmaktayız. Şimdi sıra SignalR mimarisi ile uygulamayı geliştirmeye geldi.

SignalR Uygulamasının Geliştirilmesi

SignalR mimarisi, server ve client olmak üzere ikiye ayrıldığından dolayı bizler öncelikle server kısmını inşa edecek ve ardından client olarak web uygulaması geliştireceğiz. Süreçte programatik davranışı tercih edeceğimizden dolayı, Hub’ları Strongly Typed Hubs özelliğini kullanarak geliştirmeye özen göstereceğiz.

SignalR Server Side
İlk olarak kullanıcı kayıt ve giriş işlemlerinin sorumluluğunu üstlenen hub’u geliştirelim. Bunun için öncelikle ‘ILoginHub’ arayüzünü, ardından ‘LoginHub’ sınıfını aşağıdaki gibi inşa edelim.

    public interface ILoginHub
    {
        Task Login(Token token);
        Task Create(bool result);
    }
    public class LoginHub : Hub<ILoginHub>
    {
        readonly IConfiguration _configuration;
        readonly SignalRExampleContext _context;
        public LoginHub(IConfiguration configuration, SignalRExampleContext context)
        {
            _configuration = configuration;
            _context = context;
        }
        public async Task Create(string userName, string password)
        {
            await _context.Users.AddAsync(new User
            {
                Password = password,
                Username = userName
            });

            await Clients.Caller.Create(await _context.SaveChangesAsync() > 0);
        }
        public async Task Login(string userName, string password)
        {
            User user = await _context.Users.FirstOrDefaultAsync(u => u.Username == userName && u.Password == password);
            Token token = null;
            if (user != null)
            {
                TokenHandler tokenHandler = new TokenHandler(_configuration);
                token = tokenHandler.CreateAccessToken(5);
                user.RefreshToken = token.RefreshToken;
                user.RefreshTokenEndDate = token.Expiration.AddMinutes(3);
                await _context.SaveChangesAsync();
            }
            await Clients.Caller.Login(user != null ? token : null);
        }
    }

Yukarıdaki kod bloğunu incelerseniz eğer ilgili hub kullanıcı kayıt ve giriş işlemlerinin sorumluluğunu üstlenmekte ve access token ile birlikte refresh token değerlerini üretmektedir. Böylece içeriğimizin ilk paragraflarında da değinildiği üzere authentication işlemleri için hub’lar üzerinden yürütülecek şekilde bir tasarım gerçekleştirilmektedir.

Devamında ise mesaj iletiminden sorumlu olan ‘MessageHub’ isimli class’ıda yine aynı şekilde inşa edelim.

    public interface IMessageHub
    {
        Task ReceiveMessage(string message);
    }
    [Authorize]
    public class MessageHub : Hub<IMessageHub>
    {
        public async Task SendMessage(string message)
        {
            await Clients.Others.ReceiveMessage(message);
        }
    }

Burada dikkatinizi ‘Authorize’ attribute’una çekerim. ‘Authorize’ attribute’u sayesinde, SignalR ile ‘MessageHub’a bağlanabilmek için geçerli bir JWT’ye ihtiyaç duyulmakta ve böylece kullanıcı denetimi sağlanmış olmaktadır. Makalenin ilerleyen satırlarında rol yönetimininde nasıl gerçekleştirilebileceği de ele alınacaktır.

Server tarafında son olarak; SignalR servisiyle birlikte, geliştirilen bu hub’ların endpointlerini aşağıdaki gibi tanımlayalım.

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            .
            .
            .
            services.AddSignalR();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            .
            .
            .
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapHub<LoginHub>("/login");
                endpoints.MapHub<MessageHub>("/message");
            });
        }
    }

SignalR Client Side
Yapısal olarak; kullanıcı kayıt, giriş ve chat olmak üzere üç aşamalı olacak şekilde son kullanıcıya tüm detaylarıyla hitap edecek özellikte bir client çalışması gerçekleştireceğiz. İlk olarak kullanıcı kayıt sayfasını geliştirerek başlayalım.
Create.html;

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="signalr.min.js"></script>
    <script src="jquery.min.js"></script>
    <script src="bootstrap.min.js"></script>
    <link rel="stylesheet" href="bootstrap.min.css">
    <script>
        $(document).ready(() => {

            const login = new signalR.HubConnectionBuilder()
                .withUrl("https://localhost:5001/login")
                .build();

            login.start();

            $("#btnCreateUser").click(() => login.invoke("Create", $("#txtUsername").val(), $("#txtPassword").val()));

            login.on("Create", result => {
                if (result)
                    alert("Kayıt işlemi başarıyla gerçekleştirildi.")
                else
                    alert("Kayıt gerçekleştirilirken beklenmeyen bir hatayla karşılaşıldı.")
            });
        });
    </script>
</head>
<body>
    <ul class="nav">
        <li class="nav-item">
            <a class="nav-link active" href="create.html">Create User</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="login.html">Login</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" href="chatroom.html">Chat Room</a>
        </li>
    </ul>
    <div class="row">
        <div class="col-md-3"></div>
        <div class="col-md-6">
            <h2>Create User</h2>
            <div class="form-group">
                <label>User Name</label>
                <input type="text" class="form-control" id="txtUsername">
            </div>
            <div class="form-group">
                <label>Password</label>
                <input type="password" class="form-control" id="txtPassword">
            </div>
            <button type="submit" class="btn btn-primary" id="btnCreateUser">Create User</button>
        </div>
        <div class="col-md-3"></div>
    </div>
</body>
</html>

Kaynak kodu incelerseniz eğer ‘/login’ hub’ına bağlanılmakta ve ilgili hub üzerinden aşağıdaki ekran görüntüsünde olduğu gibi kullanıcı kaydı gerçekleştirilmektedir.
Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)
Veritabanınada göz atıldığında ise,
Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)
kaydın başarıyla gerçekleştirildiğini göreceksiniz.

Sıra kullanıcı giriş sayfasını oluşturmaya geldi.
Login.html;

<!DOCTYPE html>
<html lang="en">
<head>
   <script src="signalr.min.js"></script>
   <script src="jquery.min.js"></script>
   <script src="bootstrap.min.js"></script>
   <link rel="stylesheet" href="bootstrap.min.css">
   <script>
      $(document).ready(() => {

         const login = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:5001/login")
            .build();

         login.start();

         $("#btnLogin").click(() => login.invoke("Login", $("#txtUsername").val(), $("#txtPassword").val()));

         login.on("Login", token => {
            if (token) {
               localStorage.setItem("accessToken", token.accessToken);
               localStorage.setItem("refreshToken", token.refreshToken);
               alert("Giriş başarılı.");
            } else
               alert("Lütfen kullanıcı adı ve şifrenizi kontrol ediniz.");
         });
      });
   </script>
</head>
<body>
   <ul class="nav">
      <li class="nav-item">
         <a class="nav-link active" href="create.html">Create User</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="login.html">Login</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="chatroom.html">Chat Room</a>
      </li>
   </ul>
   <div class="row">
      <div class="col-md-3"></div>
      <div class="col-md-6">
         <h2>Login</h2>
         <div class="form-group">
            <label>User Name</label>
            <input type="text" class="form-control" id="txtUsername">
         </div>
         <div class="form-group">
            <label>Password</label>
            <input type="password" class="form-control" id="txtPassword">
         </div>
         <button type="submit" class="btn btn-primary" id="btnLogin">Login</button>
      </div>
      <div class="col-md-3"></div>
   </div>
</body>
</html>

Burada ise kullanıcı adı ve şifre ile bulunulan giriş talebine, server’da ‘/login’ hub’ı tarafından karşılık verilmekte ve daha önce oluşturduğumuz ‘TokenHandler’ sınıfı ile acess token ve refresh token üretilmektedir. Üretilen token değerleri local storage üzerinde uygun anahtarlar eşliğinde depolanmaktadır.
Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)
Local storage’a göz atıldığında ise,

Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)
token değerlerinin kaydedildiğini göreceksiniz.

Son olarak kullanıcılar arasında mesajlaşmanın gerçekleştirilebilmesi için sıra chat sayfasının tasarlanmasına geldi.
Chatroom.html;

<!DOCTYPE html>
<html lang="en">
<head>
   <script src="signalr.min.js"></script>
   <script src="jquery.min.js"></script>
   <link rel="stylesheet" href="bootstrap.min.css">
   <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
   <script src="bootstrap.min.js"></script>
   <script>
      function connectionMessageWrite(state, message) {
         $("#connectionMessage").find("div")[1].removeAttribute("class");
         $("#connectionMessage").find("div")[1].setAttribute("class", "alert alert-" + state);
         $("#connectionMessage").find("div")[1].innerHTML = message;
         $("#connectionMessage").show(2000);
      }
      $(document).ready(() => {
         $("#logOut").click(() => {
            localStorage.removeItem("accessToken");
            localStorage.removeItem("refreshToken");
         });

         $(".toast").toast('show');
         const message = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:5001/message",
               { accessTokenFactory: () => localStorage.getItem("accessToken") != null ? localStorage.getItem("accessToken") : "" })
            .withAutomaticReconnect([10])
            .build();

         message.start()
            .then(() => connectionMessageWrite("success", "Bağlantı başarılı."))
            .catch(err => connectionMessageWrite("danger", "Bağlantı başarısız! Lütfen oturum açınız."));
         message.onclose((err) => $("#connectionMessage").hide(2000, () => {
            connectionMessageWrite("warning", "Bağlantı koptu!");
         }));

         $("#btnSendMessage").click(() => {
            message.invoke("SendMessage", $("#txtMessage").val());
            $("#txtMessage").val("");
         });
         let messageCount = 0;
         message.on("ReceiveMessage", message => {

            let object = '<div style="margin-top: 10px;" class="toast" id="toast-' + ++messageCount + '" role="alert" aria-live="assertive" aria-atomic="true" data-delay="5000">';
            object += ' <div class="toast-header">';
            object += '    <img src="messageicon.png" width="20px" class="rounded mr-2" alt="...">';
            object += '    <strong class="mr-auto">Client</strong>';
            object += '    <small class="text-muted">Biraz önce</small>';
            object += '    <button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">';
            object += '       <span aria-hidden="true">&times;</span>';
            object += '    </button>'
            object += ' </div>'
            object += ' <div class="toast-body">';
            object += message;
            object += ' </div>';
            object += '</div>';
            $("#divMessage").append(object);
            $("#toast-" + messageCount).toast('show');
         });

      });
   </script>
</head>
<body>
   <ul class="nav">
      <li class="nav-item">
         <a class="nav-link active" href="create.html">Create User</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="login.html">Login</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" href="chatroom.html">Chat Room</a>
      </li>
      <li class="nav-item">
         <a class="nav-link" style="cursor: pointer;" id="logOut">Logout</a>
      </li>
   </ul>
   <div class="row" id="connectionMessage" style="display: none;">
      <div class="col-md-3">
      </div>
      <div class="col-md-6">
         <div role="alert">

         </div>
      </div>
      <div class="col-md-3">
      </div>
   </div>
   <div class="row">
      <div class="col-md-3"></div>
      <div class="col-md-6">
         <h2>Chat Room</h2>
         <div class="form-group">
            <label>Message</label>
            <textarea class="form-control" id="txtMessage" placeholder="Mesajınızı giriniz." rows="3"></textarea>
         </div>
         <button type="submit" class="btn btn-primary" id="btnSendMessage">Send Message</button>
      </div>
      <div class="col-md-3"></div>
      <div class="col-md-3"></div>
      <div class="col-md-6" id="divMessage">

      </div>
      <div class="col-md-3"></div>
   </div>
</body>
</html>

Yukarıdaki kod bloğunu incelerseniz eğer; ‘Authorize’ attribute’u ile işaretlenmiş olan ‘/message’ hub’ına client’tan erişim sağlayabilmek için 24. satırda bulunan ‘withUrl’ metodunun ikinci parametresine, içerisinde ‘accessTokenFactory’ alanını barındıran bir object verilmesi ve bu alana server’dan gelen token’ın set edilmesi yeterli olacaktır. Böylece SignalR mimarisi, bahsedilen konfigürasyon sayesinde client’tan gelecek olan token değerini elde edip doğrulayacak ve kullanıcının bağlantı kurmasını sağlayacaktır. Tabi ki de geçersiz bir token değerinin kullanılması yahut token’ın süresinin dolması neticesinde hub’a bağlantı sağlanamayacak ve böylece denetim sağlanmış olacaktır.

İlgili sayfanın işlevsel açıdan ekran görüntüsü aşağıdaki gibidir;
Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)
Token aldıktan sonra client’lar arası mesaj iletimi ise aşağıdaki gibidir;
Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)

SignalR İle Refresh Token Kullanımı

SignalR ile birlikte refresh token’ı kullanabilmek için server’ın refresh token stratejisini benimsemesi gerekmektedir. Bizler bu içeriğimizin yukarıdaki satırlarında JWT mekanizmasının inşasını ele alırken refresh token inşasını da oluşturmuştuk. Lakin ekstradan refresh token üzerinden giriş kodunu da geliştirmemiz gerekmektedir. Bunun için ‘/login’ hub’da aşağıdaki gibi bir geliştirme gerçekleştirebiliriz;

    public class LoginHub : Hub<ILoginHub>
    {
        readonly IConfiguration _configuration;
        readonly SignalRExampleContext _context;
        .
        .
        .
        public async Task RefreshTokenLogin(string refreshToken)
        {
            User user = await _context.Users.FirstOrDefaultAsync(x => x.RefreshToken == refreshToken);
            Token token = null;
            if (user != null && user?.RefreshTokenEndDate > DateTime.Now)
            {
                TokenHandler tokenHandler = new TokenHandler(_configuration);
                token = tokenHandler.CreateAccessToken(1);

                user.RefreshToken = token.RefreshToken;
                user.RefreshTokenEndDate = token.Expiration.AddMinutes(1);
                await _context.SaveChangesAsync();
            }
            await Clients.Caller.Login(user != null ? token : null);
        }
    }

Ardından client’ta aşağıdaki gibi geliştirme yapılması gerekmektedir;
Chatroom.html;

         .
         .
         .
         const message = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:5001/message",
               { accessTokenFactory: () => localStorage.getItem("accessToken") != null ? localStorage.getItem("accessToken") : "" })
            .withAutomaticReconnect([10])
            .build();

         function start() {
            message.start()
               .then(() => connectionMessageWrite("success", "Bağlantı başarılı."))
               .catch(err => connectionMessageWrite("danger", "Bağlantı başarısız! Lütfen oturum açınız."));
         }
         start();

         message.onclose(() => $("#connectionMessage").hide(2000, () => {
            const login = new signalR.HubConnectionBuilder()
               .withUrl("https://localhost:5001/login")
               .build();
            connectionMessageWrite("warning", "Bağlantı koptu!");
            login.start().then(() => {
               login.invoke("RefreshTokenLogin", localStorage.getItem("refreshToken"));
               login.on("login", token => {
                  localStorage.setItem("accessToken", token.accessToken);
                  localStorage.setItem("refreshToken", token.refreshToken);
                  message.connection.accessTokenFactory = token.accessToken;
                  start();
               });
            });
         }));
         .
         .
         .

Yukarıdaki kod bloğunu incelerseniz eğer ‘/message’ hub’ına olan bağlantı close olduğu zaman ‘onclose’ fonksiyonu tetiklenmekte ve ilgili event içerisinde ‘/login’ hub’ına bağlantı sağlanarak server’da ki ‘RefreshTokenLogin’ metoduna refresh token değeri ileti olarak gönderilmektedir. Sunucuda doğrulanan refresh token, kullanıcıya dair yeni bir access token üretilmesini sağlamakta ve bu token değeri server tarafından tetiklenen client’ta ki ‘Login’ fonksiyonu(24. satır) aracılığıyla gönderilmekte ve client tarafından karşılanmaktadır. Ardından client, elde edilen bu token değeri ile ‘/message’ hub’ında ki ‘accessTokenFactory’ değerini değiştirmekte ve tekrardan ilgili hub’a bağlantıyı tazelemektedir. Tabi burada otomatik bağlantı konfigürasyonunun kullanıldığına dikkatinizi çekmekte fayda görmekteyim.

En nihayetinde yapılan bu refresh token yapılanması aşağıdaki ekran görüntüsünde olduğu gibi davranış sergilemekte ve eşzamanlı veri akışını hiçbir zaman sekteye uğratmayacak şekilde işlevselliğini göstermektedir.

Asp.NET Core – SignalR Serisi #10 - Kimlik Doğrulama(Authentication) ve Yetkilendirme(Authorization)

Örnek amaçlı farazi bir süreliğine oluşturulmuş token değerinin refresh token ile kullanıcı talebi olmaksızın temsili yenilenmesi…

SignalR İle Rol Yönetimi

SignalR ile rol yönetimi, bildiğiniz Asp.NET Core MVC yahut Web API ile birebir aynı mantıkta işlemektedir. Örneğin; nasıl ki Asp.NET Core Web API’da ‘Authorize’ olan controller’lara rol tanımlayabilmek için ‘Roles=”…”‘ propertysi kullanılıyorsa benzer mantıkla SignalR’da ki hub’lar içinde aynı özelliğin kullanılması gerekmektedir.

Sözgelimi ‘/message’ hub’ını ele alırsak;

    [Authorize(Roles = "Admin")]
    public class MessageHub : Hub<IMessageHub>
    {
        public async Task SendMessage(string message)
        {
            await Clients.Others.ReceiveMessage(message);
        }
    }

şeklinde tanımlanması, bu hub’a ‘Admin’ rolüne sahip olan yetkili kişilerin erişebileceğini ifade etmektedir.

Peki kullanıcının üstleneceği rolü nereden belirleyeceğiz?
Aslında bu sorununda cevabı yine bilinen klasik rol tanımlamasında yatmaktadır. Yani claim yapılanmasında. Kullanıcı giriş talebinde bulunup, doğrulandığı vakit, token değerinin üretilebilmesi için yukarılarda oluşturduğumuz ‘TokenHandler’ sınıfı kullanılmaktadır. Haliyle ilgili sınıf içerisinde ‘JwtSecurityToken’ nesnesinin ‘claims’ parametresi bu işlem için yeterli olacaktır. Şöyle ki;

    public class TokenHandler
    {
        readonly IConfiguration _configuration;
        .
        .
        .
        public Token CreateAccessToken(int duration)
        {
            .
            .
            .
            JwtSecurityToken securityToken = new JwtSecurityToken
                (
                issuer: _configuration["JWT:Issuer"],
                .
                .
                .
                claims: new List<Claim>
                {
                    new Claim(ClaimTypes.Role, "Admin")
                }
            );
            .
            .
            .
        }
        .
        .
        .
    }

Yukarıdaki kaynak kodun 20. satırında, üretilecek token’a ‘Admin’ rolü eklenmekte ve ilgili token ile yapılan isteklerde ‘Authorize(Roles = “Admin”)’ attribute’una karşılık doğrulama başarıyla gerçekleştirilmektedir.

Client Web Dışında Console App. yahut Windows Form Olursa?

Yazı serimizin sonraki makalelerinde, SignalR ile etkileşime giren client’ların web dışında farklı platformlardan olma durumunu ayrı bir başlık altında tam teferruatlı inceleyeceğiz. Lakin yeri gelmişken farklı platformlarda da authentication durumunun nasıl gerçekleştirildiğine dair detaya değinmekte fayda görmekteyim.

Eğer ki client olarak web dışında bir Console Application yahut Windows Form’da çalışıyor ve bir yandan da authentication kullanıyorsanız her ikisinde de aşağıdaki çalışmayı gerçekleştirebilirsiniz.

    class Program
    {
        static HubConnection connection;
        async static Task Main(string[] args)
        {
            connection = new HubConnectionBuilder().WithUrl("https://localhost:5001/message",
                options => options.AccessTokenProvider = () => Task.FromResult("...Token Value...")).Build();
            await connection.StartAsync();
            Console.WriteLine(connection.State);
            connection.On<string>("receiveMessage", message =>
            {
                Console.WriteLine($"--->{message}");
            });

            while (true)
            {
                Console.WriteLine("Gönderilecek mesajı yazınız.");
                await connection.InvokeAsync("SendMessage", Console.ReadLine());
            }
        }
    }

Görüldüğü üzere 7. satırdaki ‘AccessTokenProvider’ özelliği aracılığıyla elde edilen token değeri bağlantı talebine verilerek, connection başarıyla sağlanabilmektedir.

İşte bu kadar…

Nihai olarak, eşzamanlı uygulamalarda authentication, authorization, refresh token gibi yapılandırması zor ve ağır olan işlemlerin nasıl efektif bir şekilde inşa edildiğini adım adım irdelemiş bulunmaktayız. Umarım kalemim gam teline vurmuş, mürekkebim anlaşılabilir olmuştur. Bu noktaya kadar sabredip okuyan, özellikle okurken makaleye eşlik eden siz değerli okuyucularıma teşekkür ederim…

İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…

Not: Örnek server ve UI uygulamalarını indirebilmek için aşağıdaki linklere tıklayınız.

Bunlar da hoşunuza gidebilir...

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

*