Spring Security’nin hazır paket şeklinde sunduğu servislerden birisi de beni hatırla (remember-me) servisidir. Beni hatırla ile form tabanlı kimliklendirmeye tabi tutulan kullanıcı, tarayıcısını her açtığında kullanıcı adı ve şifresini tekrar girmek zorunda kalmaz. Bunun için login ekranında “Beni hatırla” şeklinde bir seçimlik alan olur. Bunu seçen kullanıcının kimlik bilgileri çerez olarak kullanıcının bilgisayarında saklanır ve tarayıcı kapatılıp açıldığında bu çerez Spring Security tarafından yorumlanarak kullanıcının sisteme otomatik login olması sağlanır. Beni hatırla kabiliyetinden yararlanabilmek için öncelikle login sayfasının içerisine bir input html elemanı ekleyip ismini “_spring_security_remember_me” olarak belirtmemiz gerekiyor. Bu sayede Spring Security kullanıcının, kimlik bilgilerinin çerez olarak saklanmasını isteyip istemediğini anlayacaktır.
<input type="checkbox" name="_spring_security_remember_me" accesskey="r" tabindex="3"/>
Spring Security’nin 2.x serisinden önce aldığı en büyük eleştirilerden birisi zor bir konfigürasyona sahip olmasıydı. Filtre tabanlı olan bu framework’ün çalışabilmesi için gerekli bean tanımlarının eksik veya aralarındaki bağımlılıkların yanlış yapılması, filtrelerin yanlış sıralanması en sık karşılaşılan problemlerdendi. 2.x serisinden itibaren Spring’in xml namespace konfigürasyon özelliği security framework’e de dahil edildi. Böylece minimal bir xml konfigürasyonu ile Spring Security framework’ü uygulamamızda çalışır hale getirmek mümkün hale geldi. Örneğin, application context dosyamız içerisinde şeklinde bir tanım framework’ün varsayılan ayarlarla çalışması için yeterlidir. Varsayılan ayarlar içerisinde beni hatırla servisi de mevcuttur.
Tabi xml namespace kabiliyeti, konfigürasyonu oldukça kolaylaştırmasına ve hata ihtimalini en aza indirmesine karşın framework’ün iç yapısını ve bean ilişkilerini göz önünden uzaklaştırdığı için işleyişi tam kavramayı zorlaştırmaktadır. Framework’ün kullanımı biraz ezbere olmaktadır. Bu nedenle Spring security framework’ü kullanırken örnek çalışmalar ve hızlı startup’lar dışında xml namespace konfigürasyonu yerine gerekli bean wiring işlemlerini doğrudan kendim yapmayı daha çok seviyorum. Beni hatırla servisi de yukarıdaki gibi bir xml elemanı ile aktive edilse bile çalışabilmek için framework içerisinde değişik bean tanımlarına ve bean’lar arasındaki ilişkilere ihtiyaç duymaktadır. Bu tanımları ve ilişkileri bilirsek runtime’da ortaya çıkabilecek hatalara daha kolay yorum yapmamız ve doğru bir tespitte bulunmamız mümkün olabilir. Şimdi beni hatırla servisinin detaylarına hep birlikte bakalım.
Servisi detaylı olarak incelemeyi beni hatırla çerezinin oluşturulması, çerezin okunması ve otomatik kimliklendirme işleminin gerçekleştirilmesi, çerezin geçersiz kılınması şeklinde üç ana bölüme ayırarak gerçekleştireceğim.
Beni Hatırla Çerezinin Oluşturulması
Form tabanlı kimliklendirmenin temel bean’ı AuthenticationProcessingFilter’dır. Bu filtre kullanıcının kimlik bilgilerini gönderilen formdan aldıktan sonra AuthenticationManager vasıtası ile kimliklendirme işlemini gerçekleştirir. Kimliklendirme işlemi başarılı ise kullanıcı asıl erişmek istediği sayfaya yönlendirilir. Başarılı biçimde sonuçlanan login işlemi sonrası AuthenticationProcessingFilter, asıl sayfaya yönlendirme yapmadan evvel RememberMeServices bean’ı vasıtası ile beni hatırla çerezinin oluşturulmasını tetikler. RememberMeServices bean’ının loginSuccess() metodu çağırılır ve bu metod içerisinde çerez hazırlanıp http response’a set edilir.
Beni Hatırla Çerezinin Okunması ve Otomatik Kimliklendirme İşlemi
Beni hatırla servisinin giriş noktası ise RememberMeAuthenticationFilter bean’ıdır. Bu bean securityFilterChain’de AuthenticationProcessingFilter bean’ından sonra gelmelidir. Web uygulamasına herhangi bir web isteği geldiği zaman RememberMeAuthenticationFilter devreye girer ve SecurityContext’de Authentication nesnesi yoksa kendisine kayıtlı RememberMeServices bean’ının autoLogin() metodunu çağırarak bir Authentication nesnesi elde etmeye çalışır. RememberMeServices beanı tarayıcının gönderdiği çerezler arasında beni hatırla çerezini bulur ve geçerliliğini kontrol ettikten sonra çerez içindeki kullanıcı adı bilgisini kullanarak Athentication nesnesini döner. Bu noktada RememberMeServices bean’ının Authentication nesnesini nasıl elde ettiğine yakından bakmak yararlı olacaktır. “Beni hatırla” çerezi default olarak kullanıcının sisteme interaktif biçimde, yani kullanıcı adı ve şifre ile, giriş yaptığı tarihten itibaren 14 gün boyunca geçerlidir. Bu süre içerisinde kullanıcı şifresini değiştirirse çerez içerisinde tutulan MD5 hash bilgisi ile uyumsuzluk ortaya çıkacağı için geçerli bir Authentication nesnesi dönülmeyecektir. Eğer çerezde herhangi bir problem yoksa RememberMeServices bean’ı çerezden elde edilen username bilgisini kullanarak UserDetailsService aracılığı ile UserDetails nesnesini elde eder ve Authentication nesnesini oluşturarak döner.
Eğer bir Authentication nesnesi dönülürse bu sefer de AuthenticationManager’dan bu nesneyi authenticate() etmesi istenir. AuthenticationManager değişik Authentication tiplerini işleyebilmek için farklı AuthenticationProvider’lar kullanmaktadır. RememberMeServices tarafından oluşturulan Authentication nesnesini değerlendirmesi için RememberMeAuthenticationProvider’ın AuthenticationManager’a eklenmesi gerekir. RememberMeAuthenticationProvider, Authentication nesnesine RememberMeServices tarafından verilen key bilgisi ile kendi elindeki key bilgisinin aynı olup olmadığını kontrol eder. Eğer key bilgilerinde bir farklılık yoksa Authentication geçerlidir, aksi durumda BadCredentialsException fırlatılarak authentication hatası verilir. AuthenticaitonManager’ın bu noktada çağırılması sayesinde “concurrent session” ve diğer authentication sürecine özgü davranışlar korunmuş olur.
Yeri gelmişken Spring Security tarafından TokenBasedRememberMeServices ve PersistentTokenBasedRememberMeServices adında iki farklı RememberMeServices sağlandığını belirtelim. Birincisi Authentication bilgisini çerez olarak kullanıcının tarayıcısında saklamaktadır. Tabi bu saklanan çerez içerisinde kesinlikle kullanıcının şifresi tutulmuyor. Çerez içerisinde sadece username bilgisi, token’ın son kullanım tarihi ve içerisinde kullanıcı şifresininde bulunduğu bir string ifadenin MD5 hash değeri bulunmaktadır. Bu pek çok web uygulaması için yeterlidir. Ancak username bilgisinin dahi tarayıcı tarafında tutulmasını istemiyorsanız daha güvenli bir çözüm olarak PersistentTokenBasedRememberMeServices bean’ını kullanabilirsiniz. Ben yazıda TokenBasedRememberMeServices’i baz aldım.
Beni Hatırla Çerezinin Geçersiz Kılınması
Spring Security’nin sağladığı RememberMeServices sınıfları LogoutHandler arayüzünü implement etmektedirler. LogoutHandler arayüzünü implement eden beanların logout() metodları LogoutFilter tarafından logout aşamasında sıra ile çağırılmaktadır. RememberMeServices bean’ının logout() metodu çerezi iptal etmekle görevlidir. Bu sayede uygulamadan logout olan kullanıcı bir sonraki sefer uygulamaya erişmeye çalıştığında karşısına login penceresi gelecektir. RememberMeServices sınıfının LogoutHandler arayüzünü kullanan diğer bir sınıf ise ConcurrentSessionFilter’dır. Eğer uygulamanızda Spring Security’nin bir kullanıcının açtığı oturum sayısını yönetmeyi sağlayan “concurrent session” kabiliyetini kullanıyorsanız ConcurrentSessionFilter da kullanıcının oturumunu sonlandırma aşamasında kendisine kayıt olunan logout handler nesnelerini çağırmaktadır. Çerezleri geçersiz kılmanın bir diğer yolu ise sistem yöneticisinin key bilgisini değiştirmesidir. Bu sayede sistem genelinde bütün çerezler geçersiz hale gelir.
Diğer Noktalar
Remember me servisinin konfigürasyonu sırasında dikkat edilmesi gereken önemli bir nokta da kimliklendirme isteği sırasında HttpSession’ın oluşturulmasıdır. Normal interaktif bir kimliklendirme isteğinde, AuthenticationProcessingFilter eğer kimliklendirme başarılı ise varsayılan ayarlarla HttpSession oluşmasını sağlamaktadır. Bunun olmadığı durum AuthenticationProcessingFilter bean’ının allowSessionCreation değişkeninin değerinin false yapılmasıdır. Ancak beni hatırla çerezi ile kimliklendirmenin otomatik olarak yapılması durumunda RememberMeAuthenticationFilter ve RememberMeServices bean’ları bu şekilde davranmamaktadır. Bu beanlar kimliklendirme sırasında HttpSession oluşmasını tetiklemezler. Peki bu farklılık sistemde nasıl bir probleme yol açabilir? Eğer uygulamanızda bir kullanıcının açtığı oturum sayısını yönetmeyi sağlayan “concurrent session” kabiliyetini kullanıyorsanız problem olacaktır. AuthenticationManager vasıtası ile ConcurrentSessionController, web isteğini yapan kullanıcının hali hazırda yeni bir oturum açıp açamayacağını kontrol için Authentication nesnesinden geçerli bir sessionId beklemektedir. Fakat Authentication’daki sessionId bilgisi de ancak HttpSession mevcutsa oluşturulabilmektedir, aksi takdirde sessionId null olacaktır. Bu problemin ortadan kaldırılabilmesi için RememberMeServices’in Authentication nesnesini oluşturması aşamasında geçerli bir HttpSession’ın mevcut olması gerekmektedir. Bunun için en uygun yer de HttpSessionContextIntegrationFilter bean’ıdır. HttpSessionContextIntegrationFilter normalde sadece yeni bir SecurityContext mevcut ise HttpSession oluşmasını tetiklemektedir. Ancak forceEagerSessionCreation değişkenini true yaparak bu filtrenin gelen her web isteği için bir HttpSession oluşması sağlanabilir. Bu sayede remember me servisi tarafından yapılan otomatik kimlik denetimi sonucu oluşacak Authentication nesnesine de geçerli bir sessionId atanmış olur.