#2 Gelişme: JWT + SignalR ile güvenli sohbet odası ve direk mesajlaşma

Önceki yazımda, bir .NET Web API projesine basit şekilde SignalR entegre etmiş ve temel bir sohbet odası oluşturmuştuk. Oradaki amacımız, temel WebSocket mantığını ve .NET Web API proje yapısını anlamaktı. Bu yazıda ise bir adım daha ileri gidip JWT ile kimlik doğrulama, oturum yönetimi ve yetki kontrolü ekleyeceğiz.

Hadi sıfırdan, adım adım başlayalım!

Paketler

Öncelikle bir .net core web api projesi oluşturuyorum ve signalR paketini ve gerekli JWT paketlerini ekliyorum.

Visual studio ve nuget paket yöneticisini kullanarak kolayca yapabilirsiniz, konsol örneklerini de buraya bırakıyorum.

Bash
#Proje oluşturuyoruz
dotnet new webapi -n SignalRJwtChatApi
# proje dizinine geçiyoruz
cd SignalRJwtChatApi
# Gerekli paketler 1 signalR
dotnet add package Microsoft.AspNetCore.SignalR
# Gerekli paketler 2 JWT
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Gerekli paketleri ister nuget paket yöneticisi isterseniz konsoldan kurduktan sonra test ediyoruz.

Bash
dotnet run

Proje Yapısı

Öncelikle proje/dosya yapımızı oluşturalım.

  • Controllers
  • Hubs
  • Entities
  • DTOs
  • Services

Konumuz veritabanı veya katmanlı mimari olmadığı için daha fazla katman oluşturmayacağım, bu şekilde bir örnek JWT ve SignalR kullanımını anlamamız için yeterli.

Burada en farklı yapı Hubs dizini olacaktır, SignalR ile ilgili konfigürasyonu / ayarları burada tutacağız, onun dışında veritabanına ihtiyacımız yok şimdilik.

Şimdilik kullanıcı bilgisini Liste olarak DI içinde tutacağız. Programı durdurunca tüm bilgi silinecek fakat bu örnek için gayet yeterli.

Şimdı sırayla dosyalarımızı oluşturalım.

Modeller

Entities/User.cs

C#
namespace SignalRJwtChatApi.Entities;

public class User
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public string Role { get; set; } = "User";
}

Bu model:

Sistemimideki kullanıcıyı temsil edecek, ileride DB tablosu olarak da kullanabiliriz fakat şimdilik sadece bir kontrat gibi düşünebilirsiniz.

DTOs

DTO’lar request’ler de taşıyacağımız veriyi temsil edecek bizim için.

Öncelikle kullanıcıların sisteme kaydı ile başlıyoruz.

DTOs/RegisterRequest.cs

C#
namespace SignalRJwtChatApi.DTOs;

public class RegisterRequest
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}

DTOs/LoginRequest.cs

C#
namespace SignalRJwtChatApi.DTOs;

public class LoginRequest
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}

DTOs/AuthResponse.cs

C#
namespace SignalRJwtChatApi.DTOs;

public class AuthResponse
{
    public string Token { get; set; } = string.Empty;
    public string Username { get; set; } = string.Empty;
}

Kullanıcı girişi için kullanacağımız response sınıfımız, giriş sonrası veya kayıt sonrası bu şekilde bir response döneceğiz

Servisler

Sırada servis sınıflarımız var, bu sınıflar uygulamamızda iş parçacıklarını temsil ediyor, controller’ın şişmemesi ve karmaşıklaşmaması için oluşturduğumuz ara sınıflar.

Services/UserService.cs

C#
using SignalRJwtChatApi.Entities;

namespace SignalRJwtChatApi.Services;

public class UserService
{
    private readonly List<User> _users = new();

    public User? GetByUsername(string username)
    {
        return _users.FirstOrDefault(x => 
            x.Username.Equals(username, StringComparison.OrdinalIgnoreCase));
    }

    public User? GetById(string id)
    {
        return _users.FirstOrDefault(x => x.Id == id);
    }

    public User Create(string username, string password, string role = "User")
    {
        var user = new User
        {
            Username = username,
            Password = password,
            Role = role
        };

        _users.Add(user);
        return user;
    }

