16. [Cours]: Spring Security ile bir web hizmetine erişimi güvenli hale getirme
Anahtar kelimeler: çok katmanlı mimari, Spring, bağımlılık enjeksiyonu, web hizmeti / güvenli jSON, istemci / sunucu
16.1. Support
![]() | ![]() |
Bu bölümdeki projeler [support / chap-16] klasöründe bulunur. SQL komut dosyası, testler için gerekli veritabanını oluşturmak amacıyla kullanılır.
16.2. Web Uygulamasında Spring Security’nin Yeri
Spring Security'yi bir web uygulaması geliştirme sürecinde ele alalım. Çoğu zaman, bu uygulama aşağıdaki gibi çok katmanlı bir mimari üzerine inşa edilir:
![]() |
- [Spring Security] katmanı, [web] katmanına yalnızca yetkili kullanıcıların erişimine izin verir.
16.3. Spring Security ile ilgili bir eğitim
Aşağıdaki 1'den 3'e kadar olan adımları izleyerek bir Spring kılavuzunu tekrar içe aktaracağız:
![]() |
![]() |
Proje aşağıdaki öğelerden oluşur:
- [templates] klasöründe, projenin HTML sayfaları bulunur;
- [Application]: projenin yürütülebilir sınıfıdır;
- [MvcConfig]: Spring yapılandırma sınıfıdır; MVC;
- [WebSecurityConfig]: Spring Security yapılandırma sınıfıdır;
16.3.1. Maven Yapılandırması
[3] projesi bir Maven projesidir. Bağımlılıklarını öğrenmek için [pom.xml] dosyasını inceleyelim:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- etiket::güvenlik[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 10-14. satırlar: proje bir Spring Boot projesidir;
- 17-20. satırlar: [Thymeleaf] çerçevesine bağımlılık;
- 22-25. satırlar: Spring Security çerçevesine bağımlılık;
16.3.2. Thymeleaf görünümleri
![]() |
[home.html] görünümü şu şekildedir:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- 12. satır: [th:href="@{/hello}"] özniteliği, [<a>] etiketinin [href] özniteliğini oluşturacaktır. [@{/hello}] değeri, [<context>/hello] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
[hello.html] görünümü şöyledir:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- 9. satır: [th:inline="text"] özniteliği, [<h1>] etiketinin metnini oluşturacaktır. Bu metin, değerlendirilmesi gereken bir $ ifadesi içerir. [[${#httpServletRequest.remoteUser}]] öğesi, geçerli HTTP sorgusunun [RemoteUser] özniteliğinin değeridir. Bu, oturum açmış kullanıcının adıdır;
- 10. satır: bir HTML formu. [th:action="@{/logout}"] özniteliği, [form] etiketinin [action] özniteliğini oluşturacaktır. [@{/logout}] değeri, [<context>/logout] yolunu oluşturacaktır; burada [context], web uygulamasının bağlamıdır;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- 8. satır: Hello [document.forms[0]]! ifadesinin çevirisi;
- 9. satır: @{/logout} ifadesinin çevirisi;
- 11. satır: (name özniteliği) adlı gizli bir alan _csrf;
[login.html] görünümü şöyledir:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- 9. satır: [th:if="${param.error}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL'in [error] (http://context/login?error) parametresini içermesi durumunda oluşturulmasını sağlar;
- 10. satır: [th:if="${param.logout}"] özniteliği, <div> etiketinin yalnızca giriş sayfasını görüntüleyen URL etiketinde [logout] parametresi (http://context/login?logout) varsa oluşturulmasını sağlar;
- 11-23. satırlar: bir HTML formu;
- 11. satır: Form, <context> web uygulamasının bağlamı olan URL [<context>/login] adresine gönderilecektir;
- 13. satır: [username] adlı bir giriş alanı;
- 17. satır: [password] adlı bir giriş alanı;
Oluşturulan HTML kodu şöyledir:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<div>
<label>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
28. satırda, Thymeleaf'in [_csrf] adlı gizli bir alan eklediği görülmektedir.
16.3.3. Spring MVC yapılandırması
![]() |
[MvcConfig] sınıfı, MVC Spring çerçevesini yapılandırır:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- 7. satır: [@Configuration] anotasyonu, [MvcConfig] sınıfını bir yapılandırma sınıfı haline getirir;
- 8. satır: [MvcConfig] sınıfı, [WebMvcConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
- 10. satır: üst sınıfın bir yönteminin yeniden tanımlanması;
- satır 11-16: [addViewControllers] yöntemi, URL'leri HTML görünümlerine ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
görünüm | |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
[html] uzantısı ve [templates] klasörü, Thymeleaf tarafından kullanılan varsayılan değerlerdir. Bunlar yapılandırma yoluyla değiştirilebilir. [templates] klasörü, projenin Classpath kök dizininde bulunmalıdır:
![]() |
Yukarıdaki [1] örneğinde, [java] ve [resources] klasörlerinin her ikisi de kaynak klasörlerdir (source folders). Bu, içeriklerinin projenin Classpath kök dizininde yer alacağı anlamına gelir. Dolayısıyla, [2] içinde, [hello] ve [templates] klasörleri Classpath'in kök dizininde yer alacaktır.
16.3.4. Spring Security Yapılandırması
![]() |
[WebSecurityConfig] sınıfı, Spring Security çerçevesini yapılandırır:
package hello;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- 9. satır: [@Configuration] anotasyonu, [WebSecurityConfig] sınıfını bir yapılandırma sınıfı haline getirir;
- 10. satır: [@EnableWebSecurity] anotasyonu, [WebSecurityConfig] sınıfını bir Spring Security yapılandırma sınıfı haline getirir;
- 11. satır: [WebSecurity] sınıfı, [WebSecurityConfigurerAdapter] sınıfını genişleterek bazı yöntemlerini yeniden tanımlar;
- 12. satır: üst sınıfın bir yöntemi yeniden tanımlanmıştır;
- 13-16. satırlar: [configure(HttpSecurity http)] yöntemi, uygulamanın çeşitli URL'lerine erişim haklarını tanımlamak üzere yeniden tanımlanmıştır;
- 14. satır: [http.authorizeRequests()] yöntemi, URL'leri erişim haklarıyla ilişkilendirmeyi sağlar. Burada aşağıdaki ilişkilendirmeler yapılır:
kural | kod | |
kimlik doğrulaması yapılmadan erişim | | |
Yalnızca kimlik doğrulaması yapılmış erişim |
- 15. satır: kimlik doğrulama yöntemini tanımlar. Kimlik doğrulama, herkesin erişebileceği bir URL [/login] formu aracılığıyla yapılır [http.formLogin().loginPage("/login").permitAll()]. Oturum kapatma (logout) işlemi de herkes tarafından gerçekleştirilebilir;
- 19-21. satırlar: kullanıcıları yöneten [configure(AuthenticationManagerBuilder auth)] yöntemini yeniden tanımlar;
- 20. satır: Kimlik doğrulama, "sabit" olarak tanımlanmış kullanıcılarla yapılır ([auth.inMemoryAuthentication()]). Burada bir kullanıcı, [user] kullanıcı adı, [password] şifre ve [USER] rolü ile tanımlanmıştır. Aynı role sahip kullanıcılara aynı haklar verilebilir;
16.3.5. Çalıştırılabilir sınıf
![]() |
[Application] sınıfı aşağıdaki gibidir:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- 8. satır: [@EnableAutoConfiguration] anotasyonu, Spring Boot'tan (3. satır) geliştiricinin açıkça yapmadığı yapılandırmayı gerçekleştirmesini ister;
- 9. satır: [Application] sınıfını bir Spring yapılandırma sınıfı haline getirir;
- 10. satır: Spring bileşenlerini aramak üzere [Application] sınıfının bulunduğu klasörün taranmasını ister. [MvcConfig] ve [WebSecurityConfig] sınıfları, [@Configuration] anotasyonuna sahip oldukları için bu şekilde tespit edilecektir;
- 13. satır: yürütülebilir sınıfın [main] yöntemi;
- 14. satır: [SpringApplication.run] statik yöntemi, [Application] yapılandırma sınıfını parametre olarak alarak yürütülür. Bu süreci daha önce görmüştük ve projenin Maven bağımlılıklarında bulunan gömülü Tomcat sunucusunun başlatılacağını ve projenin bu sunucuya dağıtılacağını biliyoruz. Dört adet URL'in [/, /home, /login, /hello] tarafından yönetildiğini ve bazılarının erişim haklarıyla korunduğunu görmüştük.
16.3.6. Uygulama Testleri
Öncelikle, kabul edilen dört URL dosyasından biri olan URL [/] dosyasını talep edelim. Bu dosya, [/templates/home.html] görünümüne bağlıdır:
![]() |
Talep edilen URL, [/] herkes tarafından erişilebilir. Bu yüzden onu elde ettik. [here] bağlantısı şöyledir:
Bağlantıya tıklandığında URL ve [/hello] dosyaları istenecektir. Bu dosya korumalıdır:
kural | kod | |
kimlik doğrulaması yapılmadan erişim | | |
sadece kimliği doğrulanmış kullanıcılar için erişim |
Bunu elde etmek için kimlik doğrulaması yapılmalıdır. Spring Security, istemci tarayıcısını kimlik doğrulama sayfasına yönlendirecektir. Görülen yapılandırmaya göre, bu sayfa URL [/login]'tir. Bu sayfaya herkes erişebilir:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Böylece [1] adresini elde ediyoruz:
![]() |
Elde edilen sayfanın kaynak kodu şöyledir:
- 7. satırda, orijinal [login.html] sayfasında bulunmayan gizli bir alan ortaya çıkıyor. Bunu Thymeleaf eklemiştir. CSRF (Cross Site Request Forgery) olarak adlandırılan bu kod, bir güvenlik açığını ortadan kaldırmayı amaçlamaktadır. Bu jetonun, kimlik doğrulamanın kabul edilmesi için Spring Security’ye kimlik doğrulama bilgileriyle birlikte geri gönderilmesi gerekir;
Hatırlayacağımız üzere, Spring Security tarafından yalnızca user/password kullanıcısı tanınmaktadır. [2] yerine başka bir şey girersek, [3]'te bir hata mesajı içeren aynı sayfayı görürüz. Spring Security, tarayıcıyı URL adresinden [http://localhost:8080/login?error] adresine yönlendirdi. [error] parametresinin varlığı, şu etiketinin görüntülenmesine neden oldu:
<div th:if="${param.error}">Invalid username and password.</div>
Şimdi, beklenen kullanıcı adı/şifre değerlerini [4] olarak girelim:
![]() |
- [4]'te kimlik doğrulaması yapıyoruz;
- [5] adresinde, Spring Security bizi URL adresine yönlendirir çünkü giriş sayfasına yönlendirildiğimizde talep ettiğimiz adres URL idi. Kullanıcının kimliği, [hello.html]'teki aşağıdaki satırda görüntülenmiştir:
[5] sayfası aşağıdaki formu görüntülüyor:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
[Sign Out] düğmesine tıklandığında, URL ve [/logout] sayfalarında bir POST işlemi gerçekleştirilecektir. Bu dosya, URL ve [/login] dosyaları gibi herkes tarafından erişilebilir:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
URL / görünümler grubumuzda, URL ve [/logout] için herhangi bir tanımlama yapmadık. Ne olacak? Deneyelim:
![]() |
- [6] sayfasında, [Sign Out] düğmesine tıklıyoruz;
- [7]'e geldiğimizde, URL [http://localhost:8080/login?logout] sayfasına yönlendirildiğimizi görüyoruz. Bu yönlendirmeyi Spring Security talep etmiştir. URL'te [logout] parametresinin bulunması, görünümde şu satırın görüntülenmesine neden oldu:
<div th:if="${param.logout}">You have been logged out.</div>
16.3.7. Sonuç
Önceki örnekte, önce web uygulamasını yazıp ardından güvenliğini sağlayabilirdik. Spring Security müdahaleci değildir. Zaten yazılmış bir web uygulamasının güvenliğini de sağlayabiliriz. Ayrıca, şu noktaları da keşfettik:
- bir kimlik doğrulama sayfası tanımlamak mümkündür;
- kimlik doğrulama işlemi, Spring Security tarafından verilen CSRF jetonuyla birlikte yapılmalıdır;
- kimlik doğrulama başarısız olursa, URL içinde bir error parametresi de bulunacak şekilde kimlik doğrulama sayfasına yönlendiriliriz;
- kimlik doğrulama başarılı olursa, kimlik doğrulama işlemi gerçekleştiğinde istenen sayfaya yönlendiriliriz. Ara sayfa üzerinden geçmeden doğrudan kimlik doğrulama sayfasına erişilmeye çalışılırsa, Spring Security bizi URL [/] sayfasına yönlendirir (bu durum ele alınmamıştır);
- POST ile URL [/logout] sayfasını talep ederek oturumdan çıkılır. Spring Security, URL'te logout parametresiyle bizi kimlik doğrulama sayfasına yönlendirir;
Tüm bu sonuçlar, Spring Security'nin varsayılan davranışlarına dayanmaktadır. Bu davranışlar, [WebSecurityConfigurerAdapter] sınıfındaki belirli yöntemleri yeniden tanımlayarak yapılandırma yoluyla değiştirilebilir.
Önceki öğretici, bundan sonra pek işimize yaramayacak. Aslında şunları kullanacağız:
- kullanıcıları, şifrelerini ve rollerini depolamak için bir veritabanı;
- HTTP başlık tabanlı kimlik doğrulama;
Burada yapmak istediğimiz şey için oldukça az sayıda öğretici bulunmaktadır. Önerilecek çözüm, çeşitli kaynaklardan derlenen kodların bir birleşimidir.
16.4. Ürünlerin web / json hizmetinde güvenlik ayarlarının yapılması
16.4.1. Veritabanı
[dbintrospringdata] veritabanı, kullanıcıları, şifrelerini ve rollerini dikkate alacak şekilde güncelleniyor. Üç yeni tablo ekleniyor:

[USERS] tablosu: kullanıcılar
- ID: birincil anahtar;
- VERSION: satır sürüm bilgisi sütunu;
- IDENTITY: kullanıcının tanımlayıcı kimliği;
- LOGIN: kullanıcının kullanıcı adı;
- PASSWORD: kullanıcının şifresi;
USERS tablosunda şifreler açık metin olarak depolanmaz:
![]() |
Şifreleri şifreleyen algoritma, BCRYPT algoritmasıdır.
[ROLES] tablosu: roller
- ID: birincil anahtar;
- VERSION: satırın sürüm bilgisi sütunu;
- NAME: rol adı. Varsayılan olarak, Spring Security, ROLE_XX biçiminde adlar bekler; örneğin, ROLE_ADMIN veya ROLE_GUEST;
![]() |
[USERS_ROLES] tablosu: USERS / ROLES birleştirme tablosu
Bir kullanıcı birden fazla role sahip olabilir; bir rol ise birden fazla kullanıcıyı bir araya getirebilir. [USERS_ROLES] tablosu ile somutlaştırılan çok-çok ilişkisi söz konusudur.
- ID: birincil anahtar;
- VERSION: satırın sürüm bilgisi sütunu;
- USER_ID: bir kullanıcının kimliği;
- ROLE_ID: bir rolün kimliği;
![]() |
16.4.2. Eclipse projesi
Aşağıdaki Eclipse projesini oluşturuyoruz:
1 ![]() |
- [1]: Aşağıdaki paketleri içeren yeni proje:
- [spring.security.entities]: Veritabanındaki üç yeni tabloya karşılık gelen JPA varlıklarını içerir;
- [spring.security.repositories]: üç yeni tabloyla ilişkili Spring Data'ya ait [repositories]'leri içerir;
- [spring.security.dao]: [repositories]'leri temel alan bir hizmet içerir;
- [spring.security.config]: proje yapılandırmasını ve özellikle web hizmetine güvenli erişim yapılandırmasını içerir;
- [spring.security.boot]: güvenli web hizmetinin başlatma sınıfını içerir;
16.4.3. Maven yapılandırması
Yeni proje, aşağıdaki [pom.xml] dosyası ile yapılandırılmış bir Maven projesidir:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring.security</groupId>
<artifactId>intro-spring-security-server-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-spring-security-server-01</name>
<description>démo spring security</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.webjson</groupId>
<artifactId>intro-server-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring logs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<!-- Spring Boot Testi -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- eklentiler -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- 23-27. satırlar: İncelenen web hizmeti / json arşiviyle mevcut yapı devralınır;
- 29-32. satırlar: Spring Security sınıflarını getiren bağımlılık;
- 34-37. satırlar: günlük kütüphanesi;
- 39-42. satırlar: Spring Boot anotasyonlarını kullanmaya olanak tanıyan kütüphane;
- satır 44-48: testler için gerekli kütüphane;
16.4.4. Yeni varlıklar [JPA]
![]() |
JPA katmanı üç yeni varlık tanımlar:
![]() |
[User] sınıfı, [USERS] tablosunun görüntüsüdür:
package spring.security.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import spring.data.entities.AbstractEntity;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
// özellikler
@Column(name = "NAME")
private String name;
@Column(name = "LOGIN")
private String login;
@Column(name = "PASSWORD")
private String password;
// yapıcı
public User() {
}
public User(String name, String login, String password) {
this.name = name;
this.login = login;
this.password = password;
}
// alıcı ve ayarlayıcılar
...
}
- 11. satır: Sınıf, diğer varlıklar için halihazırda kullanılan [AbstractEntity] sınıfını genişletir;
[Role] sınıfı, [ROLES] tablosunun bir kopyasıdır:
package spring.security.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import spring.data.entities.AbstractEntity;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
// özellikler
@Column(name="NAME")
private String name;
// yapıcılar
public Role() {
}
public Role(String name) {
this.name = name;
}
// alıcı ve ayarlayıcılar
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
[UserRole] sınıfı, [USERS_ROLES] tablosunun aynısıdır:
package spring.security.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import spring.data.entities.AbstractEntity;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
// dış anahtarlar
@Column(name = "USER_ID", insertable = false, updatable = false)
private Long userId;
@Column(name = "ROLE_ID", insertable = false, updatable = false)
private Long roleId;
// bir UserRole, bir User'a başvurur
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// bir UserRole bir Rol'e başvurur
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// yapıcılar
public UserRole() {
}
public UserRole(User user, Role role) {
this.user = user;
this.role = role;
}
// alıcı ve ayarlayıcılar
...
}
- 22-24. satırlar: [USERS_ROLES] tablosundan [USERS] tablosuna yabancı anahtarı oluşturur;
- 27-29. satırlar: [USERS_ROLES] tablosundan [ROLES] tablosuna yabancı anahtarı tanımlar;
16.4.5. [repositories]
![]() |
Yukarıdaki JPA varlıklarının her biri, bir [repository] Spring Data tarafından yönetilir:
![]() |
[UserRepository] arayüzü, [User] varlıklarına erişimi yönetir:
package spring.security.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import spring.security.entities.Role;
import spring.security.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// kimlik numarasıyla tanımlanan bir kullanıcının rol listesi
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// benzersiz giriş adıyla tanımlanan bir kullanıcının rol listesi
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// kullanıcı adı ile kullanıcı arama
User findUserByLogin(String login);
}
- 9. satır: [UserRepository] arayüzü, Spring Data'nın [CrudRepository] arayüzünü genişletir (4. satır);
- 12-13. satırlar: [getRoles(User user)] yöntemi, [id] ile tanımlanan bir kullanıcının tüm rollerini almayı sağlar
- 16-17. satırlar: aynı işlem, ancak kullanıcı adı ve şifre ile tanımlanan bir kullanıcı için;
- 20. satır: bir kullanıcıyı kullanıcı adı ile bulmak için;
[RoleRepository] arayüzü, [Role] varlıklarına erişimi yönetir:
package spring.security.repositories;
import org.springframework.data.repository.CrudRepository;
import spring.security.entities.Role;
public interface RoleRepository extends CrudRepository<Role, Long> {
// adına göre bir rolün aranması
Role findRoleByName(String name);
}
- 7. satır: [RoleRepository] arayüzü, [CrudRepository] arayüzünü genişletir;
- 10. satır: Bir rol, adı üzerinden aranabilir;
[UserRoleRepository] arayüzü, [UserRole] varlıklarına erişimi yönetir:
package spring.security.repositories;
import org.springframework.data.repository.CrudRepository;
import spring.security.entities.UserRole;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- satır 5: [UserRoleRepository] arayüzü, [CrudRepository] arayüzünü yeni yöntemler eklemeden genişletmekle yetinir;
16.4.6. Kullanıcı ve rol yönetimi sınıfları
![]() |
![]() |
Spring Security, aşağıdaki [UsersDetail] arayüzünü uygulayan bir sınıfın oluşturulmasını zorunlu kılar:
![]() |
Bu arayüz, burada [AppUserDetails] sınıfı tarafından uygulanmaktadır:
package spring.security.dao;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// özellikler
private User user;
private UserRepository userRepository;
// oluşturucular
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------arayüz
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// alıcı ve ayarlayıcılar
...
}
- 14. satır: [AppUserDetails] sınıfı, [UserDetails] arayüzünü uygular;
- 19-20. satırlar: Sınıf, bir kullanıcıyı (19. satır) ve bu kullanıcının ayrıntılarını elde etmeyi sağlayan depoyu (20. satır) kapsüller;
- satır 26-29: sınıfı bir kullanıcı ve onun deposu ile örneklendiren oluşturucu;
- satır 32-36: [UserDetails] arayüzünün [getAuthorities] yönteminin uygulaması. Bu yöntem, [GrantedAuthority] türünde veya türevinde öğelerden oluşan bir koleksiyon oluşturmalıdır. Burada, 19. satırdaki kullanıcı rollerinden birinin adını kapsayan türetilmiş [SimpleGrantedAuthority] türünü (36. satır) kullanıyoruz;
- 35-37. satırlar: 19. satırdaki kullanıcının rol listesi taranarak [SimpleGrantedAuthority] türünde bir öğe listesi oluşturulur;
- 42-44. satırlar: [UserDetails] arayüzünün [getPassword] yöntemini uygular. 19. satırdaki kullanıcının şifresi döndürülür;
- satır 42-44: [UserDetails] arayüzünün [getUserName] yöntemini uygular. 19. satırdaki kullanıcının kullanıcı adı döndürülür;
- 51-54. satırlar: Kullanıcının hesabı hiçbir zaman süresi dolmaz;
- 56-59. satırlar: Kullanıcının hesabı hiçbir zaman engellenmez;
- 61-64. satırlar: Kullanıcının kimlik bilgileri hiçbir zaman geçerliliğini yitirmez;
- 66-69. satırlar: kullanıcının hesabı her zaman aktiftir;
Spring Security ayrıca [AppUserDetailsService] arayüzünü uygulayan bir sınıfın varlığını zorunlu kılar:
![]() |
Bu arayüz, aşağıdaki [AppUserDetailsService] sınıfı tarafından uygulanır:
package spring.security.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// kullanıcı adıyla kullanıcı aranıyor
User user = userRepository.findUserByLogin(login);
// bulundu mu?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// kullanıcının ayrıntılarını döndürme
return new AppUserDetails(user, userRepository);
}
}
- 12. satır: Sınıf bir Spring bileşeni olacak, dolayısıyla kendi bağlamında kullanılabilir olacaktır;
- 15-16. satırlar: [UserRepository] bileşeni buraya enjekte edilecektir;
- satır 19-28: [UserDetailsService] arayüzünün (satır 10) [loadUserByUsername] yönteminin uygulanması. Parametre, kullanıcının giriş adıdır;
- satır 21: kullanıcı, kullanıcı adı aracılığıyla aranır;
- 23-25. satırlar: kullanıcı bulunamazsa bir istisna atılır;
- satır 27: bir [AppUserDetails] nesnesi oluşturulur ve işlenir. Bu nesne, [UserDetails] türündedir (satır 19);
16.4.7. Projenin yapılandırılması
![]() |
Proje iki sınıfla yapılandırılmıştır:
![]() |
[DaoConfig] sınıfı, yeni projeyle birlikte gelen [DAO] katmanını yapılandırır:
package spring.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {
// sabitler
final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
}
- 10. satır: Ürünler ve kategoriler katmanını ([DAO]) uygulayan [intro-spring-data-01] projesinden [spring.data.config.DaoConfig] yapılandırma sınıfı içe aktarılır;
- 8. satır: [repositories] Spring Data bileşenlerini içeren mevcut projenin klasörleri belirtilir;
- 9. satır: [DAO] katmanıyla ilgili Spring bileşenlerini içeren mevcut projenin klasörleri belirtilir;
- 14. satır: JPA varlıklarını içeren klasörler belirtilir. Bunlar, [intro-spring-data-01] projesine ait olanlar ve güvenli sunucu projesine ait olanlardır. Bu bilgi, 16-19. satırlardaki bean'in konusudur. Bu bean, [intro-spring-data-01] projesindeki aynı isimli bean'i yeniden tanımlar:
final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(packagesToScan());
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public String[] packagesToScan() {
return ENTITIES_PACKAGES;
}
[DAO] katmanında, 8. satır 1. satırda belirtilen klasörleri tarar. Güvenli projedeki 14-17. satırlardaki bean'in yeniden tanımlanması (16-19. satırlar) nedeniyle, yukarıdaki 8. satır artık ["spring.data.entities", "spring.security.entities"] klasörlerini tarayacaktır. [spring.security.config.DaoConfig] sınıfından 10. satırda içe aktarılan sınıfın [@Configuration] anotasyonunu içermesi gerektiği unutulmamalıdır; aksi takdirde az önce açıklanan durum çalışmaz.
[SecurityConfig] sınıfı, projenin güvenlik ayarlarını yapılandırır. Daha önce bir Spring Security yapılandırma sınıfıyla karşılaşmıştık:
package hello;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Aynı yaklaşımı izleyeceğiz:
- 11. satır: [WebSecurityConfigurerAdapter] sınıfını genişleten bir sınıf tanımlayın;
- 13. satır: Web hizmetinin çeşitli URL'lerine erişim haklarını tanımlayan [configure(HttpSecurity http)] yöntemini tanımlayın;
- 19. satır: kullanıcıları ve rollerini tanımlayan [configure(AuthenticationManagerBuilder auth)] yöntemini tanımlayalım;
[SecurityConfig] sınıfı şu şekilde olacaktır:
package spring.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import spring.security.dao.AppUserDetailsService;
@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
// güvenlik
private boolean activateSecurity = true;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// kimlik doğrulama, [appUserDetailsService] bean'i tarafından yapılır
// şifre, BCrypt hash algoritmasıyla şifrelenir
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// güvenli uygulama mı?
if (activateSecurity) {
// şifre, Authorization: Basic xxxx başlığıyla iletilir
http.httpBasic();
// HTTP OPTIONS yöntemi herkes için yetkilendirilmelidir
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// uygulamayı yalnızca ADMIN rolü kullanabilir
http.authorizeRequests() //
.antMatchers("/", "/**") // tüm URL
.hasRole("ADMIN");
// oturum yok
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- 16. satır: Spring Security bileşenlerini etkinleştirmek için;
- 17. satır: [spring.security.service] paketindeki Spring bileşenlerini ekleyin;
- 18. satır: az önce tanıttığımız [DAO] katmanındaki bean'leri ve güvenli olmayan web sunucusu / jSON'teki bean'leri içe aktarıyoruz;
- 21-22. satırlar: Uygulamanın kullanıcılarına erişim sağlayan [AppUserDetails] sınıfı enjekte edilir;
- 25. satır: Web uygulamasını güvenli hale getiren (true) veya güvenli hale getirmeyen (false) bir boole değeri;
- 27-32. satırlar: [configure(HttpSecurity http)] yöntemi, kullanıcıları ve rollerini tanımlar. Bu yöntem, parametre olarak bir [AuthenticationManagerBuilder] türü alır. Bu parametre, iki bilgiyle zenginleştirilir (38. satır):
- 22. satırdaki [appUserDetailsService] hizmetine bir referans; bu, kayıtlı kullanıcılara erişim sağlar. Burada, kullanıcıların bir veritabanında kayıtlı oldukları bilgisinin görünmediğine dikkat edilmelidir. Dolayısıyla, bunlar bir önbellekte bulunabilir, bir web hizmeti tarafından sağlanabilir, ...
- şifre için kullanılan şifreleme türü. Burada, BCrypt algoritmasını kullandığımızı hatırlatırız;
- 34-52. satırlar: [configure(HttpSecurity http)] yöntemi, web hizmetinin URL'lerine erişim haklarını tanımlar;
- 37. satır: Giriş projesinde gördüğümüz gibi, Spring Security varsayılan olarak bir CSRF jetonu (Cross Site Request Forgery) yönetir ve kimlik doğrulaması yapmak isteyen kullanıcının bu jetonu sunucuya geri göndermesi gerekir. Burada bu mekanizma devre dışı bırakılmıştır. Bu ayar, boole değeri (isSecured=false) ile birleştiğinde, web uygulamasının güvenlik olmadan kullanılmasına olanak tanır;
- 41. satır: HTTP başlığıyla kimlik doğrulama modu etkinleştirilir. İstemci, aşağıdaki HTTP başlığını göndermelidir:
burada code, "login:password" dizesinin Base64 algoritmasıyla kodlanmış halidir. Örneğin, admin:admin dizesinin Base64 kodlaması YWRtaW46YWRtaW4= şeklindedir. Dolayısıyla, kullanıcı adı [admin] ve şifre [admin] olan kullanıcı, kimlik doğrulaması için aşağıdaki HTTP başlığını gönderecektir:
- 46-48. satırlar: Web hizmetindeki tüm URL öğelerinin, [ROLE_ADMIN] rolüne sahip kullanıcılar tarafından erişilebilir olduğunu belirtir. Bu, bu role sahip olmayan bir kullanıcının web hizmetine erişemeyeceği anlamına gelir;
- 50. satır: [session] modunda, bir kez kimlik doğrulaması yapmış bir kullanıcının sonraki erişimlerinde kimlik doğrulaması yapması gerekmez. Burada bu mod devre dışı bırakıldığından, kullanıcı her erişimde kimlik doğrulaması yapmak zorunda kalacaktır;
16.4.8. [DAO] katmanının testleri
![]() |
![]() |
Öncelikle, bir rol atamalı kullanıcı oluşturabilen [CreateUser] adlı bir yürütülebilir sınıf oluşturuyoruz:
package sprin.security.tests;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// sözdizimi: kullanıcı adı şifre roleName
// üç parametre gereklidir
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parametreleri alıyoruz
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// Spring bağlamı
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// rol zaten mevcut mu?
Role role = roleRepository.findRoleByName(roleName);
// mevcut değilse oluşturulur
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// kullanıcı zaten mevcut mu?
User user = userRepository.findUserByLogin(login);
// yoksa oluşturulur
if (user == null) {
// şifreyi bcrypt ile hash'liyoruz
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// kullanıcı kaydedilir
user = userRepository.save(new User(login, login, crypt));
// rol ile ilişki oluşturulur
userRoleRepository.save(new UserRole(user, role));
} else {
// kullanıcı zaten mevcutsa, istenen role sahip mi?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// bulunmazsa, rol ile ilişki oluşturulur
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// Spring bağlamı kapatılıyor
context.close();
// son
System.out.println("Travail terminé...");
}
}
- 17. satır: Sınıf, bir kullanıcıyı tanımlayan üç argüman bekler: kullanıcı adı, şifre ve rol;
- 25-27. satırlar: Bu üç parametre alınır;
- 29. satır: Spring bağlamı, [AppConfig] yapılandırma sınıfından oluşturulur;
- satır 30-32: kullanıcıyı oluşturmak için bize faydalı olabilecek üç [Repository] referansı alınır;
- satır 34: rolün zaten mevcut olup olmadığına bakılır;
- satır 36-38: mevcut değilse, veritabanında oluşturulur. Adı [ROLE_XX] şeklinde olacaktır;
- 40. satır: kullanıcı adı zaten mevcut mu diye kontrol edilir;
- satır 42-49: kullanıcı adı yoksa, veritabanında oluşturulur;
- 44. satır: Şifre şifrelenir. Burada Spring Security'nin [BCrypt] sınıfı kullanılır (4. satır). Dolayısıyla bu çerçevenin arşivlerine ihtiyaç vardır. [pom.xml] dosyası şu bağımlılığı içerir:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 46. satır: Kullanıcı veritabanına kaydedilir;
- 48. satır: kullanıcıyı rolüne bağlayan ilişki de veritabanına kaydedilir;
- satır 51-57: kullanıcı zaten mevcutsa – bu durumda, atamak istediğimiz rolün mevcut roller arasında olup olmadığına bakılır;
- satır 59-61: aranan rol bulunamazsa, kullanıcıyı rolüyle ilişkilendirmek için [USERS_ROLES] tablosunda bir satır oluşturulur;
- Olası istisnalara karşı herhangi bir önlem alınmamıştır. Bu, bir kullanıcıyı bir rolle hızlı bir şekilde oluşturmak için kullanılan bir destek sınıfıdır.
Sınıf, [x x guest] argümanlarıyla çalıştırıldığında, veritabanında aşağıdaki sonuçlar elde edilir:
[USERS] tablosu
![]() |
[ROLES] tablosu
![]() |
[USERS_ROLES] tablosu
![]() |
Şimdi, JUnit testini oluşturan ikinci sınıf [UsersTest]'i ele alalım:
![]() |
package spring.security.tests;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
// jSON eşleştiricisi
private ObjectMapper mapper = new ObjectMapper();
@Test
public void findAllUsersWithTheirRoles() throws JsonProcessingException {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// kullanıcıyı alıyoruz [admin]
User user = userRepository.findUserByLogin("admin");
// şifresinin [admin] olup olmadığını kontrol ediyoruz
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// admin / admin rolünü kontrol ediyoruz
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// [admin] kullanıcısını alıyoruz
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// şifresinin [admin] olup olmadığı kontrol edilir
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// admin / admin rolünü kontrol ediyoruz
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// yardımcı yöntem - bir koleksiyondaki öğeleri görüntüler
private void display(String message, Iterable<?> elements) throws JsonProcessingException {
System.out.println(message);
for (Object element : elements) {
System.out.println(mapper.writeValueAsString(element));
}
}
}
- 37-44. satırlar: görsel test. Tüm kullanıcılar rolleriyle birlikte görüntülenir;
- 46-56. satırlar: [UserRepository] deposunu kullanarak [admin] kullanıcısının [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğu kontrol edilir;
- 51. satır: [admin], şifrelenmemiş şifredir. Veritabanında, BCrypt algoritmasına göre şifrelenmiştir. [BCrypt.checkpw] yöntemi, şifrelenmiş açık metin şifresinin veritabanındaki şifreyle tam olarak eşleşip eşleşmediğini doğrulamaya yarar;
- 58-69. satırlar: [admin] kullanıcısının [admin] şifresine ve [ROLE_ADMIN] rolüne sahip olduğu, [appUserDetailsService] hizmeti kullanılarak doğrulanır;
Testler aşağıdaki günlük kayıtlarıyla başarıyla tamamlanır:
16.4.9. Web hizmeti testleri
Web hizmetini [Advanced Rest Client] Chrome istemcisiyle test edeceğiz. Kimlik doğrulama için HTTP başlığını belirtmemiz gerekecek:
burada [code], [login:password] dizesinin Base64 kodudur. Bu kodu oluşturmak için aşağıdaki programı kullanabilirsiniz:
![]() |
package spring.security.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// iki argüman beklenir: kullanıcı adı ve şifre
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// iki argümanı alınır
String chaîne = String.format("%s:%s", args[0], args[1]);
// dizgiyi kodluyor
byte[] data = Base64.encode(chaîne.getBytes());
// Base64 kodlamasını görüntüler
System.out.println(new String(data));
}
}
Bu programı [admin admin] argümanlarıyla çalıştırırsak:
![]() |
şu sonucu elde ederiz:
Artık HTTP kimlik doğrulama başlığını oluşturmayı öğrendiğimize göre, güvenli web hizmetini başlatıyoruz ve ardından Chrome istemcisi [Advanced Rest Client] ile tüm ürünlerin listesini istiyoruz:
![]() |
- [1] ile kategorilerin listesini talep ediyoruz;
- [2] adresinde, GET yöntemini kullanarak;
- [3]'te, kimlik doğrulama başlığı olarak HTTP'i veriyoruz. [YWRtaW46YWRtaW4=] kodu, [admin:admin] dizesinin Base64 kodlamasıdır;
- [4]'te, HTTP komutunu gönderiyoruz;
Sunucunun yanıtı şöyledir:
![]() |
- [1]'te, HTTP kimlik doğrulama başlığı;
- [2]'te, sunucu jSON yanıtını geri gönderir;
Kategori listesi başarıyla alınır:
![]() |
Şimdi, hatalı bir kimlik doğrulama başlığıyla HTTP isteği göndermeyi deneyelim. Bu durumda yanıt şu şekildedir:
![]() |
- [1]'te: HTTP kimlik doğrulama başlığı;
Aşağıdaki yanıtı alıyoruz:
![]() |
- [2]: web hizmetinin yanıtı;
Şimdi user / user kullanıcısını deneyelim. Bu kullanıcı mevcut ancak web hizmetine erişimi yok. Base64 kodlama programını [user user] argümanlarıyla çalıştırırsak:
![]() |
şu sonucu elde ederiz:
![]() |
- [1]'te: hatalı HTTP kimlik doğrulama başlığı;
![]() |
- [2]: web hizmetinin yanıtı. Bu, önceki [401 Unauthorized] yanıtından farklıdır. Bu sefer kullanıcı doğru bir şekilde kimlik doğrulaması yapmıştır ancak URL'e erişmek için yeterli haklara sahip değildir;
Güvenli web hizmetimiz artık çalışır durumda.
16.4.10. Bir URL kimlik doğrulama
![]() |
Bir kullanıcının web hizmetine erişim izni olup olmadığını belirlememizi sağlayacak bir URL oluşturacağız. Bunun için aşağıdaki gibi yeni bir denetleyici oluşturuyoruz: MVC [AuthenticateController]
package spring.security.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.webjson.models.Response;
@Controller
public class AuthenticateController {
// Spring bağımlılıkları
@Autowired
private ApplicationContext context;
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {
// yanıt jSON
ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
}
}
- 15. satır: [AuthenticateController] sınıfı bir Spring denetleyicisidir. Bu nedenle URL'i ortaya koyar;
- 22. satır: URL'i [/authenticate]'e sunar;
- 23. satır: Yöntemin sonucu doğrudan istemciye gönderilecektir;
- 26-27. satırlar: Yöntem, boş bir [Response] nesnesi döndürmekle yetinir, ancak [status] değeri 0'dır; bu da hata olmadığını gösterir;
Bu URL ne işe yarar? Bir kullanıcıyı doğrulamak istediğimizde bunu talep edeceğiz. Güvenlik katmanı bu kullanıcıyı kabul etmezse bir istisna döndürdüğünü gördük. İşte bir örnek;
[admin:admin] kullanıcısıyla:
![]() | ![]() |
Boş bir yanıt alıyoruz, ancak istisna oluşmuyor.
[user:user] kullanıcısıyla:
![]() | ![]() |
Bir istisna oluştu.
16.4.11. Sonuç
Spring Security için gerekli sınıfların eklenmesi, orijinal web / json projesinde herhangi bir değişiklik yapılmadan gerçekleştirilebildi. Bu çok elverişli durum, veritabanına eklenen üç tablonun mevcut tablolardan bağımsız olmasından kaynaklanmaktadır. Hatta bunları ayrı bir veritabanına bile yerleştirebilirdik. Diğer durumlarda, eklenen tablolar mevcut tablolarla ilişkili olabilir. Bu durumda JPA varlıklarını değiştirmek gerekir ve bu genellikle projenin tüm katmanlarını etkiler.
16.5. Güvenli web hizmeti / jSON için programlanmış bir istemci
Güvenli olmayan web hizmeti / jSON için daha önce bir istemci yazmıştık:
![]() |
Şimdi güvenli web hizmeti için programlanmış bir istemci oluşturacağız:
![]() |
Daha önce yazdığımız [intro-webjson-client] projesini yeni bir proje olan [intro-spring-security-client-01]'e kopyalıyoruz:
![]() |
16.5.1. [AbstractDao] sınıfı
[AbstractDao] sınıfı, HTTP ile güvenli web sunucusu / jSON arasındaki iletişimi sağlar. Az önce gördüğümüz gibi, bu HTTP iletişiminde, istemci artık bir kimlik doğrulama başlığı göndermelidir; örneğin:
Bu işlem şu şekilde gerçekleştirilir:
package spring.security.client.dao;
import java.net.URI;
...
public abstract class AbstractDao {
// veriler
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// genel istek
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL ile iletişime geçin
- 15. satır: Güvenli web hizmeti ile HTTP iletişimini yürüten genel [getResponse] yöntemi, artık ilk parametre olarak URL talep eden kullanıcıyı kabul etmektedir. [User] sınıfı şu şekildedir:
Bu sınıf şu şekildedir:
![]() |
package spring.security.client.entities;
public class User {
// özellikler
private String login;
private String password;
// yapıcı
public User() {
}
public User(String login, String password) {
this.login = login;
this.password = password;
}
// alıcı ve ayarlayıcılar
...
}
Böylece [getResponse] yöntemi şu şekilde olur:
package spring.security.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;
import spring.security.client.entities.User;
public abstract class AbstractDao {
// veriler
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
private String getBase64(User user) {
// kullanıcı adı ve şifreyi base64 ile kodlar - java 8 gerektirir
String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// genel istek
protected String getResponse(User user, String url, String jsonPost) {
// URL: URL ile iletişime geçin
// jsonPost: jSON değeri gönderilmeli
try {
// isteğin yürütülmesi
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// istek yürütülüyor
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- 42-44, 49-51. satırlar: [user] kullanıcısı null değilse, kimlik doğrulama başlığı eklenir. Kullanıcının ve şifresinin Base64 şifrelenmesi, 25-29. satırlardaki [getBase64] yöntemi ile sağlanır. Bu yöntemin, JDK 1.8'e ait [Base64] sınıfını kullandığına dikkat edilmelidir.
- Yukarıdaki satırlar dışında kodda herhangi bir değişiklik yoktur;
16.5.2. [IDao] arayüzü
[IDao] arayüzündeki tüm yöntemler, [User user] adlı ek bir parametre alır:
![]() |
package spring.security.client.dao;
import java.util.List;
import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;
public interface IDaoClient {
// kimlik doğrulama
public void authenticate(User user);
// ürün listesinin eklenmesi
public List<Produit> addProduits(User user, List<Produit> produits);
// tüm ürünlerin silinmesi
public void deleteAllProduits(User user);
// ürün listesinin güncellenmesi
public List<Produit> updateProduits(User user, List<Produit> produits);
// tüm ürünleri alma
public List<Produit> getAllProduits(User user);
// kategori listesi ekleme
public List<Categorie> addCategories(User user, List<Categorie> categories);
// tüm kategorilerin silinmesi
public void deleteAllCategories(User user);
// kategori listesinin güncellenmesi
public List<Categorie> updateCategories(User user, List<Categorie> categories);
// tüm kategorileri alma
public List<Categorie> getAllCategories(User user);
// belirli bir ürün
public Produit getProduitByIdWithCategorie(User user, Long idProduit);
public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);
public Produit getProduitByNameWithCategorie(User user, String nom);
public Produit getProduitByNameWithoutCategorie(User user, String nom);
// belirli bir kategori
public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);
public Categorie getCategorieByNameWithProduits(User user, String nom);
public Categorie getCategorieByNameWithoutProduits(User user, String nom);
}
- 12. satır: Bir kullanıcıyı doğrulamak için [authenticate(User user)] yöntemini ekledik. Kullanıcı, web hizmetinin URL ve [/authenticate] yöntemlerine erişim iznine sahip değilse, bu yöntem bir istisna oluşturur;
16.5.3. [Dao] sınıfı
[Dao] sınıfındaki tüm yöntemler, [User user] adlı ek bir parametre alır ve bunu [AbstractDao] sınıfındaki [getResponse] genel yöntemine aktarır. İşte iki örnek:
// kimlik doğrulama
@Override
public void authenticate(User user) {
getResponse(user, "/authenticate", null);
}
@Override
public List<Produit> addProduits(User user, List<Produit> produits) {
// ----------- ürün ekleme (kategorileri olmadan)
try {
// eşleştiriciler jSON
ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
ObjectMapper mapperResponse = mapperPost;
// istek
Response<List<Produit>> response = mapperResponse.readValue(
getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
new TypeReference<Response<List<Produit>>>() {
});
// hata mı?
if (response.getStatus() != 0) {
// 1 istisna atılıyor
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// sunucu yanıtının göbeği döndürülüyor
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (IOException | RuntimeException e2) {
throw new DaoException(100, e2);
}
}
16.5.4. [Dao] sınıfının birim testleri
[Dao] sınıfının birim testlerini içeren [Test01] sınıfı şu şekilde değiştirilir:
![]() |
package client.tests.junit;
...
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// Spring bağlamı
@Autowired
private ApplicationContext context;
// [DAO] katmanı
@Autowired
private IDaoClient dao;
// kullanıcılar
static private User admin;
static private User user;
static private User unknown;
@BeforeClass
public static void init() {
admin = new User("admin", "admin");
user = new User("user", "user");
unknown = new User("x", "y");
}
@Before
public void cleanAndFill() {
// her testten önce veritabanını temizliyoruz
log("Vidage de la base de données", 1);
// [CATEGORIES] tablosu boşaltılır - bu işlem, [PRODUITS] tablosunun da boşaltılmasına yol açar
dao.deleteAllCategories(admin);
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// tablolar doldurulur
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// kategori eklenir - zincirleme olarak ürünler de eklenecektir
dao.addCategories(admin, categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// kategori listesi
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories(admin);
affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
// ürün listesi
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits(admin);
affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
// bazı kontroller
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
...
@Test()
public void checkUserUser() {
ServiceException se = null;
try {
dao.authenticate(user);
} catch (ServiceException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
}
@Test()
public void checkUserUnknown() {
ServiceException se = null;
try {
dao.authenticate(unknown);
} catch (ServiceException e) {
se = e;
}
Assert.assertNotNull(se);
Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
}
@Test()
public void checkUserAdmin() {
ServiceException se = null;
try {
dao.authenticate(admin);
} catch (ServiceException e) {
se = e;
}
Assert.assertNull(se);
}
...
}
- Test sınıfının başlatılması sırasında (21-26. satırlar), üç kullanıcı oluşturulur:
- [admin] kullanıcısı, web hizmetinin URL'lerine erişebilir (96-104. satırlar);
- [user] kullanıcısı mevcut ancak web hizmetindeki URL'i kullanma yetkisi yok, test satır 71-81;
- [unknown] kullanıcısı mevcut değildir, 83-93. satırları test edin;
- test yöntemleri, güvenli olmayan web hizmeti için daha önce görülenlerle aynıdır; tek fark, [IDaoClient] arayüzündeki yöntemlerin, URL'i kullanma hakkına sahip olan [admin] kullanıcısını ilk parametre olarak alarak çağrılmasıdır;
Test başarılı oluyor ancak güvenli olmayan web hizmetine kıyasla daha yavaş olduğu görülüyor. Bir uygulamanın güvenli hale getirilmesi, yanıt sürelerini önemli ölçüde artırıyor. Güvenli web hizmetinin performansında dikkate değer bir unsur göze çarpıyor: Hizmeti yapılandıran [AppConfig] sınıfında şunu yazmıştık:
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// güvenli uygulama mı?
if (activateSecurity) {
// şifre, Authorization: Basic xxxx başlığıyla iletilir
http.httpBasic();
// HTTP OPTIONS yöntemi herkes için yetkilendirilmelidir
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// uygulamayı yalnızca ADMIN rolü kullanabilir
http.authorizeRequests() //
.antMatchers("/", "/**") // tüm URL
.hasRole("ADMIN");
// oturum yok
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
17. satırın bir maliyeti var. Bu satır, kullanıcının her erişimde kimlik doğrulaması yapmasını zorunlu kılıyor. Bu satırı yorum satırına alırsak, önceki JUnit testinin süresi 10,57 saniyeden 4,21 saniyeye düşüyor; çünkü [admin] kullanıcısı yalnızca ilk test için kimlik doğrulaması yapar, sonraki testler için yapmaz (istemci tarafından HTTP kimlik doğrulama başlığı gönderilse bile, sunucu kullanıcının şifresini yeniden doğrulamaz). Güvenli olmayan bir web hizmetinde, JUnit testinin süresi 2,33 saniyeye düşer.
























































