Ö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.
#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.JwtBearerGerekli paketleri ister nuget paket yöneticisi isterseniz konsoldan kurduktan sonra test ediyoruz.
dotnet runProje 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
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
namespace SignalRJwtChatApi.DTOs;
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}DTOs/LoginRequest.cs
namespace SignalRJwtChatApi.DTOs;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}DTOs/AuthResponse.cs
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
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
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.
// ö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
builder.Services.AddSingleton<UserService>();
builder.Services.AddScoped<JwtService>();JWT Authentication ayarlarını ekliyoruz.
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
builder.Services.AddAuthorization();Sırada SignalR eklemek var
builder.Services.AddSignalR();Middleware’leri aktif edelim
app.UseAuthentication();
app.UseAuthorization();Hub endpointlerini açalım
app.MapHub<ChatHub>("/chatHub");Henüz hub oluşturmadık ama Program.cs de eklememiz gerekenleri şimdilik ekleyelim.
Program.cs son hali:
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
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
builder.Services.AddSingleton<IUserIdProvider, CustomUserIdProvider>();SignalR Hub’u oluşturalım
Hubs/ChatHub.cs
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)
<!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>
Bir yanıt yazın