Hero Background

Herkes İçin , Her Yerde Bilgi

Diller, kültürler ve sınırlar ötesinde okuyuculara ulaşan, özgün, araştırmaya dayalı ve insan emeğiyle hazırlanmış makaleler.

Keşfet

Öne Çıkan Makaleler

Tümünü Gör
Spring Security ile adım adım JWT Tabanlı Kimlik Doğrulama ve Yetkilendirme SüreçleriPratik Java Uygulamaları • 11 Aralık 2025Spring Security ile adım adım JWT TabanlıKimlik Doğrulama ve Yetkilendirme SüreçleriPratik Java Uygulamaları • 11 Aralık 2025Pratik Java Uygulamaları

Spring Security ile adım adım JWT Tabanlı Kimlik Doğrulama ve Yetkilendirme Süreçleri

Bilgisayar çağında , verinin ve bilginin artışı, bilginin korunmasını sağlama konusunda kritik bir gereksinim ortaya çıkarmıştır. Araştırmalar, dijital çağda üretilen veri ve bilgi miktarının, insanlık tarihi boyunca üretilen toplam veri miktarını aştığını göstermektedir.

1*EYKbL8aMQJLJKVO2UIVFOQ.jpeg

-Yıllara göre , veri hacmindeki dağılım-

Web’in gelişimiyle birlikte, statik tek sayfalık web sitelerinin yerini dinamik web sayfaları almıştır. Bu dönüşümün bir sonucu olarak, bilgi kaynaklarına erişimin güvenli hale getirilmesi ihtiyacı doğmuş, erişimlerimizin belirli güvenlik çerçeveleri aracılığıyla kontrol altına alınması sağlanmıştır. Bu güvenlik çerçeveleri, bilgi kaynaklarını yetkisiz kullanıcılar veya sistemlerden korumak amacıyla oluşturulmuştur.

Bu makalede, Spring projelerinde güvenliğin standardı olan Spring Security kullanarak servislerimize ve bilgi kaynaklarımıza erişimi nasıl izleyip düzenleyebileceğimizi inceleyeceğiz. Ayrıca, bu izleme mekanizmalarının nasıl etkili bir şekilde yapılandırılabileceğini de basit bir örnek üzerinden ele alacak , geliştirilmiş olan bu yöntemleri uygulayarak servislerimizi yetkisiz erişimlerden koruyacağız.

Uygulamamızı geliştirmeye başlamadan önce;

Bir Web Sitesinin Temel Güvenlik Gereksinimleri Nelerdir? sorusuna cevap verelim.

Yapılandırmalara geçmeden önce, bir web sitesinin güvenlik gereksinimlerini nelerin oluşturduğunu inceleyelim.
Bir web sitesinin temel güvenlik ihtiyaçları aşağıdaki bileşenlerden oluşur:

  1. Authentication

  2. Authorization

  3. Data Encryption

  4. Logging

  5. Firewall, IDS, and IPS Systems

  6. DDoS Protection

  7. Software Updates

  8. Database Security

  9. Error Management

  10. Backup Solutions

  11. Access Control Lists (ACLs)

  12. Security Measures Against Attacks like XSS, CSRF, etc.

Güvenlik ihtiyaçlarımızı karşılamak için, bu makalede kimlik doğrulama (authentication) ve yetkilendirme (authorization) süreçlerinin Spring Security kullanılarak nasıl yapıldığını inceleceğiz.

1- Spring Boot’ta Kimlik Doğrulama Süreçleri / Authentication Processes in Spring Boot

1*Fi3d-6buKECWDGMVn3ynNQ.jpeg

Genel olarak, kimlik doğrulama yaşam döngüsü yukarıda belirtilen adımları içermektedir. Ancak, kullanılan bileşenler , uyguladığınız senaryoya bağlı olarak değişebilecektir.

1-HttpRequest

Kimlik doğrulama süreci, kullanıcının uygulamada korunan bir kaynağa erişmek için bir HTTP isteği (HttpRequest) göndermesiyle başlar. Bu genellikle bir giriş formu (kullanıcı adı ve parola gönderimi) ya da bir API isteği aracılığıyla gerçekleşir.

2-Authentication Filter (Kimlik Doğrulama Filtresi)

İlk olarak kullanıcının isteği bir Authentication Filter (Kimlik Doğrulama Filtresi) üzerinden geçer.

örneğin: UsernamePasswordAuthenticationFilter, kullanıcının girdiği kullanıcı adı ve parolayı yakalar.

Token tabanlı bir sistemde ise bu filtre, bir JWT (JSON Web Token) token veya başka bir token türünü doğrular.

Ardından, kullanıcının giriş bilgilerini içeren bir UsernamePasswordAuthenticationToken nesnesi oluşturur.

