← Kursa Dön
📄 Text · 25 min

Remember-Me ve Session Yönetimi

HTTP stateless bir protokoldür — her istek bağımsızdır ve sunucu önceki istekleri hatırlamaz. Kullanıcının giriş yapmış durumda kalabilmesi için session (oturum) mekanizması kullanılır. Spring Security, session yönetimi için kapsamlı araçlar sunar: Remember-Me ile uzun süreli oturum, session fixation koruması, eşzamanlı oturum sınırlaması ve session timeout yapılandırması.

Bunu bir otele giriş sürecine benzetebilirsiniz. Check-in yaptığınızda size bir oda kartı (session cookie) verilir. Bu kartla otelin herhangi bir yerine erişebilirsiniz. Kart süresi dolduğunda (session timeout) tekrar resepsiyona gitmeniz gerekir. "Beni hatırla" seçeneği ise otel sadık müşteri kartı gibidir — bir sonraki gelişinizde bilgileriniz zaten sistemdedir.

HTTP Session Temelleri

Kullanıcı giriş yaptığında Spring Security, SecurityContext'i HTTP session'a kaydeder. Sonraki isteklerde JSESSIONID cookie'si ile session bulunur ve SecurityContext yüklenir:

1. POST /login (username + password)
       ↓
2. Authentication başarılı
       ↓
3. SecurityContext oluşturulur (username, roles, authorities)
       ↓
4. SecurityContext session'a yazılır (HttpSession)
       ↓
5. Yanıt header: Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly
       ↓
6. Tarayıcı JSESSIONID cookie'sini saklar
       ↓
7. Sonraki istekler: Cookie: JSESSIONID=abc123
       ↓
8. Spring Security JSESSIONID ile session'ı bulur
       ↓
9. Session'dan SecurityContext yüklenir → kullanıcı tanınır

Bu mekanizma, HTTP'nin stateless doğasını aşmanın standart yoludur. Cookie tarayıcı tarafında, session ise sunucu tarafında saklanır.

Session Yapılandırması

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .sessionManagement(session -> session
            // Session oluşturma politikası
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)

            // Session fixation koruması
            .sessionFixation(fix -> fix.migrateSession())

            // Concurrent session kontrolü
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false)
            .expiredUrl("/login?expired")
        )
        .build();
}

SessionCreationPolicy Seçenekleri

PolitikaAçıklamaKullanım
IF_REQUIREDGerektiğinde session oluştururVarsayılan — web uygulamaları
ALWAYSHer zaman session oluştururNadir — özel gereksinimler
NEVERSession oluşturmaz ama varsa kullanırHibrit uygulamalar
STATELESSSession hiç kullanmazJWT tabanlı REST API'ler
// REST API — stateless (session yok, her istekte token gönderilir)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// Web uygulaması — session tabanlı (varsayılan)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))

💡 İpucu: JWT tabanlı authentication kullanıyorsanız STATELESS seçin. Bu durumda Spring Security session oluşturmaz ve JSESSIONID cookie'si gönderilmez. Her istekte JWT token ile authentication yapılır.

Session Fixation Koruması

Session fixation, saldırganın bildiği bir session ID'sini kurbana kullandırtmasıdır. Saldırgan bu session ID ile kurbanın oturumunu ele geçirir:

Saldırı senaryosu:
1. Saldırgan siteyi ziyaret eder → JSESSIONID=evil123 alır
2. Saldırgan bu session ID'sini kurbanın tarayıcısına yerleştirir
   (XSS, URL manipulation, fiziksel erişim vb.)
3. Kurban evil123 session'ı ile giriş yapar
4. Artık evil123 session'ında authentication bilgisi var!
5. Saldırgan evil123 ile kurbanın oturumuna erişir!

Spring Security koruması: Login başarılı olduğunda yeni bir session oluşturulur ve eski session ID geçersiz olur:

.sessionFixation(fix -> fix.migrateSession())

// migrateSession (VARSAYILAN): Yeni session oluştur, attribute'ları kopyala
//   → En dengeli: güvenli ve mevcut session verilerini korur

// newSession: Yeni session oluştur, attribute'ları kopyalama
//   → Daha güvenli ama shopping cart gibi veriler kaybolur

