Spring security, güvenlik için bir framework tanımlar ve Servlet filterlarını kullanarak arka planda süreci yürütür. 2 tipi vardır.
- Declarative Security
- Programmatic Security
Authentication ve Authorization
Authentication : Kullanıcının username ve passwordünü veritabanında yada appte kontrol etme sürecidir.
Authorization : Kullanıcı giriş yaptıktan sonraki role (rolünü) kontrol etme sürecidir.
Declarative ve Programmatic Security
- Declarative Security
- Uygulamanın güvenlik kısıtlamalarını @Configuration annoitationa sahip olan Java config dosyasında yazdığımız durumdur. Business logic'ten ayrı, merkezi bir yerde toplama sürecidir.
- Binanın girişindeki güvenlik görevlisi olarak düşünebilirsiniz. Güvenlik görevlisi amcamız elindeki listeye bakarak (SecurityFilterChain) ve kimin (Role) hangi kata (URL) çıkabileceğine daha kapıdayken karar verir. Odaların (@RestController) içindeki insanların bu kontrolden haberi bile yoktur. Bu süreç @Configuration annotationuyla işaretlenmiş ve içerisinde SecurityFilterChain methoduna sahip yöntemle yapılır
- Programmatic Security
- Güvenlik kontrolünü, merkezi bir yerine doğrudan business logic içinde, yani metodlarımızın içerisinde kod yazarak (if-else) yapmaktır. Örneğin yukarıdaki gibi bir declarative security'i geçtik ama odaya (@RestController) girik ve ancak odanın içindeki görevli (@Service metodu), bizden ekstra bir güvenlik kartı (Authenticaton nesnesi) ister. Bu anlık ve duruma özel bir kontroldür.
Bazı URL bazlı kurallar yetmediğinde kullanılır örneğin sipariş detayı için kullanıcı sadece kendi siparişlerini görebilsin diğerlerinkini değil (/orders/16).
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>Passwordlar specific bir formatta depolanır.
{ID}encodedPassword şeklinde.
ID
{noop} : Plain text password
{bcrypt} : Bcrypt password hashing
REST API Security - Temel Configuration
Kullanıcılarımızı InMemoryUserDetailsManager kullanarak yani bellekte depolayalım. Eğer ki biz application.properties'e kullanıcılarımız vermişsek aşağıdaki gibi bunlar artık geçersiz olur.
spring.security.user.name=kerem
spring.security.user.password=kerem123@Configuration
public class AppSecurity {
@Bean
public InMemoryUserDetailsManager userDetailsManager(){
UserDetails minterStudent = User.builder()
.username("minter")
.password("{noop}minter123")
.roles("STUDENT")
.build();
UserDetails coderStudent = User.builder()
.username("coder")
.password("{noop}coder123")
.roles("STUDENT")
.build();
return new InMemoryUserDetailsManager(minterStudent,coderStudent);
}
}Role tabanlı olarak Kullanıcıların gideceği URL'leri Kısıtlama ve SecurityFilterChain Kullanma
- Genel syntax:
requestMatchers(<<eşleşecek tek path>>)
.hasRole(<<yetkili tek bir rol>>)- HTTP Method ile Kısıtlama
requestMatchers(<<http method>>,<<eşleşecek tek path>>)
.hasRole(<<yetkili tek bir rol>>)- Herhangi bir role sahip ise
requestMatches(<<http method>>,<<eşleşecek path>>)
.hasAnyRole(<<yetkili rollerin listesi>>)Örneğin
/api/students Endpoint Güvenlik Kuralları (Ayrıntılı)
Aşağıdaki tablo, student kaynakları üzerindeki CRUD işlemlerini ve bu kuralları SecurityFilterChain içinde nasıl tanımlayacağımızı özetlemektedir.
| Eylem | HTTP Metodu | URL Pattern | Gerekli Rol | requestMatchers Kodu (Java) |
| Tüm öğrencileri listele | GET | /api/students | STUDENT | .requestMatchers(HttpMethod.GET, "/api/students").hasRole("STUDENT") |
| Tek bir öğrenciyi gör | GET | /api/students/{ID} | STUDENT | .requestMatchers(HttpMethod.GET, "/api/students/**").hasRole("STUDENT") |
| Yeni öğrenci oluştur | POST | /api/students | TEACHER | .requestMatchers(HttpMethod.POST, "/api/students").hasRole("TEACHER") |
| Öğrenciyi güncelle | PUT | /api/students/{ID} | TEACHER | .requestMatchers(HttpMethod.PUT, "/api/students/**").hasRole("TEACHER") |
| Öğrenciyi sil | DELETE | /api/students/{ID} | ADMIN | .requestMatchers(HttpMethod.DELETE, "/api/students/**").hasRole("ADMIN") |
Önemli Not: /** Wildcard'ı Nasıl Çalışır?
Bu tabloda kafa karıştırıcı olabilecek en önemli nokta /** (wildcard) kullanımıdır.
requestMatchers(HttpMethod.GET, "/api/students/**") kuralı:
- /api/students/123 (Tek öğrenci)
- /api/students/123/grades (Öğrencinin notları)
- ve aynı zamanda /api/students (Tüm öğrenciler)
yollarının hepsiyle eşleşir.
Bu yüzden, SecurityFilterChain kodunuzu yazarken ilk iki GET kuralını tek bir satırda birleştirmek en iyi yöntemdir:
// Bu tek satır, hem /api/students hem de /api/students/123 için
// GET isteklerini kapsar.
.requestMatchers(HttpMethod.GET, "/api/students/**").hasRole("STUDENT")
Şimdi de Basic authentication ile kodlayabiliriz.
@Configuration
public class AppSecurity {
@Bean
public InMemoryUserDetailsManager userDetailsManager(){
UserDetails minterStudent = User.builder()
.username("minter")
.password("{noop}minter123")
.roles("STUDENT")
.build();
UserDetails coderStudent = User.builder()
.username("coder")
.password("{noop}coder123")
.roles("STUDENT")
.build();
return new InMemoryUserDetailsManager(minterStudent,coderStudent);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(configurer -> configurer
.requestMatchers(HttpMethod.GET,"/api/students").hasRole("STUDENT")
.requestMatchers(HttpMethod.GET,"/api/students/**").hasRole("STUDENT")
.requestMatchers(HttpMethod.POST,"/api/students").hasRole("TEACHER")
.requestMatchers(HttpMethod.PUT,"/api/students/**").hasRole("TEACHER")
.requestMatchers(HttpMethod.DELETE,"/api/students/**").hasRole("ADMIN")
);
http.httpBasic(Customizer.withDefaults());
return http.build();
}
}CSRF (Cross-Site Request Forgery)
Spring Securtiy bizi CSRF ataklarına karşı da koruyabiliriz.
Spring Security Ekibi:
- CSRF korumasını sadece normal browserin isteklerinde bırakılmasını söylemiştir.
- Ekstra geleneksel eski web uygulamalarının HTML formlarıyla data'yı ekleme ve modify edilmesinde kullanılmasını söylemiştir.
Eğer ki REST API yapıyorsak non-browser client için csrf protectionu disable edebiliriz.
http.csrf(csrf -> csrf.disable());REST API Security - JDBC Configuration
Spring Security kullanıcı bilgilerini veritabanından da okuyabilir.
pom.xml'ye
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>spring.datasource.url=jdbc:mysql://localhost:3306/DB_NAME
spring.datasource.username=username
spring.datasource.password=passwordusers için SQL kodları
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` varchar(68) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY (`username`)
);authorities için SQL kodları
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`, `authority`),
CONSTRAINT `authorities_ibfk_1`
FOREIGN KEY (`username`)
REFERENCES `users` (`username`)
);Unutulmaması gereken ÖNEMLİ ÖZELLİK!
Role eklemesi yaparken
Spring Security authorities eklemesinde "ROLE_" prefixini zorunlu tutmuştur yani.
ROLE_STUDENT
ROLE_TEACHER
ROLE_ADMIN
olmalıdır.
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class AppSecurity {
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource){
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.authorizeHttpRequests(configurer -> configurer
.requestMatchers(HttpMethod.GET,"/api/students").hasRole("STUDENT")
.requestMatchers(HttpMethod.GET,"/api/students/**").hasRole("STUDENT")
.requestMatchers(HttpMethod.POST,"/api/students").hasRole("TEACHER")
.requestMatchers(HttpMethod.PUT,"/api/students/**").hasRole("TEACHER")
.requestMatchers(HttpMethod.DELETE,"/api/students/**").hasRole("ADMIN")
);
http.httpBasic(Customizer.withDefaults());
http.csrf(csrf -> csrf.disable());
return http.build();
}
}Veritabanına en sağlam parolaları bcrypt ile encrypted formatta eklemektir.
bcrypt:
- One-way encrypted hashing yöntemini uygular.
- Parolaya random bir salt ekler ekstra koruma için.
Veritabanına eklenirken: {bcrypt}$2........ şeklinde eklenir.
{bcrypt} -> 8 karakter
encodedPass -> 60 karakter toplamda password sütunu en azından 68 karakter olmalıdır.