3-Authentication Token (Kimlik Doğrulama token'ı)

Bu aşamada, filtre tarafından oluşturulan kimlik doğrulama token'ı (örneğin,UsernamePasswordAuthenticationToken ) Spring Security’nin Authentication Manager bileşenine iletilir. Bu token, kullanıcı adı ve parola gibi kimlik bilgilerini içerir.

4-Authentication Manager (Kimlik Doğrulama Yöneticisi)

Merkezi yönlendirici olarak görev yapan bu bileşen, kimlik doğrulama token'ını doğrulamak için uygun Authentication Provider’ı seçer.
Örneğin, bir uygulama hem DaoAuthenticationProvider hem de LdapAuthenticationProvider kullanıyorsa, Authentication Manager, token için hangi sağlayıcının kullanılacağını belirler.

5 - Authentication Provider (Kimlik Doğrulama Sağlayıcısı)

Kimlik doğrulama süreci bu aşamada gerçekleştirilir: Seçilen Authentication Provider, gerçek kimlik doğrulama işlemini yapar.
Birden fazla sağlayıcı yapılandırılabilir.

Örneğin:

DaoAuthenticationProvider: Kullanıcı kimlik bilgilerini bir veritabanı (örneğin JDBC veya JPA kullanarak) üzerinden doğrular.

LdapAuthenticationProvider: Kullanıcıları bir LDAP (Lightweight Directory Access Protocol) sunucusu aracılığıyla kimlik doğrulamadan geçirir.

CasAuthenticationProvider: Kimlik doğrulamayı CAS (Central Authentication Server – Merkezi Kimlik Doğrulama Sunucusu) protokolüyle entegre eder.

6-UserDetailsService (Kullanıcı Detay Servisi)

Eğer, DaoAuthenticationProvider kullanılıyorsa, kullanıcı bilgilerini almak için UserDetailsService çağrılır:

MemoryUserDetailsService: Kullanıcıların bellekte (memory) saklandığı durumlarda kullanılır.

CustomUserDetailsService: Kullanıcı bilgilerini bir veritabanı veya başka bir veri kaynağından çeker.

Kullanıcı bilgileri başarıyla alındıktan sonra bir UserDetails nesnesi döndürülür.

7-UserDetails (Kullanıcı Detayları)

UserDetails nesnesi aşağıdaki bilgileri içerir:

Kullanıcı adı (username)

Parola (password)

Kullanıcıya atanmış roller (örneğin: ROLE_USER, ROLE_ADMIN)

8 - Authentication Provider (devamı)
Kullanıcı bilgileri başarıyla doğrulandıktan sonra, Authentication Provider, kimlik doğrulama sonucunu Authentication Manager’a geri döndürür.

9 - Provider Manager
Elde edilen sonuç Provider Manager tarafından değerlendirilir:

Eğer kimlik doğrulama başarılıysa, bir Authentication nesnesi oluşturulur.

Eğer kimlik doğrulama başarısız olursa, BadCredentialsException gibi bir istisna fırlatılır.

Uygulamanın tüm yaşam döngüsü boyunca, kullanıcı kimlik bilgileri SecurityContext içerisinde saklanır.
Bu bağlam genellikle SecurityContextHolder üzerinden erişilerek yönetilir.

Genel İş Akışı:

  1. Kullanıcı bir istek (request) gönderir.

  2. Authentication Filter, bir kimlik doğrulama token'ı oluşturur ve bunu Authentication Manager’a iletir.

  3. Authentication Manager, uygun Authentication Provider’ı seçer.

  4. Authentication Provider, UserDetailsService aracılığıyla kullanıcıyı doğrular.

  5. Kimlik doğrulama başarılı olduktan sonra, kullanıcı bilgileri Security Context içinde saklanır.

Bu akış, genellikle Spring Boot uygulamalarında spring-boot-starter-security bağımlılığı dahil edilerek uygulanır.
Özelleştirme gerektiğinde, UserDetailsService, AuthenticationProvider veya AuthenticationFilter gibi bileşenler (yeniden tanımlanarak) özelleştirilebilir.

Spring Security’de Kimlik Doğrulama Mekanizmaları

Spring Security, zengin ve kapsamlı bir kimlik doğrulama mekanizması seti sunar.
2023 yılı itibarıyla, Spring Security tarafından desteklenen başlıca kimlik doğrulama mekanizmaları şunlardır:

Genel Kimlik Doğrulama Mekanizmaları:

  • Username and Password (Kullanıcı Adı ve Parola)

  • OAuth 2.0 Login

  • SAML 2.0 Login

  • Central Authentication Server (CAS) (Merkezi Kimlik Doğrulama Sunucusu)

  • Remember Me (Beni Hatırla )

  • JAAS (Java Authentication and Authorization Service) Authentication

  • OpenID

  • Pre-Authentication Scenarios (Ön Kimlik Doğrulama Senaryoları)

  • X509 Authentication (X.509 Sertifika Tabanlı Kimlik Doğrulama)

Username and Password-Based Authentication Methods (Kullanıcı Adı ve Parola Tabanlı Kimlik Doğrulama Yöntemleri)

Form Login (Form Girişi):
Bu yöntem, kullanıcıların kullanıcı adı ve parola bilgilerini girebileceği özel bir arayüz sunar. Kimlik doğrulama işlemi, bu kullanıcı dostu giriş formu aracılığıyla gerçekleştirilir.

Basic Authentication (Temel Kimlik Doğrulama):
HTTP Basic Authentication olarak da bilinen bu yöntemde, kullanıcı adı ve parola Base64 formatında kodlanarak HTTP isteğiyle birlikte gönderilir. Basitliği nedeniyle genellikle API entegrasyonlarında kullanılır.

Digest Authentication (Özet Kimlik Doğrulama):
Bu yöntemde, HTTP Digest Authentication, kullanıcı adı, parola ve sunucu tarafından sağlanan ek bilgileri kullanarak bir hash (özet) oluşturur. Kimlik doğrulama bu hash üzerinden yapılır ve bu sayede Basic Authentication’a göre daha güvenlidir.

JDBC Authentication (Veritabanı Tabanlı Kimlik Doğrulama):
Kullanıcı kimlik bilgilerinin veritabanında saklandığı senaryolar için uygundur. Kimlik doğrulama sırasında sistem, veritabanıyla etkileşime girerek kullanıcının bilgilerini doğrular.

LDAP Authentication (LDAP Tabanlı Kimlik Doğrulama):
Lightweight Directory Access Protocol (LDAP) temeline dayanan bu yöntem, kurumlarda kullanıcı ve yetkilendirme bilgilerinin merkezi olarak saklandığı LDAP sunucuları ile entegre çalışır. Kimlik doğrulama doğrudan LDAP sunucusu üzerinden gerçekleştirilir.

Makalemize , Kullanıcı adı ve parola tabanlı , kimlik doğrulamaya dayalı bir Spring Boot uygulama örneği ile devam edeceğiz.

Uygulamamızda, JWT tabanlı kimlik doğrulama ve yetkilendirme mekanizmaları kullanılacaktır. Bu uygulamayla, bir satış tablosunun yönetimsel işlemlerini temsilen oluşturulan api'lere , güvenli bir şekilde erişim sağlamak hedeflenmektedir.

Sisteme kayıt olan ve giriş yapan kullanıcılar, JWT token aracılığıyla ürün tablolarına istek gönderebilir.
Bu sayede, ürün ve satış listelerine erişerek ilgili bilgilere ulaşabilirler. Eğer kimlik bilgileri hatalıysa, kullanıcıya bilgilendirici hata mesajları gösterilir. Ayrıca, token süresi dolmuşsa ilgili tablolara erişim engellenir ve böylece sistemin güvenliği sağlanır.

Mutfaktan bir ses : Projemiz, Spring Boot framework'u üzerinde geliştirilmiştir. API dokümantasyonu, Swagger kullanılarak oluşturulmuştur; isteklerin testleri ise Postman aracılığıyla gerçekleştirilmiştir.
Veritabanı olarak MySQL kullanılmış olup, veritabanı modülleri DBeaver ile görselleştirilmiştir.

Uygulama İş Akışı:
Kullanıcı Kaydı → Giriş → JWT ile İşlemler

Kullanıcı Kaydı (User Registration)

Sisteme kullanıcı eklemek için bir API endpoint’i (/register) oluşturulur.

Kullanıcıdan kullanıcı adı ve parola gibi bilgiler alınır.

Parola, BCryptPasswordEncoder kullanılarak şifrelenir ve güvenli bir şekilde veritabanında saklanır

Kayıtlı Kullanıcıyla Giriş (Login with a Registered User)

Kullanıcı, giriş esnasında kullanıcı adı ve parolasını gönderir.

Giriş başarılı olursa, kullanıcıya bir JWT token verilir.

Bu token sayesinde kullanıcı, korunan endpoint’lere erişim sağlayabilir.

JWT ile İşlemler (Operations with JWT)

Kullanıcı, yaptığı isteklerde Authorization başlığına JWT token’ını ekler.

Token doğrulandıktan ve kullanıcının kimliği onaylandıktan sonra, endpoint’e erişim izni verilir.

Uygulamamızı katmanlı mimari (layered architecture) temelinde tanımlamak, yapısal bütünlüğü ve anlamsal açıklığı güçlendirerek sistemi daha anlaşılır hale getirecektir.

1-Entities (User , Product , Sale)

2-Repository(UserRepository,ProductRepository,SaleRepository)

3-Service(ProductService,UserService)

4-Controller(AuthController, ProductController)

5-Configuration (SecurityConfig,JwtAuthenticationFilter)

6-Domain(LoginRequest,RegisterRequest)

7-Util(JwtTokenUtil)

Uygulamamızın Paket Hiyerarşisi (Package Hierarchy) Aşağıdaki Gibidir:

1*K42wBjHDII0SK8lJn6MaUQ.png

Projemizde Kullanılan Bağımlılıklar (Dependencies) Şu Şekildedir:

<dependencies>
<!-- Spring Boot Starter Dependencies -->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Devtools for Development -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<!-- Testing Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Database Dependency -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<!-- JWT Dependencies -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version> <!-- or latest -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version> <!-- or latest -->
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version> <!-- or latest -->
<scope>runtime</scope>
</dependency>

<!-- Lombok for Boilerplate Code -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>

<!-- Swagger Dependencies-->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies>

Bağımlılıklarımızın liste hali aşağıdaki gibidir;

spring-boot-starter-data-jpa

spring-boot-starter-security

spring-boot-starter-web

spring-boot-devtools

spring-boot-starter-test

spring-security-test

mysql-connector-j

jjwt-api

jjwt-impl

jjwt-jackson

lombok

springdoc-openapi-starter-webmvc-ui

Uygulamamızın properties dosyasında, veritabanı bağlantı tanımlamaları, JPA yapılandırmaları ve Swagger ayarları yer almaktadır.

spring.application.name=com.timeissecuritytime

spring.datasource.url=jdbc:mysql://localhost:3307/springsecurityexamples?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=test1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.api-docs.path=/v3/api-docs

Yukarıda paylaştığım , Katmanlı mimari modeline göre ilerleyecek olursak, öncelikle entity (varlık) sınıflarını ve bunlara karşılık gelen SQL kodlarını paylaşacağım.

User (Entity)

package com.timeissecuritytime.entity;
import lombok.*;
import jakarta.persistence.*;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
}