// changeSessionId: Aynı session, yeni ID (Servlet 3.1+)
//   → En performanslı — session nesnesi aynı kalır, sadece ID değişir

// none: Koruma YOK — KESİNLİKLE YAPMAYIN!

⚠️ Dikkat: Session fixation korumasını asla devre dışı bırakmayın (fix.none()). Bu, OWASP Top 10 güvenlik açıklarından biridir.

Concurrent Session Kontrolü

Aynı kullanıcının kaç farklı tarayıcıdan/cihazdan aynı anda giriş yapabileceğini sınırlandırır. Bu, hesap paylaşımını ve ele geçirmeyi önler:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .sessionManagement(session -> session
            .maximumSessions(1)               // Aynı anda en fazla 1 oturum
            .maxSessionsPreventsLogin(false)   // false: eski oturum sonlandırılır
                                                // true: yeni giriş engellenir
            .expiredUrl("/login?expired=true") // Eski oturum expire olduğunda
            .sessionRegistry(sessionRegistry())
        )
        .build();
}

// Concurrent session kontrolü için GEREKLI
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

@Bean
public SessionRegistry sessionRegistry() {
    return new SessionRegistryImpl();
}

maxSessionsPreventsLogin davranışı:

`false` (varsayılan) — Son giren kazanır:

  • Ali bilgisayardan giriş yapar → Session A aktif

  • Ali telefondan giriş yapar → Session B aktif, Session A geçersiz kılınır

  • Bilgisayarda sonraki istekte "Session expired" mesajı görünür

  • Kullanıcı dostu yaklaşım — normal kullanımda sorun çıkarmaz

`true` — İlk giren kalır:

  • Ali bilgisayardan giriş yapar → Session A aktif

  • Ali telefondan giriş yapmaya çalışır → Reddedilir, Session A aktif kalır

  • "Maximum sessions exceeded" hatası alınır

  • Güvenlik odaklı yaklaşım — hesap paylaşımını kesinlikle engeller

Aktif Oturumları Yönetme

SessionRegistry ile aktif oturumları listeleyebilir ve sonlandırabilirsiniz:

@RestController
@RequestMapping("/api/sessions")
public class SessionController {

    private final SessionRegistry sessionRegistry;

    public SessionController(SessionRegistry sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }

    // Bir kullanıcının aktif oturumlarını listele
    @GetMapping("/active/{username}")
    @PreAuthorize("hasRole('ADMIN')")
    public List<Map<String, Object>> getActiveSessions(@PathVariable String username) {
        return sessionRegistry.getAllSessions(
                new org.springframework.security.core.userdetails.User(
                    username, "", List.of()), false)
            .stream()
            .map(info -> Map.<String, Object>of(
                "sessionId", info.getSessionId(),
                "lastRequest", info.getLastRequest(),
                "expired", info.isExpired()
            ))
            .toList();
    }

    // Belirli bir oturumu sonlandır
    @DeleteMapping("/{sessionId}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> invalidateSession(@PathVariable String sessionId) {
        SessionInformation info = sessionRegistry.getSessionInformation(sessionId);
        if (info != null) {
            info.expireNow();
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }
}

Remember-Me Authentication

Remember-Me, kullanıcının tarayıcısını kapattıktan sonra bile giriş yapmış kalmasını sağlar. Uzun ömürlü bir cookie oluşturulur ve sonraki ziyaretlerde bu cookie ile otomatik authentication yapılır.

Hash-Based Remember-Me (Basit Yaklaşım)

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .rememberMe(remember -> remember
            .key("uniqueAndSecretKey-change-in-production!")  // Token imzalama anahtarı
            .tokenValiditySeconds(604800)   // 7 gün (saniye cinsinden)
            .rememberMeParameter("remember") // Checkbox name attribute
            .rememberMeCookieName("remember-me")
            .userDetailsService(userDetailsService)
        )
        .build();
}

Login form'unda checkbox ekleyin:

<div class="form-check">
    <input type="checkbox" name="remember" id="remember" class="form-check-input"/>
    <label for="remember" class="form-check-label">Beni hatırla (7 gün)</label>
</div>

Token formülü:

token = base64(username + ":" + expirationTime + ":" + 
        md5(username + ":" + expirationTime + ":" + password + ":" + key))