    public bool ValidateCredentials(string username, string password)
    {
        return _users.Any(x =>
            x.Username.Equals(username, StringComparison.OrdinalIgnoreCase) &&
            x.Password == password);
    }
}

Kullanıcı Girişi

Sırada kullanıcının işlemler için ulaşacağı endpointler var.

Controllers/AuthController.cs

C#
using Microsoft.AspNetCore.Mvc;
using SignalRJwtChatApi.DTOs;
using SignalRJwtChatApi.Services;

namespace SignalRJwtChatApi.Controllers;

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserService _userService;
    private readonly JwtService _jwtService;

    public AuthController(UserService userService, JwtService jwtService)
    {
        _userService = userService;
        _jwtService = jwtService;
    }

    [HttpPost("register")]
    public IActionResult Register(RegisterRequest request)
    {
        var existingUser = _userService.GetByUsername(request.Username);

        if (existingUser is not null)
            return BadRequest("Bu kullanıcı adı zaten kullanılıyor.");

        var user = _userService.Create(request.Username, request.Password);

        return Ok(new
        {
            message = "Kullanıcı oluşturuldu",
            user.Id,
            user.Username,
            user.Role
        });
    }

    [HttpPost("login")]
    public IActionResult Login(LoginRequest request)
    {
        var isValid = _userService.ValidateCredentials(request.Username, request.Password);

        if (!isValid)
            return Unauthorized("Kullanıcı adı veya şifre hatalı");

        var user = _userService.GetByUsername(request.Username)!;
        var token = _jwtService.GenerateToken(user);

        return Ok(new AuthResponse
        {
            Token = token,
            Username = user.Username
        });
    }
}

Şimdi sırada Program.cs ve SignalR konfigürasyonu var.

Program.cs

Paketleri dahil ediyoruz.

C#
// öncelikle kullandığımız paketleri dahil ediyoruz. projemizin en üstüne
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using SignalRJwtChatApi.Services;

Servislerimizi kaydediyoruz

C#
builder.Services.AddSingleton<UserService>();
builder.Services.AddScoped<JwtService>();

JWT Authentication ayarlarını ekliyoruz.

C#
builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!);

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateIssuerSigningKey = true,
        ValidateLifetime = true,

        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(key)
    };

    // SignalR için kritik kısım
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];

            var path = context.HttpContext.Request.Path;

            if (!string.IsNullOrEmpty(accessToken) &&
                path.StartsWithSegments("/chatHub"))
            {
                context.Token = accessToken;
            }

            return Task.CompletedTask;
        }
    };
});

Normalde JWT header’dan gelir ama biz SignalR kullandığımız için Query String den alıyoruz.
context.Request.Query[“access_token”]

bu kısım bunu temsil ediyor.

Authorization Ekleyelim

C#
builder.Services.AddAuthorization();

Sırada SignalR eklemek var

C#
builder.Services.AddSignalR();

Middleware’leri aktif edelim

C#
app.UseAuthentication();
app.UseAuthorization();

Hub endpointlerini açalım

C#
app.MapHub<ChatHub>("/chatHub");

Henüz hub oluşturmadık ama Program.cs de eklememiz gerekenleri şimdilik ekleyelim.

Program.cs son hali:

C#
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using SignalRJwtChatApi.Services;
using SignalRJwtChatApi.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddSingleton<UserService>();
builder.Services.AddScoped<JwtService>();

builder.Services.AddSignalR();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    var key = Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!);

    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateIssuerSigningKey = true,
        ValidateLifetime = true,

        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(key)
    };

    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            var accessToken = context.Request.Query["access_token"];
            var path = context.HttpContext.Request.Path;

            if (!string.IsNullOrEmpty(accessToken) &&
                path.StartsWithSegments("/chatHub"))
            {
                context.Token = accessToken;
            }

            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapHub<ChatHub>("/chatHub");

app.Run();

SignalR & Hub Kısmı

SignalR da kullanıcıların birer id ye sahip olması gerekli o nedenle bir ID provider oluşturmamız gerekitor.

Services/CustomUserIdProvider.cs

C#
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;

namespace SignalRJwtChatApi.Services;

public class CustomUserIdProvider : IUserIdProvider
{
    public string? GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    }
}

Program.cs de tanıtalım