Entity'mize karşılık db'imizde kullanacağımız sql kodu ise aşağıdaki gibidir;

CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf16;

User tablosu, sisteme kayıtlı kullanıcıların kullanıcı adı ve parola bilgilerini saklamak üzere tasarlanmıştır.
Bu tablo, uygulama içinde kullanılan DTO (Data Transfer Object) ve entity yapılarıyla doğrudan ilişkilidir.

Veritabanı tablosu ile DTO’lar arasındaki bu ilişki, uygulama katmanları arasında kesintisiz veri akışı sağlamaktadır; böylece veritabanı kayıtlarının, servis ve controller katmanlarında kullanılan nesnelere etkin bir şekilde eşlenmesi (mapping) mümkün olur.

Product (Entity)

package com.timeissecuritytime.entity;

import lombok.Data;
import jakarta.persistence.*;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;

@Entity
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private String productDescription;
private Double price;
private Integer stockQuantity;
}

Product Entity'mize karşılık db'imizde kullanacağımız sql kodu ise aşağıdaki gibidir;

CREATE TABLE `product` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_name` VARCHAR(255) NOT NULL,
`product_description` TEXT,
`price` DOUBLE NOT NULL,
`stock_quantity` INT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;


Sale (Entity)

package com.timeissecuritytime.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Data;

@Entity
@Data
public class Sale {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
private Integer saleQuantity;
private java.util.Date saleDate;
}

Sale Entity'mize karşılık db'imizde kullanacağımız sql kodu ise aşağıdaki gibidir;

CREATE TABLE `sale` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_id` BIGINT NOT NULL,
`sale_quantity` INT NOT NULL,
`sale_date` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `product_id` (`product_id`),
CONSTRAINT `sales_ibfk_1` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

Product ve Sale entity’leri ve db objeleri makalemizin ana konusu değildir. Yalnızca giriş yapmış kullanıcıların erişebildiği bazı endpoint’leri gösterebilmek amacıyla bu entity’ler oluşturulmuştur.

Bu örnekle, JWT kullanımını pratik bir şekilde göstermek ve oluşturulan servisler aracılığıyla erişim kontrol mekanizmalarını vurgulamak hedeflendi.

Bu doğrultuda , uygulamamız için ProductRepository, UserRepository ve SaleRepository oluşturulmuştur.

package com.timeissecuritytime.repository;


import com.timeissecuritytime.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}
package com.timeissecuritytime.repository;
import com.timeissecuritytime.entity.Sale;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface SaleRepository extends JpaRepository<Sale, Long> {
List<Sale> findByProductId(Long productId);
}
package com.timeissecuritytime.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.timeissecuritytime.entity.User;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}

Product ve Sale tabloları için örnek veri kümelerini (data instance) aşağıdaki SQL scriptleri çalıştırarak oluşturabilirsiniz:

INSERT INTO `product` (`product_name`, `product_description`, `price`, `stock_quantity`)
VALUES
('Laptop', 'High-performance gaming laptop', 1500.00, 10),
('Smartphone', 'Latest model smartphone with advanced features', 999.99, 20);

INSERT INTO `sale` (`product_id`, `sale_quantity`, `sale_date`)
VALUES
(1, 2, '2025-01-28 12:00:00'),
(2, 1, '2025-01-28 13:30:00');

Şimdi, projemizin service (servis) dosyalarının tanımlanmasına geçiyoruz.

UserService

package com.timeissecuritytime.service;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.timeissecuritytime.entity.User;
import com.timeissecuritytime.repository.UserRepository;
import com.timeissecuritytime.Constants;

@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {

private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

public User registerUser(String username, String password) {
User user = new User();
user.setUsername(username);
user.setPassword(bCryptPasswordEncoder.encode(password));
return userRepository.save(user);
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(Constants.USER_NOT_FOUND + username));

return org.springframework.security.core.userdetails.User
.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles("USER")
.build();
}

public User authenticateUser(String username, String password) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(Constants.USER_NOT_FOUND + username));

if (!bCryptPasswordEncoder.matches(password, user.getPassword())) {
throw new IllegalArgumentException(Constants.INVALID_PASSWORD);
}
return user;
}
}

UserService Sınıfındaki Temel Metotlarımız ve Teknik İşlevleri aşağıdaki gibidir;

1-registerUser(String username, String password)

Bu metot, sisteme yeni bir kullanıcı kaydetmek için kullanılır.

Teknik akış:

Gelen kullanıcı adı ve parola parametrelerini alır.

Bir User nesnesi oluşturulur ve parametrelerden gelen veriler nesneye atanır.

Parola, BCryptPasswordEncoder kullanılarak şifrelenir (hashlenir) ve User nesnesine set edilir.

userRepository.save(user) metodu aracılığıyla kullanıcı nesnesi veritabanına kaydedilir.

Özetle ,kullanıcının parolasının güvenli şekilde saklanmasını sağlamak ve sisteme yeni kullanıcı eklemek amacıyla oluşturulmuştur.

2-loadUserByUsername(String username)

(Spring Security’nin UserDetailsService Uygulaması / Implementation of Spring Security’s UserDetailsService)

Kullanıcı bilgilerini toplamak ve Spring Security’nin kimlik doğrulama mekanizmalarıyla uyumlu bir UserDetails nesnesi döndürmek amacıyla geliştirilmiştir.Spring Security’nin kimlik doğrulama süreci sırasında otomatik olarak çağrılır.

Teknik akış:

Kullanıcı, userRepository.findByUsername(username) metodu kullanılarak veritabanında aranır.

Eğer kullanıcı bulunamazsa, UsernameNotFoundException fırlatılır.

