Merhabalar arkadaşlar. Bu yazıda Spring çatısının güvenlik modülü olan Spring Security’nin anotasyonlarla nasıl kullanıldığını öğrenmeye çalışacağız.
Bu yazıda kullanılan projeyi Github hesabımda paylaştım. Maven projesi olduğu için IDE’nizde çalıştırabilir, test edebilirsiniz. Proje şuradadır:https://github.com/ilkgunel/SpringSecurityWithAnnotations
Daha önce şuradaki yazımda bir XML dosyası ile Spring Security’nin kullanımını anlatmaya çalışmıştım. Bu yazıda ise tamamen XML olmadan aynı işi yapmaya çalışacağız. Şimdi başlayalım 🙂
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <context-param> <param-name>javax.faces.PROJECT_STAGE</param-name> <param-value>Development</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.xhtml</url-pattern> </servlet-mapping> <session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>/hello.xhtml</welcome-file> </welcome-file-list> </web-app>
Web.xml dosyasına şöyle bir göz gezdirdiğinizde Spring’e dair hiçbir şey göremeyeceksiniz. Yazımızın konsepti de bu. Tüm işlemler kod aracılığı ile yapılacak.
Web.xml’den anlayacağımız üzere ana sayfamız hello.xhtml.
hello.xhtml
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <h:head> <title>Ana Sayfa</title> </h:head> <h:body> <h:form> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <center><h2>Anotasyonlarla Spring Security Kullanımı</h2></center> <center><h:commandLink value="Giriş Yapmak İçin Tıklayın" action="girisYap.xhtml?faces-redirect=true"/></center> </h:form> </h:body> </html>
Hello sayfasında basit bir karşılama yapıyoruz ve giriş sayfasına yönlendiren commandLink kullanıyoruz. <input type=”hidden” />’nın bulunduğu satırda bir CSRF token göndermesi yapıyoruz. Küçük bir araştırma yaptım ve Spring 4 ile birlikte CSRF token meselesinin açık olarak geldiğini gördüm. Kod içerisinden istenirse CSRF disable edilebilir fakat bu da tavsiye edilen bir durum değil.
girisYap.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html" xmlns:p="http://primefaces.org/ui"> <h:head> <title>Giriş Yapın</title> <f:event listener="#{girisDenetlemeSinifi.authorizedUserControl()}" type="preRenderView"/> </h:head> <h:body> <h:form prependId="false"> <center style="margin-top:50px;"> <p:panelGrid columns="2"> <h:outputText value="Kullanıcı Adınız:"/> <p:inputText id="j_username" required="true" requiredMessage="Kullanıcı Adı Boş Geçilemez"/> <h:outputText value="Şifreniz:" /> <p:password id="j_password" required="true" requiredMessage="Şifre Kısmı Boş Geçilemez"/> <h:outputText value="" /> <p:commandButton value="Giriş Yapın" action="#{girisDenetlemeSinifi.login()}" ajax="false"/> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <h:outputText value=""/> </p:panelGrid> </center> <h:outputLabel value="#{sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}" rendered="#{!empty sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}" style="margin-left: 5px;color: red"/> </h:form> </h:body> </html>
Giriş yapma sayfamızda <head> etiketleri arasındaki f:event bu sayfaya istekte bulunulduğunda oturum açıl açılmadığına bakan kodu çağırıyor ve ona göre ya bu sayfayı atlıyor ya da açıyor.
p:inputText ve p:password etiketleri ile kullanıcıdan kullanıcı adı ve parolasını alıyoruz.
p:commdandButton ile de giriş yaptırıyoruz.
En alttaki h:outputLabel giriş sırasında meydana gelecek hataları ekrana yazdırmak için var.
/adminPage/adminPage.xhtml
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h2>Admin Sayfasına Hoş Geldiniz</h2> <br/> <a href="#{request.contextPath}/logout">Çıkış Yapmak İçin Tıklayın</a> </h:body> </html>
Admin sayfasında bir karşılama mesajı ve çıkış linki yer alıyor.
/guestPage/guestPage.xhtml
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html"> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h2>Guest Sayfasına Hoş Geldiniz</h2> <br/> <a href="#{request.contextPath}/logout">Çıkış Yapmak İçin Tıklayın</a> </h:body> </html>
Guest sayfasında bir karşılama ve çıkış linki yer alıyor.
AppConfig.java
package myPackage; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.jdbc.datasource.DriverManagerDataSource; @Configuration @ComponentScan(value = {"myPackage"}) public class AppConfig { @Bean(name = "messageSource") public ResourceBundleMessageSource messageSource() { ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource(); resourceBundleMessageSource.setBasenames("messages"); return resourceBundleMessageSource; } @Bean(name = "dataSource") public DriverManagerDataSource dataSource() { DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource(); driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver"); driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/BlogProjesi"); driverManagerDataSource.setUsername("root"); driverManagerDataSource.setPassword(""); return driverManagerDataSource; } }
AppConfig sınıfımız iki notasyon ile işaretli ve içinde veri tabanına bağlantı kuracak kısım ile giriş sırasındaki hataları Türkçe’ye çevirecek messages dosyasının kayda geçirilmesi yapılıyor.
@Configuration notasyonu sınıfın bir ayarlama sınıfı olduğunu bildiriyor.
@ComponentScan(value = {“myPackage”}) ifadesi kendisine value olarak verilen paketleri tarayan ve gerekli yüklemeleri yapan ifadedir. Örneğin birazdan göreceğimizspringSecurityFilterChain’i yükleme görevi olan SpringSecurityInitializer sınıfı bu notasyon sayesinde işini yapar.
@Bean(name = “messageSource”) ile işaretli metot, giriş sırasında meydana gelen hataları kullanıcıya bildirmek için messages properties dosyasının kullanımını gösteriyor.
@Bean(name = “dataSource”) ifadesi ile dataSource isminde az sonra başka bir sınıfta kullanacağımız bir veri kaynağı hazırlamış oluyoruz.
SecurityConfig.java
package myPackage; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.encoding.Md5PasswordEncoder; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired DataSource dataSource; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery("select kullaniciAdi,password, enabled from uyeler where kullaniciAdi=?") .authoritiesByUsernameQuery("select kullaniciAdi, role from userroles where kullaniciAdi=?") .passwordEncoder(new Md5PasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/adminPage/**").access("hasRole('ROLE_ADMIN')") .antMatchers("/guestPage/**").access("hasRole('ROLE_GUEST') or hasRole('ROLE_ADMIN')") .and().formLogin().loginPage("/girisYap.xhtml").defaultSuccessUrl("/guestPage/guestPage.xhtml") .usernameParameter("j_username").passwordParameter("j_password") .and().csrf().csrfTokenRepository(csrfTokenRepository()); http.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout")); } private CsrfTokenRepository csrfTokenRepository() { HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); repository.setSessionAttributeName("_csrf"); return repository; } }
SecurityConfig sınıfımızda da bir takım ayarlama tanımlamaları var olduğundan bu sınıf da @Configurtion ile işaretli. İlave olarak bu sınıfta güvenlik ile ilgili tanımlamaları yapacağımız için @EnableWebSecurity notasyonu ile sınıfımız işaretli.
Sınıf içerisinde güvenlik işlemlerinde kullanılacak veri tabanı işlemleri için az önce tanımlananan dataSource’u @AutoWire notasyonu ile sınıfımıza enjekte ediyoruz. @AutoWire tanımlı ve gerekli olan sınıfdan bağımlılığı otomatik yükleyebilen bir notasyondur.
Devam eden kısımda @AutoWire ile işaretlenmiş configureGlobal metodunda kullanıcı adını ve şifresini giren kullanıcının kimlik doğrulama ve yetkilendirme işlemi yapılıyor. usersByUsernameQuery metodu kimliği doğrulama, authoritiesByUsernameQuery metodu yetkilendirme vazifelerini üzerinlerine alıyorlar. passwordEncoder metodu da veri tabanında Hash’li bir şekilde tutulan parola ile karşılaştırma yapmak için kullanıcının girdiği parolayı verilen hash algoritmasına göre hash’leyen metotdur.
configure metodu içerisinde sayfaların erişim haklarını yazıyoruz. Örneğin guestPage dizini ve dizinin içindekilere ROLE_GUEST ya da ROLE_ADMIN yetkisine sahip kişilerin erişebileceğini söylüyoruz. loginPage metodu ile giriş sayfasını tanımlıyor, defaultSuccessUrl ile başarılı girişin yönlendirileceği sayfayı belirtiyoruz. http.logout().logoutRequestMatcher(new AntPathRequestMatcher(“/logout”)); ifadesi de kullanıcının çıkış yapabilmesini sağlayan kısımdır.
SpringMvcInitializer.java
package myPackage; import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; public class SpringMvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[] { AppConfig.class }; } @Override protected Class<?>[] getServletConfigClasses() { return null; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } }
SpringMvcInitializer sınıfı projenin çalışması için gerekli olan DispatcherServlet yüklenmesi vazifesini yerine getiriyor.
SpringSecurityInitializer.java
package myPackage; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer { //do nothing }
SpringSecurityInitializer sınıfı SpringSecurityFilterChain’i otomatik yükleme vazifesini üzerine alan sınıfdır. Bu otomatik yükleme işlemini extend ettiği AbstractSecurityWebApplicationInitializer aracılığı ile yapar.
EKRAN ÇIKTILARI
Projenin ekran çıktılarından önce veri tabanındaki kullanıcıların barındığı tabloya bakalım.
Ayrıca bir de yetkilendirme için kullanılan userroles tablosuna bakalım.
Projeyi çalıştırdığımda gelen ekran. Tarayıcıdaki URL’e /adminPage/adminPage.xhtml ya da guestPage/guestPage.xhtml eklemesi yaptığımızda güvenlik çatısında yaptığımız ayarlamalardan ötürü giriş sayfasına yönlendirileceğiz. Giriş yapmak için tıklıyorum.
Giriş yapma ekranı. Şimdi geçersiz şeylerle giriş denemesi yapıyorum.
Başarısız olan giriş denemesi neticesinde sebep bize döndürüldü. Şimdi ilkgunel – 12345 kombinasyonu ile giriş yapacağım.
Başarılı bir şekilde giriş yaptım ve varsayılan olarak Guest sayfasına yönlendirildim. Veri tabanında ilkgunel ROLE_GUEST yetkisine sahip. Admin erişimine açık sayfaya erişim talep edelim.
Access is denied uyarısı alarak admin sayfasına erişimim Spring Security tarafından engellendi. Şimdi geri gelip çıkış yapıp turgunel hesabı ile giriş yapıyorum.
turgunel hesabı ROLE_ADMIN yetkisinde olduğu için admin sayfasına başarılı bir erişim sağladım.
Bu yazıda anlatıcaklarım bu kadar arkadaşlar. XML’e hiç bulaşmadan (web.xml & Spring XML) Spring Security’nin nasıl kullanıldığını öğrenmiş olduk. Umarım faydalı ve anlaşılır bir yazı olmuştur. Varsa eleştiri ve düşüncelerinizi ya da sorularınızı yoruma yazmanızı sizden rica ederim arkadaşlar. Başka bir yazıda görüşene kadar sağlıcakla kalın.