← Kursa Dön
📄 Text · 15 min

@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

ÖzellikURL-Based (HttpSecurity)Method Security
SeviyeHTTP endpointJava method
GranülerlikKaba (URL pattern)İnce (method + parametreler)
İş kurallarıZorSpEL ile kolay
Nerede tanımlanırSecurityConfigService 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: @PostAuthorize method ç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 @PreAuthorize tercih 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:

AnnotationSpELParametrelere ErişimreturnObjectStandart
@PreAuthorize✅ (#param)Spring
@PostAuthorizeSpring
@SecuredSpring
@RolesAllowedJSR-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 JPQL WHERE clause kullanmak çok daha verimlidir. @PostFilter küçü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.