Kullanıcı bulunursa, Spring Security’nin User sınıfı kullanılarak bir UserDetails nesnesi oluşturulur.
Bu nesne, kullanıcı adı, parola ve varsayılan bir rol (USER) bilgilerini içerir.

3-authenticateUser(String username, String password)

Kullanıcının giriş bilgilerini(login credentials) doğrulamak için geliştirilmiştir. Kullanıcı bilgilerinin doğruluğunu manuel olarak kontrol etmek için çağrılır.

Teknik akış:

Kullanıcı, userRepository.findByUsername(username) metodu kullanılarak veritabanında aranır.
Eğer kullanıcı bulunamazsa, UsernameNotFoundException fırlatılır.

Girilen parola, veritabanında saklanan hashlenmiş parola ile BCryptPasswordEncoder.matches metodu kullanılarak karşılaştırılır.

Parolalar eşleşmiyorsa, IllegalArgumentException fırlatılır.

Doğrulama başarılı olursa, ilgili User nesnesi döndürülür.

registerUser: Kullanıcıyı kaydetmemize yardımcı olan methoddur. Kullanıcı parolasını hashleyerek , güvenli bir şekilde saklamamıza olanak tanır.
loadUserByUsername: Spring Security’nin kimlik doğrulama süreci için bir UserDetails nesnesi bizlere sağlar.
authenticateUser: Kullanıcı kimlik bilgilerini manuel olarak doğrular; özel kimlik doğrulama mantığının gerektiği durumlarda genellikle tercih edilir.

Bu metotlar, uygulamamızda kullanıcı kimlik doğrulama ve yetkilendirme altyapısı oluşturmak amacıyla tasarlanmıştır.

ProductService

package com.timeissecuritytime.service;

import com.timeissecuritytime.entity.Product;
import com.timeissecuritytime.entity.Sale;
import com.timeissecuritytime.repository.ProductRepository;
import com.timeissecuritytime.repository.SaleRepository;
import com.timeissecuritytime.Constants;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
@RequiredArgsConstructor
public class ProductService {

private final ProductRepository productRepository;
private final SaleRepository saleRepository;

@PreAuthorize("hasRole('USER')")
@Transactional(readOnly = true)
public List<Product> findAllProducts() {
return productRepository.findAll();
}

@Transactional(readOnly = true)
public Product findProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new RuntimeException(Constants.PRODUCT_NOT_FOUND + id));
}

@Transactional
@PreAuthorize("hasRole('ADMIN')")
public Product saveProduct(Product product) {
return productRepository.save(product);
}

@Transactional(readOnly = true)
@PreAuthorize("hasRole('USER')")
public List<Sale> findSalesByProductId(Long productId) {
return saleRepository.findByProductId(productId);
}
}

Bu servis dosyamızda ürün ve satış verileri üzerinde işlem yapmak için kullanacağımız temel servis metotları bulunmaktadır. Bu api'ler , makalemizin ana odak noktasını oluşturmamaktadır.

Teknik olarak methodlarımız:

findAllProducts(): Tüm ürünleri listeler ve yalnızca USER rolüne sahip kullanıcılar tarafından erişilebilir.

findProductById(Long id): Verilen ID değerine göre bir ürünü bulur. Ürün bulunamazsa hata fırlatır.

saveProduct(Product product): Yeni bir ürünü kaydeder ve yalnızca ADMIN rolüne sahip kullanıcılar tarafından erişilebilir.

findSalesByProductId(Long productId): Belirli bir ürüne ait satışları listeler ve yalnızca USER rolündeki kullanıcıların erişimine açıktır.

Şimdi, projemizin Controller dosyalarının tanımlanmasına geçiyoruz.

AuthController

package com.timeissecuritytime.controller;

import lombok.RequiredArgsConstructor;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import com.timeissecuritytime.util.JwtTokenUtil;
import com.timeissecuritytime.domain.LoginRequest;
import com.timeissecuritytime.domain.RegisterRequest;
import com.timeissecuritytime.entity.User;
import com.timeissecuritytime.service.UserService;
import com.timeissecuritytime.Constants;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final JwtTokenUtil jwtTokenUtil;

@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
userService.registerUser(registerRequest.getUsername(), registerRequest.getPassword());
return ResponseEntity.ok(Constants.REGISTRATION_SUCCESS);
}

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
User user = userService.authenticateUser(loginRequest.getUsername(), loginRequest.getPassword());
String token = jwtTokenUtil.generateToken(user.getUsername());
return ResponseEntity.ok(String.format(Constants.TOKEN_RESPONSE, token));
}
}

AuthController sınıfı, kullanıcının kayıt ve giriş işlemleri sırasında kullanılmak üzere tasarlanmıştır.

/register (POST): Kullanıcıdan gelen kayıt isteğini alır ve UserService aracılığıyla yeni bir kullanıcı oluşturur. İşlem başarıyla tamamlandığında bir başarı mesajı döndürür.

/login (POST): Kullanıcının giriş kimlik bilgilerini doğrular. Kimlik doğrulama başarılı olursa, JwtTokenUtil kullanılarak bir JWT token üretilir ve kullanıcıya iletilir.

Bu controller, uygulamanın kimlik doğrulama süreçlerini yönetmek için oluşturulmuş temel bir controller'dır.

ProductController

package com.timeissecuritytime.controller;

import com.timeissecuritytime.entity.Product;
import com.timeissecuritytime.entity.Sale;
import com.timeissecuritytime.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {

private final ProductService productService;

@GetMapping("/all")
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.findAllProducts());
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return ResponseEntity.ok(productService.findProductById(id));
}

@PostMapping("/add")
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
return ResponseEntity.ok(productService.saveProduct(product));
}

@GetMapping("/sales/{productId}")
public ResponseEntity<List<Sale>> getSalesByProductId(@PathVariable Long productId) {
return ResponseEntity.ok(productService.findSalesByProductId(productId));
}
}

ProductController sınıfı, ürün ve satış işlemlerini yönetmek için oluşturulmuştur.Ürün ve satış verilerine erişim sağlamak üzere tasarlanmıştır.

Teknik olarak methodlarımız:

/all (GET): Tüm ürünleri listeler.

/{id} (GET): Belirtilen ID değerine sahip ürünü döndürür.

/add (POST): Yeni bir ürünü ekler ve kaydeder.

/sales/{productId} (GET): Belirtilen productId değerine ait satışları listeler.

Şimdi, projemizin konfigürasyon (configuration) dosyalarının tanımlanmasına geçiyoruz.

JwtAuthenticationFilter

package com.timeissecuritytime.configuration;

import java.io.IOException;
import java.util.Optional;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.timeissecuritytime.service.UserService;
import com.timeissecuritytime.util.JwtTokenUtil;
import com.timeissecuritytime.Constants;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenUtil jwtTokenUtil;
private final UserService userService;

public JwtAuthenticationFilter(JwtTokenUtil jwtTokenUtil, UserService userService) {
this.jwtTokenUtil = jwtTokenUtil;
this.userService = userService;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
Optional<String> tokenOptional = extractToken(request);
if (tokenOptional.isPresent()) {
String token = tokenOptional.get();
if (jwtTokenUtil.validateToken(token)) {
setAuthentication(token);
} else {
handleInvalidToken(response);
return;
}
}
} catch (Exception e) {
handleFilterException(response, e);
return;
}
filterChain.doFilter(request, response);
}