C#
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();

SignalR Hub’u oluşturalım

Hubs/ChatHub.cs

C#
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;

namespace SignalRJwtChatApi.Hubs;

[Authorize]
public class ChatHub : Hub
{
    // ROOM JOIN
    public async Task JoinRoom(string roomName)
    {
        var username = Context.User?.Identity?.Name;

        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);

        await Clients.Group(roomName)
            .SendAsync("UserJoined", $"{username} joined {roomName}");
    }

    // ROOM LEAVE
    public async Task LeaveRoom(string roomName)
    {
        var username = Context.User?.Identity?.Name;

        await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);

        await Clients.Group(roomName)
            .SendAsync("UserLeft", $"{username} left {roomName}");
    }

    // ROOM MESSAGE
    public async Task SendMessageToRoom(string roomName, string message)
    {
        var username = Context.User?.Identity?.Name;
        var userId = Context.UserIdentifier;

        await Clients.Group(roomName)
            .SendAsync("ReceiveMessage", new
            {
                userId,
                username,
                roomName,
                message,
                sentAt = DateTime.UtcNow
            });
    }

    // PRIVATE MESSAGE
    public async Task SendPrivateMessage(string targetUserId, string message)
    {
        var username = Context.User?.Identity?.Name;
        var senderId = Context.UserIdentifier;

        await Clients.User(targetUserId)
            .SendAsync("ReceivePrivateMessage", new
            {
                fromUserId = senderId,
                fromUsername = username,
                message,
                sentAt = DateTime.UtcNow
            });
    }
}


Test & FrontEnd

Frontend kısmı (test için)

C#
<!DOCTYPE html>
<html>
<head>
    <title>SignalR JWT Chat</title>
</head>
<body>

<h2>Login</h2>
<input id="username" placeholder="username" />
<input id="password" placeholder="password" />
<button onclick="login()">Login</button>

<hr/>

<h2>Chat</h2>

<input id="room" placeholder="room name" />
<button onclick="joinRoom()">Join Room</button>

<br/><br/>

<input id="message" placeholder="message" />
<button onclick="sendRoomMessage()">Send Room Message</button>

<br/><br/>

<input id="targetUserId" placeholder="target userId" />
<input id="privateMessage" placeholder="private message" />
<button onclick="sendPrivate()">Send Private Message</button>

<hr/>

<h3>Logs</h3>
<ul id="logs"></ul>

<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/7.0.5/signalr.min.js"></script>

<script>
    let token = "";
    let connection = null;

    function log(msg) {
        const li = document.createElement("li");
        li.innerText = msg;
        document.getElementById("logs").appendChild(li);
    }

    async function login() {
        const username = document.getElementById("username").value;
        const password = document.getElementById("password").value;

        const res = await fetch("https://localhost:5001/api/auth/login", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ username, password })
        });

        const data = await res.json();
        token = data.token;

        log("Login başarılı, token alındı");

        connectSignalR();
    }

    function connectSignalR() {
        connection = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:5001/chatHub", {
                accessTokenFactory: () => token
            })
            .withAutomaticReconnect()
            .build();

        // ROOM mesajı
        connection.on("ReceiveMessage", (msg) => {
            log(`[ROOM] ${msg.username}: ${msg.message}`);
        });

        // PRIVATE mesaj
        connection.on("ReceivePrivateMessage", (msg) => {
            log(`[PRIVATE] ${msg.fromUsername}: ${msg.message}`);
        });

        connection.on("UserJoined", (msg) => log(msg));
        connection.on("UserLeft", (msg) => log(msg));

        connection.start()
            .then(() => log("SignalR bağlandı"))
            .catch(err => console.error(err));
    }

    function joinRoom() {
        const room = document.getElementById("room").value;
        connection.invoke("JoinRoom", room);
    }

    function sendRoomMessage() {
        const room = document.getElementById("room").value;
        const message = document.getElementById("message").value;

        connection.invoke("SendMessageToRoom", room, message);
    }

    function sendPrivate() {
        const targetUserId = document.getElementById("targetUserId").value;
        const message = document.getElementById("privateMessage").value;

        connection.invoke("SendPrivateMessage", targetUserId, message);
    }
</script>

</body>
</html>

Yorumlar

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir