@PreAuthorize, @PostAuthorize, @Secured
URL bazlı güvenlik (HttpSecurity) coarse-grained (kaba taneli) kontrol sağlar: /admin/** gibi URL kalıplarıyla erişimi kısıtlarsınız ama "bu kullanıcı sadece kendi profilini düzenleyebilir" gibi iş kurallarını URL kalıplarıyla ifade edemezsiniz. Method security ise service katmanında fine-grained (ince taneli) yetkilendirme sunar — her method çağrısı için ayrı güvenlik kuralı tanımlayabilirsiniz.
Method Security vs URL-Based Security
| Özellik | URL-Based (HttpSecurity) | Method Security |
|---|---|---|
| Seviye | HTTP endpoint | Java method |
| Granülerlik | Kaba (URL pattern) | İnce (method + parametreler) |
| İş kuralları | Zor | SpEL ile kolay |
| Nerede tanımlanır | SecurityConfig | Service sınıfları üzerinde |
| Tavsiye | İlk savunma hattı | İş kuralı gerektiren yerler |
En iyi yaklaşım her ikisini birlikte kullanmaktır: URL bazlı güvenlik ilk katman olarak genel erişim kontrolü sağlar, method security ise iş mantığına özel kurallar uygular.
Etkinleştirme
@Configuration
@EnableMethodSecurity // Spring Boot 3+ (eski: @EnableGlobalMethodSecurity)
public class SecurityConfig {
// prePostEnabled = true (default) — @PreAuthorize, @PostAuthorize
// securedEnabled = false (default) — @Secured aktif etmek için true yapın
// jsr250Enabled = false (default) — @RolesAllowed aktif etmek için true yapın
}
// Tüm özellikleri açık hali:
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)@PreAuthorize — Method Öncesi Kontrol
@PreAuthorize method çağrılmadan önce SpEL (Spring Expression Language) ifadesini değerlendirir. İfade false dönerse AccessDeniedException fırlatılır ve method hiç çalışmaz.
@Service
public class UserService {
// Sadece ADMIN rolüne sahip kullanıcılar çağırabilir
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// ADMIN veya kendi profilini görüntüleyen kullanıcı
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public User getUser(Long id) {
return userRepository.findById(id).orElseThrow();
}
// Custom bean referansı — karmaşık iş kuralları için
@PreAuthorize("@securityService.isOwner(#postId, authentication.principal.id)")
public void editPost(Long postId, String content) {
// Sadece post sahibi düzenleyebilir
}
// Birden fazla koşul kombine etme
@PreAuthorize("hasAuthority('USER_CREATE') and #dto.departmentId == authentication.principal.departmentId")
public User createUser(CreateUserDto dto) {
return userRepository.save(dto.toEntity());
}
}SpEL (Spring Expression Language) İfadeleri
Method security'nin gücü SpEL'den gelir. Kullanabileceğiniz ifadeler:
// ── Rol ve Authority kontrolleri ──
@PreAuthorize("hasRole('ADMIN')") // ROLE_ADMIN arar
@PreAuthorize("hasAnyRole('ADMIN', 'EDITOR')") // Birden fazla rol
@PreAuthorize("hasAuthority('USER_DELETE')") // Doğrudan authority
@PreAuthorize("hasAnyAuthority('READ', 'WRITE')") // Birden fazla authority
// ── Authentication bilgilerine erişim ──
@PreAuthorize("authentication.name == #username") // Mevcut kullanıcı adı
@PreAuthorize("#id == authentication.principal.id") // Principal'dan ID
// ── Method parametrelerine erişim (#paramName) ──
@PreAuthorize("#email == authentication.name")
public User findByEmail(String email) { ... }
// ── Mantıksal operatörler ──
@PreAuthorize("hasRole('ADMIN') or (#id == authentication.principal.id)")
@PreAuthorize("hasRole('ADMIN') and hasAuthority('USER_DELETE')")
@PreAuthorize("!hasRole('BLOCKED')") // Negatif kontrol
// ── Custom bean referansı (@beanName.method()) ──
@PreAuthorize("@rateLimiter.isAllowed(authentication.name)")
@PreAuthorize("@aclService.canAccess(#resourceId, 'WRITE')")
// ── permitAll / denyAll ──
@PreAuthorize("permitAll()") // Herkese açık
@PreAuthorize("denyAll()") // Kimseye kapalı (devre dışı bırakma)Custom Security Expression Bean
Karmaşık iş kuralları için özel bean oluşturun:
@Component("securityService")
public class SecurityService {
private final PostRepository postRepository;
public SecurityService(PostRepository postRepository) {
this.postRepository = postRepository;
}
// Post sahibi mi?
public boolean isOwner(Long postId, Long userId) {
return postRepository.findById(postId)
.map(post -> post.getAuthor().getId().equals(userId))
.orElse(false);
}
// Aynı departmanda mı?
public boolean sameDepartment(Long targetUserId, Authentication auth) {
CustomUserDetails principal = (CustomUserDetails) auth.getPrincipal();
User target = userRepository.findById(targetUserId).orElseThrow();
return principal.getDepartmentId().equals(target.getDepartmentId());
}
}
// Kullanım
@PreAuthorize("@securityService.isOwner(#postId, authentication.principal.id)")
public void updatePost(Long postId, UpdatePostDto dto) { ... }
@PreAuthorize("hasRole('MANAGER') and @securityService.sameDepartment(#userId, authentication)")
public EmployeeDetails getEmployeeDetails(Long userId) { ... }@PostAuthorize — Method Sonrası Kontrol
@PostAuthorize method çalıştıktan sonra dönen değeri kontrol eder. returnObject ile method'un return değerine erişilir. Kontrol başarısız olursa AccessDeniedException fırlatılır ama method çoktan çalışmıştır (side-effect'lere dikkat!).
// Kullanıcı sadece kendi dokümanını görebilir
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) {
return documentRepository.findById(id).orElseThrow();
// Dönen objenin owner'ı mevcut kullanıcı değilse 403 döner
}
// ADMIN her şeyi görebilir, diğerleri sadece kendi departmanını
@PostAuthorize("hasRole('ADMIN') or returnObject.departmentId == authentication.principal.departmentId")
public Report getReport(Long id) {
return reportRepository.findById(id).orElseThrow();
}⚠️ Dikkat:
@PostAuthorizemethod çalıştıktan sonra kontrol yapar. Eğer method'da DELETE, UPDATE gibi side-effect varsa, kontrol başarısız olsa bile işlem geri alınmaz. Side-effect'li operasyonlarda@PreAuthorizetercih edin.
@Secured ve @RolesAllowed
@Secured (Spring'e özel) ve @RolesAllowed (JSR-250 standardı) daha basit alternatiflerdir — SpEL desteklemezler:
// @Secured — Spring'e özel, SpEL yok
@Secured("ROLE_ADMIN")
public void deleteAll() { ... }
@Secured({"ROLE_ADMIN", "ROLE_EDITOR"}) // OR mantığı
public void publishArticle(Long id) { ... }
// @RolesAllowed — Jakarta/JSR-250 standardı, taşınabilir
@RolesAllowed("ADMIN") // ROLE_ prefix otomatik eklenir
public void adminOnly() { ... }Karşılaştırma:
| Annotation | SpEL | Parametrelere Erişim | returnObject | Standart |
|---|---|---|---|---|
@PreAuthorize | ✅ | ✅ (#param) | ❌ | Spring |
@PostAuthorize | ✅ | ✅ | ✅ | Spring |
@Secured | ❌ | ❌ | ❌ | Spring |
@RolesAllowed | ❌ | ❌ | ❌ | JSR-250 |
Tavsiye: Hemen her zaman @PreAuthorize kullanın — en esnek ve güçlü olanıdır. @Secured/@RolesAllowed sadece basit rol kontrolü yeterli olduğunda tercih edilir.
@PreFilter ve @PostFilter — Koleksiyon Filtreleme
@PreFilter method'a gelen koleksiyondan (List, Set, Array) izinsiz elemanları çıkarır. @PostFilter dönen koleksiyondan izinsiz elemanları çıkarır.
// Kullanıcı sadece kendi departmanının kayıtlarını toplu güncelleyebilir
@PreFilter("filterObject.departmentId == authentication.principal.departmentId")
public void batchUpdate(List<Employee> employees) {
// employees listesinden farklı departman olanlar otomatik çıkarılır
employeeRepository.saveAll(employees);
}
// Dönen listeden sadece kullanıcının görmesi gereken projeleri filtrele
@PostFilter("filterObject.isPublic() or filterObject.owner == authentication.name")
public List<Project> getAllProjects() {
return projectRepository.findAll();
// Kullanıcı sadece public projeleri ve kendi projelerini görür
}
// Birden fazla koleksiyon parametresi varsa hangisini filtreleyeceğini belirt
@PreFilter(value = "filterObject.active", filterTarget = "items")
public void processItems(List<Item> items, String category) { ... }⚠️ Performans uyarısı:
@PostFilterönce tüm veriyi veritabanından çeker, sonra Java'da filtreler. Büyük veri setlerinde JPQLWHEREclause kullanmak çok daha verimlidir.@PostFilterküçük koleksiyonlar veya in-memory veriler için uygundur.
Method Security ile Hata Yönetimi
Method security ihlallerinde AccessDeniedException fırlatılır. Bunu global olarak yakalayabilirsiniz:
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ErrorResponse> handleAccessDenied(AccessDeniedException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.FORBIDDEN.value(),
"Bu işlem için yetkiniz bulunmamaktadır.",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
}Method Security'yi Test Etme
@SpringBootTest
class UserServiceSecurityTest {
@Autowired
private UserService userService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanDeleteUser() {
assertDoesNotThrow(() -> userService.deleteUser(1L));
}
@Test
@WithMockUser(roles = "USER")
void regularUserCannotDeleteUser() {
assertThrows(AccessDeniedException.class,
() -> userService.deleteUser(1L));
}
@Test
@WithMockUser(username = "tolgahan", roles = "USER")
void userCanAccessOwnProfile() {
// Principal ID'si 42 olan kullanıcı
assertDoesNotThrow(() -> userService.getUser(42L));
}
}💡 Özet: Method security,
@PreAuthorize(çağrı öncesi),@PostAuthorize(çağrı sonrası),@PreFilter/@PostFilter(koleksiyon filtreleme) annotation'ları ile iş mantığı seviyesinde güvenlik sağlar. SpEL ile parametrelere, authentication bilgilerine ve custom bean'lere erişilebilir. URL-based security ile birlikte derinlemesine savunma (defense in depth) oluşturur.
AI Asistan
Sorularını yanıtlamaya hazır