private Optional<String> extractToken(HttpServletRequest request) {
return Optional.ofNullable(request.getHeader(Constants.AUTHORIZATION_HEADER))
.filter(header -> header.startsWith(Constants.BEARER_PREFIX))
.map(header -> header.substring(Constants.BEARER_PREFIX.length()));
}

private void setAuthentication(String token) {
String username = jwtTokenUtil.extractUsername(token);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private void handleInvalidToken(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(Constants.INVALID_TOKEN_MESSAGE);
}

private void handleFilterException(HttpServletResponse response, Exception e) throws IOException {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.getWriter().write(Constants.UNEXPECTED_ERROR_MESSAGE);
}
}

JwtAuthenticationFilter sınıfı, Spring Security içerisinde tanımlanan özel bir filtredir ve JWT (JSON Web Token)’ların doğrulanması ve kimlik doğrulama işlemlerinden sorumludur.

Teknik olarak methodlarımız:

doFilterInternal Metodu:

Gelen isteklerdeki Authorization başlığından JWT token’ı çıkarır.

Token geçerliyse, JwtTokenUtil kullanarak kullanıcı adını çözümler ve kimlik doğrulama işlemini gerçekleştirir.

Token geçersizse veya bir hata oluşursa, uygun HTTP durum kodu (status code) ve hata mesajı ile yanıt döner.

extractToken Metodu:

Authorization başlığındaki Bearer önekini kaldırarak token’ı çıkarır.

setAuthentication Metodu:

Token’dan elde edilen kullanıcı adını kullanarak UserDetails bilgilerini yükler.

Kullanıcının kimlik doğrulama bilgilerini SecurityContext içine kaydeder ve böylece kullanıcının, isteğin tüm yaşam döngüsü boyunca doğrulanmış (authenticated) kalmasını sağlar.

handleInvalidToken ve handleFilterException Metotları:

Geçersiz token veya beklenmeyen hata durumlarında, uygun hata mesajı ve HTTP durum kodu ile yanıt dönmesine yardımcı olur.

Bu sınıf, her HTTP isteği başına bir kez çalışır ve JWT doğrulama sürecini merkezi olarak yönetmektedir.

JwtTokenUtil


package com.timeissecuritytime.configuration;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;

import java.util.Date;
import java.util.Optional;

import javax.crypto.SecretKey;

import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class JwtTokenUtil {

private static final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final long EXPIRATION_TIME = 86400000; // One-day

public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(calculateExpirationDate())
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.compact();
}

public boolean validateToken(String token) {
return parseClaims(token)
.map(this::isTokenValid)
.orElse(false);
}

public String extractUsername(String token) {
return parseClaims(token)
.map(Claims::getSubject)
.orElse(null);
}

private Date calculateExpirationDate() {
return new Date(System.currentTimeMillis() + EXPIRATION_TIME);
}

private Optional<Claims> parseClaims(String token) {
try {
return Optional.of(Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody());
} catch (JwtException e) {
return Optional.empty();
}
}

private boolean isTokenValid(Claims claims) {
Date expirationDate = claims.getExpiration();
return expirationDate != null && !expirationDate.before(new Date());
}
}

JwtTokenUtil sınıfı, JWT işlemlerini (oluşturma, doğrulama ve çözümleme) yönetmek için tasarlanmış bir yardımcı (utility) sınıftır.
Bu sınıfta yer alan metotların teknik işlevlerine dair ayrıntılı açıklamalarımız;

  • generateToken(String username) Metodu:
    setSubject(username): Kullanıcı adını token’ın subject (sub) alanına ekler.
    setIssuedAt(new Date()): Token’ın oluşturulma tarihini belirler.
    setExpiration(calculateExpirationDate()): Token’ın bitiş (expiration) tarihini hesaplayıp ekler.
    signWith(SECRET_KEY, SignatureAlgorithm.HS256): Token’ı HS256 algoritması ve SECRET_KEY ile imzalar.
    compact(): Oluşturulan JWT’yi string olarak döndürür.
    Kullanıcı adı ve geçerlilik süresi bilgilerini içeren imzalı bir JWT oluşturur.

  • validateToken(String token) Metodu:
    parseClaims(token): Token içindeki Claims nesnesini çözümler.
    isTokenValid(claims): Token’ın süresinin geçerli olup olmadığını kontrol eder.
    Token geçersizse Optional.empty() döner.
    Token’ı doğrular ve geçerliyse true, geçersizse false döndürür.

  • extractUsername(String token) Metodu:
    parseClaims(token): Token’daki Claims nesnesini çözümler.
    Claims::getSubject: Token’daki sub (kullanıcı adı) bilgisini çıkarır.

  • ExpirationDate Metodu:
    System.currentTimeMillis() + EXPIRATION_TIME: Mevcut zamana bir gün ekleyerek token’ın bitiş tarihini hesaplar.
    Expiration tarihini Date nesnesi olarak döndürür.

  • parseClaims(String token) Metodu:
    Jwts.parserBuilder().setSigningKey(SECRET_KEY).build().parseClaimsJws(token): Token’ı çözümleyerek içindeki Claims nesnesini çıkarır.
    Token geçersizse JwtException fırlatır ve Optional.empty() döner.
    Token içindeki tüm Claims verilerini döndürür.

  • isTokenValid(Claims claims) Metodu:
    claims.getExpiration(): Token’ın bitiş tarihini alır.
    !expirationDate.before(new Date()): Token süresinin dolup dolmadığını kontrol eder.
    Token geçerliyse true, süresi dolmuşsa false döndürür.

SecurityConfig