Dezavantajları:

  • Kullanıcı şifresini değiştirirse tüm remember-me token'ları geçersiz olur

  • Token ele geçirildiğinde süresi dolana kadar kullanılabilir

  • Token rotation yok — aynı token sürekli kullanılır

Persistent Token (Daha Güvenli)

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .rememberMe(remember -> remember
            .tokenRepository(persistentTokenRepository())
            .tokenValiditySeconds(604800)
            .userDetailsService(userDetailsService)
        )
        .build();
}

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
    repo.setDataSource(dataSource);
    // İlk çalıştırmada tablo oluştur (sonra false yapın):
    // repo.setCreateTableOnStartup(true);
    return repo;
}

Gerekli tablo:

CREATE TABLE persistent_logins (
    username  VARCHAR(64) NOT NULL,
    series    VARCHAR(64) PRIMARY KEY,
    token     VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

Persistent token yaklaşımında:

  1. Her remember-me kullanımında yeni token üretilir (token rotation)

  2. Eski token geçersiz olur

  3. Token çalındığında ve kullanıldığında, gerçek kullanıcı ile saldırgan arasında bir yarış oluşur

  4. Sistem iki farklı token görürse tüm remember-me token'larını geçersiz kılar (saldırı tespit)

💡 İpucu: Production uygulamalarında persistent token yaklaşımını kullanın. Hash-based yaklaşım sadece basit uygulamalar için uygundur.

Session Timeout

# application.properties
server.servlet.session.timeout=30m    # 30 dakika (varsayılan)
server.servlet.session.cookie.http-only=true   # XSS'e karşı koruma
server.servlet.session.cookie.secure=true      # Sadece HTTPS
server.servlet.session.cookie.same-site=Lax    # CSRF koruması
server.servlet.session.cookie.name=MYSESSIONID # Custom cookie adı
server.servlet.session.cookie.max-age=3600     # Cookie max age (saniye)
AyarAçıklamaÖnerilen
http-only=trueJavaScript erişimini engeller (XSS koruması)Açık
secure=trueSadece HTTPS üzerinden gönderilirProduction'da açık
same-site=LaxCross-site isteklerde cookie gönderimi kontrolLax (varsayılan)
SameSite seçenekleri:
- Strict: Cookie SADECE aynı site'den gelen isteklerde gönderilir
  → En güvenli ama harici linkten gelince oturum kaybı
  
- Lax: GET gibi güvenli metotlarda cross-site gönderilir, POST'ta gönderilmez
  → İyi denge — çoğu uygulama için ideal

- None: Cookie HER ZAMAN gönderilir (Secure flag ZORUNLU)
  → 3rd-party entegrasyonlar için

Redis ile Distributed Session

Birden fazla uygulama instance'ı (load balancer arkasında) varsa, session'ların paylaşılması gerekir:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
spring.session.store-type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.session.redis.namespace=myapp:session

Bu yapılandırmayla session bilgileri Redis'te saklanır. Kullanıcı Instance-A'da login olur, sonraki isteği Instance-B'ye gider — Redis'ten session yüklenir ve kullanıcı tanınır.

// Redis session yapılandırması
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 30 dakika
public class RedisSessionConfig {
    
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory("localhost", 6379);
    }
    
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("MYSESSIONID");
        serializer.setDomainName("example.com"); // Subdomain paylaşımı
        serializer.setCookiePath("/");
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite("Lax");
        return serializer;
    }
}

Redis'in avantajları: session verisi uygulama restart'ından etkilenmez, horizontal scaling desteklenir, session TTL Redis tarafından otomatik yönetilir.

Yaygın Session Sorunları ve Çözümleri

Sorun 1: Session Timeout Sonrası AJAX İstekleri

SPA uygulamalarında session expire olduğunda AJAX istekleri 302 (redirect to login) alır ama JavaScript bunu düzgün işleyemez:

// Custom AuthenticationEntryPoint — AJAX için JSON yanıt
http.exceptionHandling(ex -> ex
    .authenticationEntryPoint((request, response, authException) -> {
        if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))
                || request.getRequestURI().startsWith("/api/")) {
            // AJAX/API isteği → JSON 401
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\":\"Session expired\",\"loginUrl\":\"/login\"}");
        } else {
            // Normal istek → Login sayfasına yönlendir
            response.sendRedirect("/login?expired");
        }
    })
);