package com.timeissecuritytime.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter)
throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/**",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/v3/api-docs/swagger-config"
).permitAll()
.anyRequest().authenticated()
)

.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

SecurityConfig sınıfı, uygulama için Spring Security yapılandırmasını gerçekleştirir.
SecurityConfig içerisinde, güvenlik kuralları , kimlik doğrulama mekanizmaları ve kriptolama ayarları belirlenir.
Her bir bileşenin teknik açıklamasına ulaşabilirsiniz.

1. filterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) Metodu

Bu metot, uygulamanın temel güvenlik yapılandırmasını tanımlar; hangi endpoint’lerin korunduğunu, hangilerinin herkese açık olduğunu ve güvenlik filtrelerinin nasıl uygulandığını belirler.

csrf(csrf -> csrf.disable()) -> CSRF (Cross-Site Request Forgery) korumasını devre dışı bırakır.

authorizeHttpRequests(auth -> auth…) -> HTTP endpoint’leri için erişim kurallarını yapılandırmak için kullanılır.

requestMatchers(…): Herkese açık, kimlik doğrulaması gerektirmeyen endpoint’leri belirtmek için kullanılır.

/api/auth/ → Kimlik doğrulama süreçlerinde, kullanılan end point'lere erişim için herkese açık hale getirilmiştir.( login, register).
/v3/api-docs/, /swagger-ui/ → Swagger API dokümantasyonu'da benzer şekilde açık bırakılmıştır.

anyRequest().authenticated(): İzin verilen endpoint'ler haricinde kalan, diğer api'ler için authenticated şartını koymak için kullanılmıştır.

addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
Uygulamaya özel olarak oluşturulan JwtAuthenticationFilter bileşenini, Spring Security’nin filtre zincirine dahil etmek için kullanılır.

Ek olarak bu filtre, uygulamaya gelen her HTTP isteğini yakalayarak Authorization başlığındaki JWT token’ı doğrular.
Token geçerliyse, kullanıcının kimlik bilgilerini SecurityContext içerisine yerleştirir ve isteğin doğrulanmış bir kullanıcı adına devam etmesini sağlar.
Bu işlem, Spring Security’nin varsayılan UsernamePasswordAuthenticationFilter’ından önce gerçekleşir, böylece JWT tabanlı kimlik doğrulama mekanizması öncelikli hâle gelir.

sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

Oturum (session) yönetimini yapılandırmak için kullanılır.Oturum oluşturma politikasını STATELESS olarak ayarlar; bu sayede sunucu üzerinde herhangi bir oturum oluşturulmaz veya tutulmaz.
Bu yaklaşım, JWT tabanlı kimlik doğrulama sistemleri gibi durumsuz (stateless) güvenlik mimarilerinde gerekmektedir. Methodumuz , Spring Security’nin gelen istekleri nasıl işleyeceğini tanımlayan, tam olarak yapılandırılmış bir SecurityFilterChain nesnesi oluşturarak döndürür.

2. passwordEncoder() Metodu

Methodumuz, Parolaların hashlenmesi ve doğrulanması için kullanılacak şifreleyiciyi tanımlar.

Parolaların güvenli biçimde saklanması için güçlü bir BCryptPasswordEncoder örneği döndürür.

Düz metin (plaintext) parolalar hiçbir zaman saklanmaz; bunun yerine hashlenmiş olarak kaydedilir.

SecurityConfig Sınıfındaki Temel Kavramların Özeti

  • Stateless Security: Uygulama, kimlik doğrulama için JWT token’ları kullanır; bu nedenle sunucu tarafında herhangi bir oturum durumu (session state) tutulmaz. Bu durum, SessionCreationPolicy.STATELESS konfigurasyonuyla sağlanır.

  • Filter Chain: JwtAuthenticationFilter gibi özel filtreler, Spring Security filtre zincirine entegre edilerek JWT doğrulama işlemlerini yürütür.

  • Endpoint Security: /api/auth/** ve /swagger-ui/** gibi genel endpoint’ler açıkça erişime izinlidir. Bunların dışındaki tüm endpoint’ler, kimlik doğrulama gerektirir.

  • Password Encryption: Parolalar, BCryptPasswordEncoder kullanılarak şifrelenir ve bu sayede veritabanında güvenli biçimde saklanır.

Güvenlik Akışı (Security Flow):

Bir istek uygulamaya ulaştığında SecurityFilterChain tarafından işlenir.

Eğer istek herkese açık bir endpoint ile eşleşiyorsa, kimlik doğrulama yapılmadan kabul edilir.

Diğer tüm endpoint’lerde, JWT doğrulaması yapılır ve yalnızca geçerli token’a sahip kullanıcıların erişimine izin verilir.

Aşağıda, sabitlerin tanımlandığı dosyalarımız, genel hata yönetimi (global error handling) için kullanılan sınıflarımız ve domain nesneleri yer almaktadır.

Domain objelerimiz;

package com.timeissecuritytime.domain;
import lombok.Data;
@Data
public class LoginRequest {
private String username;
private String password;
}
package com.timeissecuritytime.domain;
import lombok.Data;
@Data
public class RegisterRequest {
private String username;
private String password;
}

GlobalExceptionHandler;

package com.timeissecuritytime.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}

Constants

package com.timeissecuritytime;

public class Constants {
public static final String REGISTRATION_SUCCESS = "User successfully registered!";
public static final String TOKEN_RESPONSE = "Token: %s";
public static final String PRODUCT_NOT_FOUND = "Product not found: ";
public static final String USER_NOT_FOUND = "User not found: ";
public static final String INVALID_PASSWORD = "Invalid password!";
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String BEARER_PREFIX = "Bearer ";
public static final String INVALID_TOKEN_MESSAGE = "{\"error\": \"Token has expired or is invalid. Please log in again.\"}";
public static final String UNEXPECTED_ERROR_MESSAGE = "{\"error\": \"An unexpected error occurred.\"}";
}

Şimdi, uygulamamızı test etmek için Postman kullanacağız.

Öncelikle, register (kayıt) işlemiyle yeni bir kullanıcı oluşturacağız.
Ardından, login (giriş) işlemini kullanarak bir JWT token alacağız.
Sonrasında, bu token’ı kullanmadan ürün listesini görüntülemeyi deneyeceğiz ve alacağımız yetkilendirme hatasını gözlemleyeceğiz.
Son olarak, geçerli bir JWT token sağlayarak servislerimize başarılı şekilde erişip ürünleri listeleyebileceğimizi göstereceğiz.

1 - Kullanıcının Kaydı

1*QNYYhri5aYRzcItoIE5__Q.png

2 - Giriş işlemi ve token'ın alınması

1*xuVrPyvMPGMvXe63wtDGDg.png

3 - Token Olmadan Ürünleri Listelemeyi Deneme ve Hata Mesajını Gözlemleme

1*lKpwarkFcfOv5JUh-t_ABQ.png

4 - Geçerli Bir Token ile Servislere Erişim Sağlama

1*f8KnS6r9K3N5m_M2lG8iLQ.png

Token servisimize geçirildikten sonra ürün listesini alabiliyoruz

1*Ezp4U9NfUJ4IJ_2eLmP_-Q.png

Bu makalede, Spring Security ile JWT tabanlı kimlik doğrulama ve yetkilendirme süreçlerini ayrıntılı bir şekilde inceledik. Ayrıca, uygulamamızı katmanlı mimari yapısına göre kurgulayarak güvenlik odaklı bir altyapı oluşturmanın önemini vurguladık.

Uygulamanın her bileşeni arasındaki etkileşimleri açıklarken, bu süreçleri daha anlaşılır kılmak için örnek kodlar kullandık. Bu yapı, yetkisiz erişimleri önleyerek güvenli ve sürdürülebilir bir sistem geliştirmenize yardımcı olacaktır.

----

Makalemizi okuduğunuz için teşekkür eder, faydalı bulduysanız paylaşmanızı rica ederim.

ierdoganierdogan11 Aralık 2025
WebSocket’in Temelleri ve Java ile Dinamik Çoklu Sohbet UygulamasıPratik Java Uygulamaları

WebSocket’in Temelleri ve Java ile Dinamik Çoklu Sohbet Uygulaması

WebSocket, TCP protokolü üzerinden çift yönlü (full-duplex) iletişim kanalları sağlayan esnek ve yüksek verimli bir iletişim protokoldür.Web sunucularında ve tarayıcılarda kullanılmak üzere tasarlanmıştır. 2011 yılında RFC 6455 olarak standart hale getirilmiştir.WebSocket, istemci ile sunucu arasında kalıcı (persistent) bir bağlantı kurulmasını sağlar.Çift yönlü iletişim modeli sayesinde, istemci ve sunucu birbirine istediği anda ve istediği ölçüde veri gönderebilir. Bu özellik, WebSocket’i klasik HTTP protokolün ayırmaktadır.

WebSocket Kullanmanın Avantajları nelerdir?

WebSocket protokolünün başlıca avantajlarını aşağıdaki gibi özetleyebiliriz :

Gerçek Zamanlı İletişim
WebSocket, sunucu ile istemci arasında gerçek zamanlı (real-time) iletişim sağlar. Bu sayede daha hızlı ve tepkisel uygulamalar geliştirmeye olanak tanır.

Düşük Gecikme (Low Latency)
HTTP’ye kıyasla çok daha az başlık bilgisi taşır bu durum gecikme süresini oldukça düşürür. Veri paketleri çok daha hızlı gönderilip alınır. Bu nedenle hızlı tepki süresi gerektiren uygulamalar (sohbet, canlı skor, borsa takibi vb.) için idealdir.

Daha Az Bant Genişliği Tüketimi
Kalıcı (persistent) bir bağlantı kurduğu için gereksiz veri alışverişi ortadan kalkar. Bağlantı bir kez kurulduktan sonra kapanana kadar devam eder. HTTP’de her istekte yeni bağlantı kurulurken, WebSocket'te kalıcı bağlantı sayesinde, kaynak ve bant genişliği tasarrufu sağlanır.

Çift Yönlü (Bi-Directional) İletişim
Sunucu, istemciden bağımsız olarak istediği anda istemciye veri gönderebilir (server-push). Bu özellik, klasik HTTP’nin yalnızca istek → yanıt mantığıyla çalışan yapısından tamamen farklıdır ve gerçek zamanlı uygulamaların temelini oluşturur.

Esnek ve Verimli Protokol
WebSocket çok geniş bir kullanım alanına sahip esnek bir protokoldür. Web uygulamalarından IoT cihazlarına kadar her ortamda rahatlıkla kullanılabilir. HTTP’ye göre çok daha küçük başlık (header) boyutuna sahip olduğu için daha küçük paketlerin oluşmasına olanak tanır, daha az kaynak tüketir bu durum yüksek verimlilik sağlar.

WebSocket, gerçek zamanlı, düşük gecikmeli ve bant genişliği dostu uygulamalar geliştirmek için günümüzün en önemli protokolüdür.

1*de9VVpUpwXsnEilfuVWFww.png

WebSocket protokolünün temel taşı Frame'lere genel bir bakış;

Frame'ler , istemci ile sunucu arasında düşük gecikmeli, çift yönlü (full-duplex) iletişimin gerçekleşmesini sağlayan en küçük veri birimidir. Her bir WebSocket mesajı, bir veya birden fazla frame’den oluşur. Büyük mesajlar parçalara bölünerek iletilir (fragmentation) ve alıcı tarafında yeniden birleştirilir.

Bir WebSocket frame’inin bileşenleri aşağıdaki gibidir;

1-Temel Header (2 bayt)

FIN biti (1 bit): Mesajın son frame’i olup olmadığını belirtir. 1 = bu mesajın son parçası, 0 = devamı gelecek.

RSV1, RSV2, RSV3 (her biri 1 bit): Sadece uzantılar (extension) tanımlandığında kullanılabilir (örneğin per-message compression extension).

Opcode (4 bit): Frame’in türünü tanımlar.

0x0 = devam frame’i
0x1 = text frame
0x2 = binary frame
0x8 = connection close
0x9 = ping
0xA = pong
diğerleri kontrol frame’leri veya gelecekte kullanılmak üzere ayrılmış bitlerdir.

Mask biti (1 bit): Payload’un maskelenip ,maskelenmediğini belirtmek için kullanılan pinlerdir.

İstemciden → sunucuya gönderilen tüm frame’lerde bu bit zorunlu olarak 1 olmalıdır.

Sunucudan → istemciye gönderilenlerde ise 0'dır.

Payload length (7 bit): uzunluk alanıdır.

0–125 → gerçek uzunluk
126 → sonraki 2 bayt (16-bit unsigned) gerçek uzunluğu verir
127 → sonraki 8 bayt (64-bit unsigned) gerçek uzunluğu verir

2-Extended Payload

Length (opsiyonel) : Payload length alanı 126 ise → 2 bayt daha okunur
127 ise → 8 bayt daha okunur

3-Masking Key (4 bayt)

Mask biti 1 ise bulunur. İstemciden gelen tüm frame’ler bu 4 baytlık rastgele anahtarla maskelenir.

Maskeleme işlemi sırasında kullanılan algoritmik mantık : gerçek_veri[i] = gizlenmiş_veri[i] XOR mask[i mod 4] şeklindedir.

Sunucu bu anahtarı kullanarak veriyi geri çözer (unmask).

4-Payload Data (Yük Verisi) Gerçek mesaj içeriği burada yer alır. Text frame’lerde (opcode=1) payload mutlaka geçerli ve UTF-8 olmalıdır. Binary frame’lerde (opcode=2) herhangi ikili veri olabilir.

Kontrol frame’leri (close, ping, pong) için payload uzunluğu maksimum 125 bayttır.

Kalıcı Bağlantı Kurmak, Veri Aktarım Gecikmesini Önemli Ölçüde Azaltır

WebSocket’in en büyük güçlerinden biri, bağlantıyı bir kez kurduktan sonra açık (persistent) tutmasıdır. Geleneksel HTTP’de her istek (mesaj, bildirim, güncelleme) için şu adımlar tekrarlanır:

  • Yeni TCP bağlantısı kurulur (3-way handshake)

  • HTTP başlıkları gönderilir/alınır (~200–800 byte)

  • TLS/SSL handshake (eğer HTTPS ise)

  • Veri aktarılır

  • Bağlantı kapatılır veya yeniden kullanılır (ama yine de başlık maliyeti vardır)

Bu döngü, özellikle sık veri alışverişi gereken uygulamalarda milisaniyeler hatta saniyeler düzeyinde gecikme yaratır.

WebSocket’te ise:

  • Bağlantı sadece bir kez kurulur (HTTP Upgrade ile başlar)

  • İlk handshake’ten sonra başlık boyutu yalnızca 2–14 byte olur

  • TCP bağlantısı açık kaldığı sürece ek handshake gerekmez

  • Veri,doğrudan gönderilir/alınır

1*KOaIOc_1Bj9-Oq4z9bwEqQ.png

WebSocket’in düşük gecikme ve gerçek zamanlı iletişim yeteneği sayesinde günümüzde neredeyse her sektörde tercih edilmektedir:

Kullanım Alanı

Örnekler

Mesajlaşma Uygulamaları

WhatsApp, Slack, Discord, Telegram gibi sistemlerin canlı sohbet özelliği WebSocket ile çalışır.

Gerçek Zamanlı Oyunlar

Agar.io, online satranç, çok oyunculu battle-royale veya tarayıcı tabanlı MMO oyunlar.

Canlı Güncellemeler

Borsa takip ekranları, kripto para fiyatları, canlı spor skorları, haber akışları.

IoT (Nesnelerin İnterneti)

Akıllı ev cihazları, sensör verilerinin anlık izlenmesi, uzaktan kontrol panelleri.

Finans Uygulamaları

Algo-trading platformları, gerçek zamanlı portföy takibi, emir iletimi, finansal dashboard’ları.

Güvenlik ve İzleme Sistemleri

IP kamera canlı yayınları, hırsız alarmı anlık bildirimleri, erişim kontrol sistemleri.

Blockchain ve Kripto

Cüzdan bakiyesi güncellemeleri, yeni blok/mempool takibi, DEX’lerde fiyat ve işlem akışı.

İşbirliği Araçları

Google Docs, Figma, Miro, Notion gibi aynı anda birden çok kişinin düzenleme yaptığı uygulamalar.

Canlı Yayın Kontrolü

Twitch chat, YouTube canlı yorumlar, OBS kontrol sinyalleri.

Eğitim ve Online Sınav

Canlı yoklama, anlık soru-cevap, interaktif whiteboard sistemleri.

Kullanıcıların birbirine veya sisteme anında ve sürekli veri göndermesi/alması gereken her türlü uygulama WebSocket’in doğal kullanım alanıdır.

Java ile Temel Socket Programlama: Sohbet Uygulaması

Bu bölümde, harici bir kütüphane kullanmadan,java (java.net ve java.io) ile çalışan çok kullanıcılı basit bir sohbet uygulaması oluşturuldu.Bu uygulama, socket programlamanın temel kavramlarını öğrenmek ve pratik yapmak isteyenler için, temel bilgileri içermektedir.

Uygulanın özelliklerini basitçe aşağıdaki gibi özetleyebiliriz,

  • Gerçek zamanlı mesajlaşma

  • Çoklu kullanıcı desteği

  • Kullanıcı giriş/çıkış bildirimleri

  • Düşük gecikme, hafif ve anlaşılır kod yapısı

Uygulama klasik istemci-sunucu (client-server) mimarisine dayalıdır ve iki ana bileşenden oluşur:

  • ChatSocketServer → Sunucu tarafını temsil etmektedir.

  • ChatSocketClient → İstemci tarafını temsil etmektedir.

ChatSocketServer;

ChatSocketServer 'daki temel iş akışı aşağıdaki gibidir;

1. Sunucu Başlatılır ve Port Dinlemeye Alınır

(ServerSocket serverSocket = new ServerSocket(12345))

2. Her İstemci İçin Ayrı Bir İş Parçacığı (Thread) Oluşturulur

(new ClientHandler(clientSocket).start();)

3. Kullanıcı Adı Alınır ve Sisteme Kaydedilir

(Enter your username:)

4. Yeni Katılım ve Çıkış Bildirimleri Otomatik Yapılır

broadcastMessage("Server", username + " has joined the chat.");

broadcastMessage("Server", username + " has left the chat.");

5. Mesajlar Tüm İstemcilere Anında Yayınlanır (Broadcast)

while ((message = in.readLine()) != null) {
broadcastMessage(username, message);
}

1-ChatSocketServer’a ait kod bloğu;

package com.testsocket;

import java.io.*;
import java.net.*;
import java.util.*;

public class ChatSocketServer {

private static final int PORT = 12345;
private static Map<String, PrintWriter> clientWriters = new HashMap<>();

public static void main(String[] args) {
System.out.println("Server is starting...");
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
Socket clientSocket = serverSocket.accept();
new ClientHandler(clientSocket).start();
}
} catch (IOException e) {
e.printStackTrace();
}
}

private static class ClientHandler extends Thread {
private Socket socket;
private String username;
private BufferedReader in;
private PrintWriter out;

public ClientHandler(Socket socket) {
this.socket = socket;
}

public void run() {
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true);

out.println("Enter your username:");
username = in.readLine();
synchronized (clientWriters) {
clientWriters.put(username, out);
}
System.out.println(username + " has connected.");

// Notify other clients that a user has joined
broadcastMessage("Server", username + " has joined the chat.");

// Listen for incoming messages and forward them
String message;
while ((message = in.readLine()) != null) {
System.out.println("[" + username + "]: " + message);
broadcastMessage(username, message);
}
} catch (IOException e) {
e.printStackTrace();
} finally {

if (username != null) {
synchronized (clientWriters) {
clientWriters.remove(username);
}
broadcastMessage("Server", username + " has left the chat.");
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

// Send the message to all clients
private void broadcastMessage(String sender, String message) {
synchronized (clientWriters) {
for (PrintWriter writer : clientWriters.values()) {
writer.println("[" + sender + "]: " + message);
}
}
}
}
}


ChatSocketClient;

ChatSocketClient'daki temel iş akışı aşağıdaki gibidir;

1-Sunucuya Bağlantı Kurulur

2-Giriş/Çıkış Akışları (Stream) Hazırlanır

3-Kullanıcı Adı Sunucu Tarafından Sorulur ve Gönderilir

4-Gelen Mesajları Gerçek Zamanlı Göstermek İçin Ayrı Bir Thread Başlatılır

5-Kullanıcının Yazdıkları Sunucuya Anında Gönderilir (Ana Thread)

6-Bağlantı Koparsa veya Sunucu Kapanırsa Program Temiz Şekilde Sonlanır

2-ChatSocketClient'a ait kod bloğu;

package com.testsocket;

import java.io.*;
import java.net.*;
import java.util.Scanner;

public class ChatSocketClient {

private static final String SERVER_ADDRESS = "127.0.0.1";
private static final int SERVER_PORT = 12345;

public static void main(String[] args) {
try (Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT)) {
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

Scanner scanner = new Scanner(System.in);

// A separate thread to listen for messages from the server
new Thread(() -> {
try {
String message;
while ((message = in.readLine()) != null) {
System.out.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();

// Read user messages and send them to the server
while (true) {
String message = scanner.nextLine();
out.println(message);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

Uygulama akışı aşağıdaki gibidir;

1. Sunucu Bağlantısı

İstemci, belirtilen IP adresi ve port üzerinden sunucuya bağlanarak iletişim kanalını oluşturur.

1*LSLmQMD8XeG8Ow1qFj4Fxg.png

2.İstemci Bağlantısı:

ChatSocketCLient çalıştırılır, sunucuya bağlanır ve kullanıcıdan bir kullanıcı adı girmesi beklenilir.

1*EP0vJtFHH2GlDNgrCBwhMw.png

3.Mesajlaşma:
Kullanıcılar mesaj yazdıkça, bu mesajlar sunucu aracılığıyla tüm bağlı diğer istemcilere iletilir.

1*LTYsP2u1QUMlV089Lss7gg.png

4.Bağlantı Yönetimi:
Bir istemci bağlantıyı kestiğinde, sunucu diğer istemcileri bilgilendirir ve bağlantısı kesilen istemciye ait kaynakları serbest bırakır.

WebSocket protokolü ve temel socket programlama hakkında bu makaleyi okuduğunuz için teşekkür ederim! WebSocket veya socket programlamayla ilgili düşüncelerinizi ve deneyimlerinizi yorumlarda paylaşmaktan çekinmeyin. Bu teknolojiyle geliştirdiğiniz projeleri veya karşılaştığınız zorlukları özellikle duymak isterim.

Eğer bu makaleyi faydalı bulduysanız, daha fazla kişiye ulaşmasına yardımcı olmak için paylaşmanızı rica ederim.

(Not: Aynı makale 2025 yılında Medium’da da yayınlanmıştır.)

ierdoganierdogan10 Aralık 2025