Sorun 2: CSRF Token Session Bağımlılığı

Session expire olduğunda CSRF token da geçersiz olur. Form submit'leri 403 alır:

// Session expire sonrası login sayfasına nazik yönlendirme
http.csrf(csrf -> csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);

// Veya özel InvalidSessionStrategy
http.sessionManagement(session -> session
    .invalidSessionUrl("/login?invalid-session")
    .invalidSessionStrategy((request, response) -> {
        request.getSession(); // Yeni session oluştur
        response.sendRedirect("/login?session-expired");
    })
);

Sorun 3: Remember-Me ile Güvenlik Seviyesi

Remember-Me ile giriş yapan kullanıcılar tam authentication'a sahip değildir. Hassas işlemler (şifre değiştirme, ödeme) için tekrar login gerekebilir:

http.authorizeHttpRequests(auth -> auth
    // Herkes (anonim dahil)
    .requestMatchers("/public/**").permitAll()
    
    // Remember-me VEYA tam login (hatırlanmış kullanıcılar dahil)
    .requestMatchers("/profile/**").authenticated()
    
    // SADECE tam login (remember-me yetmez — şifre ile giriş şart)
    .requestMatchers("/settings/password/**").fullyAuthenticated()
    .requestMatchers("/payments/**").fullyAuthenticated()
    .requestMatchers("/admin/**").fullyAuthenticated()
);

authenticated() → remember-me ile giriş yapan kullanıcıları da kabul eder fullyAuthenticated() → sadece username+password ile giriş yapan kullanıcıları kabul eder

Bu ayrım, güvenlik ile kullanıcı deneyimi arasında denge kurar: genel sayfalar remember-me ile erişilebilir ama hassas işlemler tam login gerektirir.

Session Events — Oturum Olayları

@Component
public class SessionEventListener {

    @EventListener
    public void onSessionCreated(HttpSessionCreatedEvent event) {
        log.info("Session oluşturuldu: {}", event.getSession().getId());
    }

    @EventListener
    public void onSessionDestroyed(HttpSessionDestroyedEvent event) {
        List<SecurityContext> contexts = event.getSecurityContexts();
        for (SecurityContext ctx : contexts) {
            if (ctx.getAuthentication() != null) {
                log.info("Session sonlandı — Kullanıcı: {}", 
                    ctx.getAuthentication().getName());
            }
        }
    }
}

Özet

  • HTTP Session, stateless HTTP protokolünde kullanıcı oturumunu yönetir. JSESSIONID cookie'si ile session tanınır.

  • SessionCreationPolicy ile session davranışı belirlenir: web uygulamalarında IF_REQUIRED, REST API'lerde STATELESS.

  • Session fixation koruması, login'de yeni session oluşturarak saldırıyı engeller. migrateSession() varsayılan ve önerilen stratejidir.

  • Concurrent session kontrolü, aynı kullanıcının kaç cihazdan giriş yapabileceğini sınırlar. HttpSessionEventPublisher bean gereklidir.

  • Remember-Me: Hash-based (basit) ve Persistent token (güvenli) olmak üzere iki yaklaşım vardır. Production'da persistent token kullanın.

  • Session cookie güvenliği: HttpOnly, Secure, SameSite flag'leri XSS ve CSRF saldırılarına karşı koruma sağlar.

  • Distributed session (Redis) ile load balancer arkasındaki çoklu instance'lar arasında session paylaşılabilir.

  • `fullyAuthenticated()` ile remember-me oturumu ve tam login arasında ayrım yapılabilir. Hassas işlemler (ödeme, şifre değiştirme) tam login gerektirir.

  • Session timeout sonrası AJAX isteklerini düzgün yönetmek için custom AuthenticationEntryPoint ile JSON 401 yanıtı döndürün.

  • Production'da session cookie güvenlik flag'lerini (HttpOnly, Secure, SameSite) mutlaka yapılandırın. Bu flag'ler olmadan session cookie'si XSS ve CSRF saldırılarına karşı savunmasız kalır.

  • HttpSessionEventPublisher bean'ini unutmayın — bu olmadan concurrent session kontrolü çalışmaz çünkü Spring session destroy event'lerinden haberdar olmaz.