Spring Boot Aufbau-Kurs – Tag 5 von 10
Von Elyndra Valen, Senior Entwicklerin bei Java Fleet Systems Consulting


Authorization

📍 Deine Position im Kurs

TagThemaStatus
1Auto-Configuration & Custom StarterAbgeschlossen ✅
2Spring Data JPA BasicsAbgeschlossen ✅
3JPA Relationships & QueriesAbgeschlossen ✅
4Spring Security – Part 1Abgeschlossen ✅
→ 5Spring Security – Part 2👉 DU BIST HIER!
6Caching & SerialisierungKommt als nächstes
7Messaging & EmailNoch nicht freigeschaltet
8Testing & DokumentationNoch nicht freigeschaltet
9Spring Boot ActuatorNoch nicht freigeschaltet
10Template Engines & MicroservicesNoch nicht freigeschaltet

Modul: Spring Boot Aufbau-Kurs (10 Arbeitstage)
Dein Ziel: Authorization & Method Security meistern


📋 Voraussetzungen

Du brauchst:

  • ✅ Tag 4 abgeschlossen – Login-System funktioniert
  • ✅ User Entity mit Roles (ADMIN, USER)
  • ✅ UserDetailsService implementiert
  • ✅ SecurityFilterChain konfiguriert
  • ✅ Du verstehst Authentication vs Authorization

Du solltest können:

  • ✅ User einloggen und ausloggen
  • ✅ Passwörter mit BCrypt verschlüsseln
  • ✅ SecurityContext nutzen

⚡ Was du heute baust:

Gestern hast du gelernt WER sich einloggen darf (Authentication). Heute lernst du WAS User tun dürfen (Authorization):

  • URL-basierte Authorization (hasRole, hasAuthority)
  • Method Security mit @PreAuthorize
  • Granulare Zugriffskontrollen
  • Custom Security Expressions
  • User kann nur EIGENE Daten ändern
  • JWT Token Authentication (Bonus)

Der Unterschied:

Authentication: "Bist du wirklich Alice?"     ← Tag 4 ✅
Authorization:  "Darf Alice diese Datei löschen?"  ← HEUTE!

🎯 Dein Lernpfad heute:

🟢 Grundlagen (Schritte 1-4)

Was du lernst:

  • URL-basierte Authorization
  • hasRole() und hasAuthority()
  • Unterschied zwischen Role und Authority
  • @PreAuthorize Basics
  • Method Security aktivieren

Ziel: Du kannst URLs und Methoden für verschiedene Rollen schützen.

🟡 Professional (Schritte 5-7)

Was du lernst:

  • SpEL Expressions in Security
  • @PostAuthorize für Response-Filterung
  • Custom Security Expressions
  • Ownership-basierte Authorization („User darf nur EIGENE Daten ändern“)
  • SecurityContext in Services nutzen

Ziel: Du baust enterprise-level, granulare Zugriffskontrollen.

🔵 Bonus: JWT & Stateless Security (Schritt 8)

Was du baust:

  • JWT Token statt Session
  • Stateless Authentication für REST APIs
  • Token Refresh Mechanismus
  • Moderne API Security

Ziel: Production-ready REST API Security.

💡 Tipp: Grundlagen (🟢) sind heute absolut essenziell – Authorization ist das Herzstück jeder sicheren App! Professional (🟡) macht dich zum Security-Profi. Bonus (🔵) ist modern und für REST APIs unerlässlich.


💻 Los geht’s!

🟢 GRUNDLAGEN

Schritt 1: URL-basierte Authorization – hasRole() verstehen

Hi Developer! 👋

Elyndra hier – gestern haben wir Authentication gebaut, heute geht’s um Authorization!

Erinnerst du dich an Tag 4? Dort hast du gelernt wie Spring Security User authentifiziert. Heute lernen wir wie wir kontrollieren WAS diese User tun dürfen!

Was ist Authorization?

Authentication vs Authorization:

Authentication (Tag 4):
"Bist du wirklich Alice?" 
→ Username + Password Check
→ Erstellt Authentication-Objekt

Authorization (HEUTE):
"Darf Alice diese Aktion ausführen?"
→ Prüft Roles/Permissions
→ Erlaubt oder verweigert Zugriff

Real-World Beispiel:

Hotel-Schlüsselkarte:
- Authentication: Karte öffnet die Tür (du bist Gast)
- Authorization: Karte öffnet NUR DEIN Zimmer (nicht andere Zimmer!)

Bank:
- Authentication: PIN-Code am Automaten (du bist Kontoinhaber)
- Authorization: Du darfst NUR DEIN Konto sehen (nicht fremde Konten!)

SecurityFilterChain erweitern

Erinnerst du dich an die SecurityFilterChain aus Tag 4? Dort haben wir nur .authenticated() genutzt – heute werden wir spezifischer!

Datei: src/main/java/com/javafleet/personmanagement/security/SecurityConfig.java

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())  // Später aktivieren
        
        .authorizeHttpRequests(auth -> auth
            // Öffentliche URLs
            .requestMatchers("/", "/login", "/register").permitAll()
            
            // Admin-Only URLs
            .requestMatchers("/admin/**").hasRole("ADMIN")
            
            // Alle anderen brauchen nur Login
            .anyRequest().authenticated()
        )
        
        .formLogin(form -> form
            .loginPage("/login")
            .defaultSuccessUrl("/persons", true)
            .permitAll()
        )
        
        .logout(logout -> logout
            .logoutSuccessUrl("/login?logout")
            .permitAll()
        )
        
        .httpBasic(basic -> {});
    
    return http.build();
}

Was ist neu?

.requestMatchers("/admin/**").hasRole("ADMIN")

Das bedeutet:

  • Alle URLs die mit /admin/ anfangen
  • Brauchen ROLE_ADMIN
  • Normale User mit ROLE_USER bekommen 403 Forbidden

hasRole() im Detail

Wichtig: hasRole("ADMIN") prüft auf „ROLE_ADMIN“!

Erinnerst du dich an Tag 4? Dort haben wir in CustomUserDetailsService den „ROLE_“ Prefix hinzugefügt:

// Tag 4 - CustomUserDetailsService
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
    return Collections.singletonList(
        new SimpleGrantedAuthority("ROLE_" + user.getRole().name())
    );
}

// user.getRole() = Role.ADMIN
// Authority wird: "ROLE_ADMIN"

Deswegen funktioniert:

.requestMatchers("/admin/**").hasRole("ADMIN")  
// Prüft auf "ROLE_ADMIN" ✅

// NICHT:
.requestMatchers("/admin/**").hasRole("ROLE_ADMIN")  
// Prüft auf "ROLE_ROLE_ADMIN" ❌ FALSCH!

Spring fügt automatisch „ROLE_“ Prefix hinzu!

Admin-Controller erstellen

Datei: src/main/java/com/javafleet/personmanagement/controller/AdminController.java

package com.javafleet.personmanagement.controller;

import com.javafleet.personmanagement.entity.User;
import com.javafleet.personmanagement.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.List;

@Controller
@RequestMapping("/admin")
@RequiredArgsConstructor
public class AdminController {
    
    private final UserRepository userRepository;
    
    @GetMapping("/users")
    public String listUsers(Model model) {
        List<User> users = userRepository.findAll();
        model.addAttribute("users", users);
        return "admin/users";
    }
    
    @GetMapping("/dashboard")
    public String dashboard(Model model) {
        long userCount = userRepository.count();
        model.addAttribute("userCount", userCount);
        return "admin/dashboard";
    }
}

Testen der Authorization

1. Als ADMIN einloggen:

Browser: http://localhost:8080/login
Username: admin
Password: admin123

Dann aufrufen:

http://localhost:8080/admin/dashboard

✅ Funktioniert! Admin hat ROLE_ADMIN!

2. Ausloggen und als USER einloggen:

http://localhost:8080/logout

Login:
Username: user
Password: user123

Dann aufrufen:

http://localhost:8080/admin/dashboard

❌ 403 Forbidden!

Whitelabel Error Page
403 Forbidden
Access Denied

Perfekt! User hat nur ROLE_USER, darf nicht auf /admin/** zugreifen!

Mit curl testen

# Als ADMIN
curl -u admin:admin123 http://localhost:8080/admin/users
# Response: 200 OK

# Als USER
curl -u user:user123 http://localhost:8080/admin/users
# Response: 403 Forbidden

🎉 AHA-Moment #1: „URL-basierte Authorization ist wie ein Türsteher! hasRole(‚ADMIN‘) lässt nur Admins durch – normale User bekommen 403 Forbidden. Spring Security prüft das bei JEDEM Request automatisch!“


Schritt 2: hasRole() vs hasAuthority() – Der Unterschied

Was ist der Unterschied zwischen Role und Authority?

Viele Entwickler verwechseln das – lass uns das GENAU verstehen!

Role vs Authority – Konzept

Role = Breite Kategorie:

ADMIN    → Kann alles
USER     → Kann Basis-Features
MODERATOR → Kann Inhalte moderieren
GUEST    → Nur lesen

Authority = Spezifische Berechtigung:

READ_PRIVILEGES
WRITE_PRIVILEGES
DELETE_PRIVILEGES
APPROVE_PRIVILEGES

Real-World Beispiel:

Firma:
- Role: "MANAGER" 
  → Authorities: [APPROVE_EXPENSES, VIEW_SALARIES, HIRE_EMPLOYEES]

- Role: "EMPLOYEE"
  → Authorities: [SUBMIT_EXPENSES, VIEW_OWN_SALARY]

Ein User kann:

  • EINE oder MEHRERE Roles haben
  • VIELE Authorities haben

In Spring Security

hasRole():

.requestMatchers("/admin/**").hasRole("ADMIN")
// Prüft auf "ROLE_ADMIN"
// Spring fügt "ROLE_" Prefix automatisch hinzu!

hasAuthority():

.requestMatchers("/api/persons/delete").hasAuthority("DELETE_PRIVILEGES")
// Prüft EXAKT auf "DELETE_PRIVILEGES"
// KEIN automatischer Prefix!

Beispiel: hasAuthority() nutzen

Authority Enum erstellen:

Datei: src/main/java/com/javafleet/personmanagement/entity/Authority.java

package com.javafleet.personmanagement.entity;

public enum Authority {
    READ_PRIVILEGES,
    WRITE_PRIVILEGES,
    DELETE_PRIVILEGES,
    ADMIN_PRIVILEGES
}

User Entity erweitern:

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;
    
    // NEU: Authorities
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_authorities", 
                     joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    @Column(name = "authority")
    private Set<Authority> authorities = new HashSet<>();
    
    @Column(nullable = false)
    private boolean enabled = true;
    
    @Column(nullable = false)
    private boolean accountNonLocked = true;
}

Was macht @ElementCollection?

@ElementCollection(fetch = FetchType.EAGER)

Das erstellt eine separate Tabelle:

CREATE TABLE user_authorities (
    user_id BIGINT,
    authority VARCHAR(50),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Beispiel-Daten:

users:
id | username | role
1  | admin    | ADMIN
2  | user     | USER

user_authorities:
user_id | authority
1       | READ_PRIVILEGES
1       | WRITE_PRIVILEGES
1       | DELETE_PRIVILEGES
1       | ADMIN_PRIVILEGES
2       | READ_PRIVILEGES
2       | WRITE_PRIVILEGES

FetchType.EAGER bedeutet: Authorities werden SOFORT mit User geladen!

Ohne EAGER:

User user = userRepository.findById(1L);
user.getAuthorities();  // LazyInitializationException! ❌

Mit EAGER:

User user = userRepository.findById(1L);
user.getAuthorities();  // [READ_PRIVILEGES, WRITE_PRIVILEGES, ...] ✅

CustomUserDetailsService erweitern

Datei: src/main/java/com/javafleet/personmanagement/security/CustomUserDetailsService.java

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User nicht gefunden: " + username));
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(getAuthorities(user))  // HIER!
            .accountLocked(!user.isAccountNonLocked())
            .disabled(!user.isEnabled())
            .build();
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(User user) {
        Set<GrantedAuthority> authorities = new HashSet<>();
        
        // 1. Role hinzufügen (mit ROLE_ Prefix!)
        authorities.add(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
        
        // 2. Authorities hinzufügen (OHNE Prefix!)
        user.getAuthorities().forEach(auth -> 
            authorities.add(new SimpleGrantedAuthority(auth.name()))
        );
        
        return authorities;
    }
}

Was ist neu?

Jetzt hat ein User BEIDES:

  • Role: ROLE_ADMIN oder ROLE_USER
  • Authorities: READ_PRIVILEGES, WRITE_PRIVILEGES, etc.

Beispiel User „admin“:

GrantedAuthorities:
- ROLE_ADMIN              ← Role
- READ_PRIVILEGES         ← Authority
- WRITE_PRIVILEGES        ← Authority
- DELETE_PRIVILEGES       ← Authority
- ADMIN_PRIVILEGES        ← Authority

SecurityFilterChain mit hasAuthority()

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())
        
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/", "/login", "/register").permitAll()
            
            // Role-basiert
            .requestMatchers("/admin/**").hasRole("ADMIN")
            
            // Authority-basiert
            .requestMatchers("/api/persons/delete/**").hasAuthority("DELETE_PRIVILEGES")
            .requestMatchers("/api/persons/create").hasAuthority("WRITE_PRIVILEGES")
            .requestMatchers("/api/persons/**").hasAuthority("READ_PRIVILEGES")
            
            .anyRequest().authenticated()
        )
        
        .formLogin(form -> form.loginPage("/login").permitAll())
        .logout(logout -> logout.permitAll())
        .httpBasic(basic -> {});
    
    return http.build();
}

Test-Daten aktualisieren

Datei: src/main/java/com/javafleet/personmanagement/DataLoader.java

@Component
@RequiredArgsConstructor
@Slf4j
public class DataLoader implements CommandLineRunner {
    
    private final UserService userService;
    
    @Override
    public void run(String... args) {
        log.info("Creating test users with authorities...");
        
        try {
            // Admin mit allen Authorities
            User admin = new User();
            admin.setUsername("admin");
            admin.setPassword("admin123");
            admin.setRole(Role.ADMIN);
            admin.setAuthorities(Set.of(
                Authority.READ_PRIVILEGES,
                Authority.WRITE_PRIVILEGES,
                Authority.DELETE_PRIVILEGES,
                Authority.ADMIN_PRIVILEGES
            ));
            userService.createUser(admin);
            
            // Normal User nur mit Read/Write
            User user = new User();
            user.setUsername("user");
            user.setPassword("user123");
            user.setRole(Role.USER);
            user.setAuthorities(Set.of(
                Authority.READ_PRIVILEGES,
                Authority.WRITE_PRIVILEGES
            ));
            userService.createUser(user);
            
            log.info("Test users created successfully");
            
        } catch (Exception e) {
            log.warn("Users already exist: {}", e.getMessage());
        }
    }
}

UserService anpassen:

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    @Transactional
    public User createUser(User user) {
        if (userRepository.existsByUsername(user.getUsername())) {
            throw new IllegalArgumentException("Username bereits vergeben");
        }
        
        // Password verschlüsseln
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setEnabled(true);
        user.setAccountNonLocked(true);
        
        return userRepository.save(user);
    }
}

Testen

Als USER einloggen:

curl -u user:user123 http://localhost:8080/api/persons
# ✅ 200 OK - Hat READ_PRIVILEGES

curl -u user:user123 -X POST http://localhost:8080/api/persons/create
# ✅ 200 OK - Hat WRITE_PRIVILEGES

curl -u user:user123 -X DELETE http://localhost:8080/api/persons/delete/1
# ❌ 403 Forbidden - Hat NICHT DELETE_PRIVILEGES!

Als ADMIN einloggen:

curl -u admin:admin123 -X DELETE http://localhost:8080/api/persons/delete/1
# ✅ 200 OK - Hat DELETE_PRIVILEGES

🎉 AHA-Moment #2: „hasRole() ist für breite Kategorien (ADMIN, USER), hasAuthority() ist für spezifische Berechtigungen (DELETE_PRIVILEGES)! Ein User kann BEIDES haben – Role UND mehrere Authorities. Das ist wie Job-Title (Role) und konkrete Berechtigungen (Authorities)!“


Schritt 3: Method Security – @PreAuthorize aktivieren

URL-basierte Security ist gut, aber nicht genug! Was wenn du Methoden in Services schützen willst?

Das Problem:

@RestController
public class PersonController {
    
    @GetMapping("/api/persons/{id}")
    public Person getPerson(@PathVariable Long id) {
        return personService.findById(id);  // Wer darf das?
    }
}

URL-Security allein reicht nicht:

  • Was wenn personService.findById() von mehreren Stellen aufgerufen wird?
  • Was wenn nur der OWNER seine Person sehen darf?
  • Security in der URL ist zu grob!

Lösung: Method Security!

Method Security aktivieren

Datei: src/main/java/com/javafleet/personmanagement/security/SecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableMethodSecurity  // ← NEU! Aktiviert @PreAuthorize
@RequiredArgsConstructor
public class SecurityConfig {
    // ... Rest bleibt gleich
}

Was macht @EnableMethodSecurity?

Das aktiviert:

  • @PreAuthorize – Prüft VOR Methoden-Aufruf
  • @PostAuthorize – Prüft NACH Methoden-Aufruf
  • @Secured – Legacy Annotation
  • @RolesAllowed – JSR-250 Standard

Wir nutzen hauptsächlich @PreAuthorize!

@PreAuthorize Basics

Datei: src/main/java/com/javafleet/personmanagement/service/PersonService.java

package com.javafleet.personmanagement.service;

import com.javafleet.personmanagement.entity.Person;
import com.javafleet.personmanagement.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
public class PersonService {
    
    private final PersonRepository personRepository;
    
    // Jeder authenticated User darf lesen
    @PreAuthorize("isAuthenticated()")
    public List<Person> findAll() {
        return personRepository.findAll();
    }
    
    // Nur ADMIN darf löschen
    @PreAuthorize("hasRole('ADMIN')")
    @Transactional
    public void deleteById(Long id) {
        personRepository.deleteById(id);
    }
    
    // Nur mit DELETE_PRIVILEGES
    @PreAuthorize("hasAuthority('DELETE_PRIVILEGES')")
    @Transactional
    public void deleteAll() {
        personRepository.deleteAll();
    }
    
    // ADMIN oder User mit WRITE_PRIVILEGES
    @PreAuthorize("hasRole('ADMIN') or hasAuthority('WRITE_PRIVILEGES')")
    @Transactional
    public Person save(Person person) {
        return personRepository.save(person);
    }
}

Was passiert hier?

@PreAuthorize prüft VOR dem Methoden-Aufruf:

1. Controller ruft personService.deleteById(1) auf
2. Spring Security Interceptor fängt den Aufruf ab
3. Evaluiert Expression: hasRole('ADMIN')
4. Prüft: Hat aktueller User ROLE_ADMIN?
5a. JA → Methode wird ausgeführt ✅
5b. NEIN → AccessDeniedException ❌ (wird zu 403 Forbidden)

Das funktioniert überall:

  • In Services
  • In Repositories
  • In Controllers
  • In beliebigen Spring Beans!

SpEL Expressions

@PreAuthorize nutzt Spring Expression Language (SpEL):

// Einfache Checks
@PreAuthorize("isAuthenticated()")           // Ist eingeloggt?
@PreAuthorize("isAnonymous()")               // Nicht eingeloggt?
@PreAuthorize("hasRole('ADMIN')")            // Hat Role?
@PreAuthorize("hasAuthority('DELETE_PRIVILEGES')")  // Hat Authority?

// Kombinationen mit AND
@PreAuthorize("hasRole('ADMIN') and hasAuthority('DELETE_PRIVILEGES')")

// Kombinationen mit OR
@PreAuthorize("hasRole('ADMIN') or hasRole('MODERATOR')")

// Multiple Roles
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR', 'MANAGER')")

// Multiple Authorities
@PreAuthorize("hasAnyAuthority('DELETE_PRIVILEGES', 'ADMIN_PRIVILEGES')")

// Negation
@PreAuthorize("!hasRole('GUEST')")  // NICHT Guest

Controller anpassen

Datei: src/main/java/com/javafleet/personmanagement/controller/PersonController.java

@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
public class PersonController {
    
    private final PersonService personService;
    
    @GetMapping
    public List<Person> getAllPersons() {
        return personService.findAll();  // @PreAuthorize prüft!
    }
    
    @PostMapping
    public Person createPerson(@RequestBody Person person) {
        return personService.save(person);  // @PreAuthorize prüft!
    }
    
    @DeleteMapping("/{id}")
    public void deletePerson(@PathVariable Long id) {
        personService.deleteById(id);  // @PreAuthorize prüft!
    }
}

Wichtig: Die Security ist jetzt IN DER SERVICE-METHODE!

Vorteil:

// Egal von wo personService.deleteById() aufgerufen wird:
// - Von REST Controller
// - Von anderem Service
// - Von Scheduled Task
// - Von Message Listener

// → Security-Check passiert IMMER! ✅

Testen

Als USER (hat nur READ + WRITE):

# Lesen - funktioniert
curl -u user:user123 http://localhost:8080/api/persons
# ✅ 200 OK

# Erstellen - funktioniert (hat WRITE_PRIVILEGES)
curl -u user:user123 -X POST http://localhost:8080/api/persons \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Test","lastname":"User","email":"test@example.com"}'
# ✅ 200 OK

# Löschen - funktioniert NICHT (braucht ADMIN Role)
curl -u user:user123 -X DELETE http://localhost:8080/api/persons/1
# ❌ 403 Forbidden

Als ADMIN:

# Löschen - funktioniert
curl -u admin:admin123 -X DELETE http://localhost:8080/api/persons/1
# ✅ 200 OK

In der Exception:

org.springframework.security.access.AccessDeniedException: 
Access Denied
	at org.springframework.security.access.prepost.PreAuthorizeAuthorizationManager.check(...)

🎉 AHA-Moment #3: „@PreAuthorize verschiebt Security in die Business-Logik! Egal von wo die Methode aufgerufen wird – der Security-Check passiert IMMER. Das ist viel sicherer als nur URL-basierte Security!“


Schritt 4: Principal in Methoden – Den aktuellen User holen

Oft brauchst du den aktuellen User IN deiner Methode!

Erinnerst du dich an Tag 4? Dort haben wir SecurityContextHolder kennengelernt:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();

Aber das ist umständlich! Spring Security bietet bessere Wege:

Method 1: @AuthenticationPrincipal

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @GetMapping("/me")
    public String currentUser(@AuthenticationPrincipal UserDetails user) {
        return "Eingeloggt als: " + user.getUsername();
    }
    
    @GetMapping("/my-authorities")
    public Collection<? extends GrantedAuthority> myAuthorities(
            @AuthenticationPrincipal UserDetails user) {
        return user.getAuthorities();
    }
}

Was macht @AuthenticationPrincipal?

Spring Security injiziert automatisch den aktuellen User als Parameter!

Request kommt → Spring Security holt Authentication aus SecurityContext
              → Extrahiert Principal (UserDetails)
              → Injiziert als Method-Parameter ✅

Method 2: Authentication Parameter

@GetMapping("/my-roles")
public String myRoles(Authentication authentication) {
    return "Username: " + authentication.getName() + 
           ", Authorities: " + authentication.getAuthorities();
}

Direkter Zugriff auf das ganze Authentication-Objekt!

Method 3: Custom Annotation für eigene User Entity

Problem: @AuthenticationPrincipal gibt Spring Security’s UserDetails zurück, nicht DEINE User Entity!

Lösung: Custom Annotation!

Datei: src/main/java/com/javafleet/personmanagement/security/CurrentUser.java

package com.javafleet.personmanagement.security;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "@userService.getUserByUsername(authentication.name)")
public @interface CurrentUser {
}

Controller nutzen:

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    
    @GetMapping("/me/full")
    public User currentUserFull(@CurrentUser User user) {
        // Direkt DEINE User Entity! ✅
        return user;
    }
    
    @GetMapping("/me/authorities")
    public Set<Authority> myAuthorities(@CurrentUser User user) {
        return user.getAuthorities();  // Aus DEINER Entity!
    }
}

Was passiert hier?

@AuthenticationPrincipal(expression = "@userService.getUserByUsername(authentication.name)")

SpEL Expression:

  1. authentication.name → Aktueller Username
  2. @userService → Spring Bean mit Name „userService“
  3. .getUserByUsername(...) → Methode aufrufen
  4. Ergebnis → DEINE User Entity!

UserService braucht die Methode:

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    
    public User getUserByUsername(String username) {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new IllegalArgumentException(
                "User nicht gefunden: " + username));
    }
}

Testen

# UserDetails (Spring Security)
curl -u admin:admin123 http://localhost:8080/api/users/me
# Response: "Eingeloggt als: admin"

# Deine User Entity
curl -u admin:admin123 http://localhost:8080/api/users/me/full
# Response:
{
  "id": 1,
  "username": "admin",
  "role": "ADMIN",
  "authorities": [
    "READ_PRIVILEGES",
    "WRITE_PRIVILEGES",
    "DELETE_PRIVILEGES",
    "ADMIN_PRIVILEGES"
  ],
  "enabled": true,
  "accountNonLocked": true
}

🎉 AHA-Moment #4: „Ich kann den aktuellen User direkt als Method-Parameter bekommen! @AuthenticationPrincipal spart mir SecurityContextHolder.getContext() Boilerplate. Mit @CurrentUser bekomme ich sogar MEINE eigene User Entity!“


🟡 PROFESSIONAL

Schritt 5: Ownership-basierte Authorization – „Nur EIGENE Daten ändern“

Jetzt wird’s interessant! Wie stellst du sicher dass User nur IHRE EIGENEN Daten ändern können?

Das Problem:

User "alice" erstellt Person "Alice Miller"
User "bob" ruft auf: DELETE /api/persons/{alices-person-id}

→ Bob darf Alice's Daten NICHT löschen!

hasRole() und hasAuthority() reichen NICHT:

@PreAuthorize("hasRole('USER')")  // ❌ NICHT genug!
public void delete(Long id) {
    // Bob hat auch ROLE_USER → kann löschen! ❌
}

Wir brauchen: „User darf nur EIGENE Daten ändern!“

Person Entity erweitern mit Owner

Datei: src/main/java/com/javafleet/personmanagement/entity/Person.java

@Entity
@Table(name = "persons")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String firstname;
    private String lastname;
    private String email;
    
    // NEU: Jede Person hat einen Owner
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id", nullable = false)
    private User owner;
}

Was bedeutet das?

persons:
id | firstname | lastname | email          | owner_id
1  | Alice     | Miller   | alice@...      | 2        ← Gehört User mit ID 2
2  | Bob       | Smith    | bob@...        | 3        ← Gehört User mit ID 3
3  | Admin     | User     | admin@...      | 1        ← Gehört User mit ID 1

users:
id | username
1  | admin
2  | alice
3  | bob

Erinnerst du dich an Tag 3? Dort haben wir @ManyToOne Relationships gelernt!

@ManyToOne: Viele Persons gehören zu EINEM User

Custom Security Expression – isOwner()

Datei: src/main/java/com/javafleet/personmanagement/security/CustomSecurityExpressions.java

package com.javafleet.personmanagement.security;

import com.javafleet.personmanagement.entity.Person;
import com.javafleet.personmanagement.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component("securityExpressions")
@RequiredArgsConstructor
public class CustomSecurityExpressions {
    
    private final PersonRepository personRepository;
    
    public boolean isOwner(Long personId) {
        // 1. Aktuellen User holen
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUsername = auth.getName();
        
        // 2. Person aus DB laden
        Person person = personRepository.findById(personId).orElse(null);
        if (person == null) {
            return false;  // Person existiert nicht
        }
        
        // 3. Prüfen: Ist currentUsername der Owner?
        return person.getOwner().getUsername().equals(currentUsername);
    }
    
    public boolean isOwnerOrAdmin(Long personId) {
        // Admin darf immer
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        boolean isAdmin = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        
        if (isAdmin) {
            return true;  // Admin darf alles
        }
        
        // Sonst nur Owner
        return isOwner(personId);
    }
}

Was macht das?

@Component(„securityExpressions“) registriert die Klasse als Spring Bean mit Namen „securityExpressions“

isOwner(Long personId):

  1. Holt aktuellen Username aus SecurityContext
  2. Lädt Person aus Datenbank
  3. Vergleicht: person.owner.username == currentUsername

isOwnerOrAdmin(Long personId):

  • Admin darf IMMER (egal wem die Person gehört)
  • Normale User nur bei EIGENEN Daten

Service mit Ownership-Check

Datei: src/main/java/com/javafleet/personmanagement/service/PersonService.java

@Service
@RequiredArgsConstructor
public class PersonService {
    
    private final PersonRepository personRepository;
    
    // Lesen: Jeder authenticated
    @PreAuthorize("isAuthenticated()")
    public List<Person> findAll() {
        return personRepository.findAll();
    }
    
    // Lesen einzeln: Jeder authenticated
    @PreAuthorize("isAuthenticated()")
    public Person findById(Long id) {
        return personRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Person nicht gefunden"));
    }
    
    // Erstellen: Authenticated + WRITE_PRIVILEGES
    @PreAuthorize("isAuthenticated() and hasAuthority('WRITE_PRIVILEGES')")
    @Transactional
    public Person create(Person person, User owner) {
        person.setOwner(owner);  // Automatisch Owner setzen!
        return personRepository.save(person);
    }
    
    // Update: Nur Owner oder Admin!
    @PreAuthorize("@securityExpressions.isOwnerOrAdmin(#id)")
    @Transactional
    public Person update(Long id, Person updatedPerson) {
        Person existing = findById(id);
        existing.setFirstname(updatedPerson.getFirstname());
        existing.setLastname(updatedPerson.getLastname());
        existing.setEmail(updatedPerson.getEmail());
        return personRepository.save(existing);
    }
    
    // Delete: Nur Owner oder Admin!
    @PreAuthorize("@securityExpressions.isOwnerOrAdmin(#id)")
    @Transactional
    public void deleteById(Long id) {
        personRepository.deleteById(id);
    }
}

Was ist neu?

@PreAuthorize("@securityExpressions.isOwnerOrAdmin(#id)")

SpEL Expression im Detail:

  • @securityExpressions → Spring Bean Name
  • .isOwnerOrAdmin(…) → Methode aufrufen
  • #id → Method-Parameter id übergeben!

Spring evaluiert:

1. Hole Bean "securityExpressions"
2. Rufe Methode isOwnerOrAdmin(id) auf
3. Ergebnis: true oder false
4. true → Methode ausführen ✅
5. false → AccessDeniedException ❌

Controller anpassen

@RestController
@RequestMapping("/api/persons")
@RequiredArgsConstructor
public class PersonController {
    
    private final PersonService personService;
    
    @GetMapping
    public List<Person> getAll() {
        return personService.findAll();
    }
    
    @PostMapping
    public Person create(
            @RequestBody Person person,
            @CurrentUser User currentUser) {
        return personService.create(person, currentUser);  // Owner = aktueller User!
    }
    
    @PutMapping("/{id}")
    public Person update(
            @PathVariable Long id,
            @RequestBody Person person) {
        return personService.update(id, person);  // Security-Check in Service!
    }
    
    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) {
        personService.deleteById(id);  // Security-Check in Service!
    }
}

Wichtig: Owner wird automatisch gesetzt als der aktuell eingeloggte User!

Testen

1. Als „alice“ einloggen und Person erstellen:

curl -u alice:alice123 -X POST http://localhost:8080/api/persons \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Alice","lastname":"Miller","email":"alice@example.com"}'

# Response:
{
  "id": 1,
  "firstname": "Alice",
  "lastname": "Miller",
  "email": "alice@example.com",
  "owner": {
    "id": 2,
    "username": "alice"
  }
}

2. Als „bob“ einloggen und Person erstellen:

curl -u bob:bob123 -X POST http://localhost:8080/api/persons \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Bob","lastname":"Smith","email":"bob@example.com"}'

# Response:
{
  "id": 2,
  "firstname": "Bob",
  "lastname": "Smith",
  "email": "bob@example.com",
  "owner": {
    "id": 3,
    "username": "bob"
  }
}

3. Bob versucht Alice’s Person zu löschen:

curl -u bob:bob123 -X DELETE http://localhost:8080/api/persons/1
# ❌ 403 Forbidden - Bob ist nicht Owner von Person 1!

4. Alice löscht ihre eigene Person:

curl -u alice:alice123 -X DELETE http://localhost:8080/api/persons/1
# ✅ 200 OK - Alice ist Owner!

5. Admin kann alles löschen:

curl -u admin:admin123 -X DELETE http://localhost:8080/api/persons/2
# ✅ 200 OK - Admin darf alles!

Perfect! 🎉

🎉 AHA-Moment #5: „Mit Custom Security Expressions kann ich komplexe Business-Rules in Security einbauen! @securityExpressions.isOwner() prüft ob der User der Eigentümer ist – so kann jeder User nur SEINE Daten ändern, nicht fremde!“


Schritt 6: @PostAuthorize – Response-Filterung

@PreAuthorize prüft VOR der Methode. Was wenn du die Response filtern willst?

Beispiel: User darf Person sehen, aber nur wenn sie nicht „vertraulich“ markiert ist!

Person Entity erweitern

@Entity
@Table(name = "persons")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String firstname;
    private String lastname;
    private String email;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id", nullable = false)
    private User owner;
    
    // NEU: Vertraulichkeits-Flag
    @Column(nullable = false)
    private boolean confidential = false;
}

@PostAuthorize nutzen

@Service
@RequiredArgsConstructor
public class PersonService {
    
    private final PersonRepository personRepository;
    
    // Nach Methoden-Ausführung prüfen
    @PostAuthorize("@securityExpressions.canView(returnObject)")
    public Person findById(Long id) {
        return personRepository.findById(id)
            .orElseThrow(() -> new IllegalArgumentException("Person nicht gefunden"));
    }
}

Was ist neu?

@PostAuthorize("@securityExpressions.canView(returnObject)")

returnObject = Das Ergebnis der Methode (die Person)!

Ablauf:

1. Methode wird ausgeführt → Person aus DB geladen
2. @PostAuthorize wird evaluiert
3. returnObject = die geladene Person
4. canView(Person) wird aufgerufen
5. true → Person wird zurückgegeben ✅
6. false → AccessDeniedException ❌

Custom Expression erweitern

@Component("securityExpressions")
@RequiredArgsConstructor
public class CustomSecurityExpressions {
    
    private final PersonRepository personRepository;
    
    public boolean canView(Person person) {
        if (person == null) {
            return false;
        }
        
        // Aktuellen User holen
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUsername = auth.getName();
        
        // Admin darf alles sehen
        boolean isAdmin = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
        if (isAdmin) {
            return true;
        }
        
        // Vertraulich? Nur Owner darf sehen
        if (person.isConfidential()) {
            return person.getOwner().getUsername().equals(currentUsername);
        }
        
        // Nicht vertraulich? Jeder darf sehen
        return true;
    }
    
    // Bestehende Methoden...
}

Logik:

IF person ist vertraulich:
  → Nur Owner oder Admin dürfen sehen
ELSE:
  → Jeder authenticated User darf sehen

Testen

1. Nicht-vertrauliche Person erstellen:

curl -u alice:alice123 -X POST http://localhost:8080/api/persons \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Public","lastname":"Person","email":"public@example.com","confidential":false}'

2. Vertrauliche Person erstellen:

curl -u alice:alice123 -X POST http://localhost:8080/api/persons \
  -H "Content-Type: application/json" \
  -d '{"firstname":"Secret","lastname":"Person","email":"secret@example.com","confidential":true}'

# Response: ID = 10 (beispielsweise)

3. Bob versucht nicht-vertrauliche Person zu sehen:

curl -u bob:bob123 http://localhost:8080/api/persons/9
# ✅ 200 OK - Nicht vertraulich, jeder darf sehen

4. Bob versucht vertrauliche Person zu sehen:

curl -u bob:bob123 http://localhost:8080/api/persons/10
# ❌ 403 Forbidden - Vertraulich, Bob ist nicht Owner!

5. Alice sieht ihre eigene vertrauliche Person:

curl -u alice:alice123 http://localhost:8080/api/persons/10
# ✅ 200 OK - Alice ist Owner!

6. Admin sieht alles:

curl -u admin:admin123 http://localhost:8080/api/persons/10
# ✅ 200 OK - Admin darf alles!

🎉 AHA-Moment #6: „@PostAuthorize prüft NACH der Methoden-Ausführung! Ich kann das ERGEBNIS (returnObject) prüfen und entscheiden ob der User es sehen darf. Perfect für Response-Filterung basierend auf Daten-Eigenschaften!“


Schritt 7: SpEL Master Class – Komplexe Expressions

SpEL (Spring Expression Language) ist MÄCHTIG! Lass uns fortgeschrittene Patterns lernen:

Pattern 1: Method-Parameter nutzen

@PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
public User getUserProfile(String username) {
    // User darf nur SEIN Profil sehen, oder ist Admin
}

#username = Method-Parameter username

authentication.name = Aktueller User aus SecurityContext

Pattern 2: Object-Properties prüfen

@PreAuthorize("#person.owner.username == authentication.name")
public void update(Person person) {
    // Person-Objekt als Parameter, prüfe Owner
}

Pattern 3: Collection-Filtering

@PostFilter("filterObject.owner.username == authentication.name or hasRole('ADMIN')")
public List<Person> findAll() {
    return personRepository.findAll();
}

@PostFilter filtert NACH Methoden-Ausführung!

filterObject = Jedes Element in der Liste

Ergebnis:

DB hat 100 Persons
→ findAll() gibt 100 Persons zurück
→ @PostFilter filtert: Nur Persons vom aktuellen User
→ Result: 10 Persons (die dem User gehören)

Performance-Warnung: @PostFilter lädt ALLE aus DB, filtert dann! Bei vielen Daten besser Query anpassen!

Pattern 4: Kombinierte Conditions

@PreAuthorize("(#id == null and hasAuthority('WRITE_PRIVILEGES')) or " +
              "(@securityExpressions.isOwner(#id) and hasAuthority('WRITE_PRIVILEGES'))")
public Person save(Long id, Person person) {
    // Neu erstellen (id == null): Braucht nur WRITE_PRIVILEGES
    // Update (id != null): Braucht Owner + WRITE_PRIVILEGES
}

Pattern 5: Custom Method mit Multiple Parameters

@Component("securityExpressions")
@RequiredArgsConstructor
public class CustomSecurityExpressions {
    
    public boolean canModify(Long personId, String action) {
        // Aktueller User
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        // Admin darf alles
        if (auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return true;
        }
        
        // Person laden
        Person person = personRepository.findById(personId).orElse(null);
        if (person == null) return false;
        
        boolean isOwner = person.getOwner().getUsername().equals(auth.getName());
        
        // Action-basierte Logic
        return switch (action) {
            case "READ" -> true;  // Jeder darf lesen
            case "UPDATE" -> isOwner;  // Nur Owner darf updaten
            case "DELETE" -> isOwner && 
                auth.getAuthorities().stream()
                    .anyMatch(a -> a.getAuthority().equals("DELETE_PRIVILEGES"));
            default -> false;
        };
    }
}

Nutzen:

@PreAuthorize("@securityExpressions.canModify(#id, 'UPDATE')")
public Person update(Long id, Person person) { ... }

@PreAuthorize("@securityExpressions.canModify(#id, 'DELETE')")
public void delete(Long id) { ... }

SpEL Built-in Functions

// String Operations
@PreAuthorize("#username.startsWith('admin_')")
@PreAuthorize("#email.contains('@company.com')")

// Null-Safe Navigation
@PreAuthorize("#person?.owner?.username == authentication.name")

// Logical Operators
@PreAuthorize("hasRole('ADMIN') and !#person.confidential")
@PreAuthorize("hasRole('ADMIN') or hasRole('MODERATOR')")

// Method Calls
@PreAuthorize("@myService.isAllowed(#id)")
@PreAuthorize("T(java.time.LocalDate).now().isAfter(#person.expiryDate)")

// Collections
@PreAuthorize("#roles.contains('ADMIN')")
@PreAuthorize("#permissions.?[authority == 'DELETE'].size() > 0")

🎉 AHA-Moment #7: „SpEL ist eine vollständige Expression Language! Ich kann Method-Parameter nutzen (#id), auf Properties zugreifen (#person.owner.username), Spring Beans aufrufen (@myService), und sogar Java-Klassen verwenden (T(LocalDate)). Das ist extrem mächtig für komplexe Authorization-Rules!“


🔵 BONUS: JWT Token Authentication

Session-basierte Authentication ist gut für Websites, aber nicht ideal für REST APIs!

Das Problem mit Sessions:

1. Client loggt ein → Server erstellt Session
2. Server speichert Session in Memory/DB
3. Client bekommt JSESSIONID Cookie
4. Bei jedem Request: Cookie → Server prüft Session in DB
5. Multi-Server: Session muss geteilt werden (Redis, Sticky Sessions)

Nachteile:

  • ❌ Stateful – Server muss Sessions speichern
  • ❌ Scaling schwierig – Sessions müssen synchronisiert werden
  • ❌ Cookies funktionieren nicht gut mit Mobile Apps
  • ❌ CORS kompliziert mit Cookies

Lösung: JWT (JSON Web Token)!

Was ist JWT?

JWT ist ein Token im JSON Format:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTY5ODQ4MzIwMCwiZXhwIjoxNjk4NDg2ODAwfQ.4X8s7_KqJ9p3K8vN2xR1mF5lQ8wE3pY7nC9dT2aB6Hk

Anatomie:

HEADER.PAYLOAD.SIGNATURE

HEADER (Base64):
{
  "alg": "HS256",
  "typ": "JWT"
}

PAYLOAD (Base64):
{
  "sub": "admin",           ← Username
  "iat": 1698483200,        ← Issued At
  "exp": 1698486800,        ← Expiry
  "authorities": ["ROLE_ADMIN", "DELETE_PRIVILEGES"]
}

SIGNATURE:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

Vorteile:

  • ✅ Stateless – Keine Server-Side Session
  • ✅ Self-contained – Alle Infos im Token
  • ✅ Scaling easy – Jeder Server kann validieren
  • ✅ Mobile-friendly – Nur HTTP Header
  • ✅ CORS-friendly – Kein Cookie

JWT Dependencies

Datei: pom.xml

<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

JWT Utility Class

Datei: src/main/java/com/javafleet/personmanagement/security/jwt/JwtTokenProvider.java

package com.javafleet.personmanagement.security.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.stream.Collectors;

@Component
@Slf4j
public class JwtTokenProvider {
    
    private final SecretKey secretKey;
    
    @Value("${app.jwt.expiration:86400000}")  // 24h default
    private long jwtExpirationMs;
    
    public JwtTokenProvider(@Value("${app.jwt.secret:MySecretKeyForJWTTokenGenerationThatIsLongEnough}") String secret) {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes());
    }
    
    public String generateToken(Authentication authentication) {
        String username = authentication.getName();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
        
        // Authorities als String sammeln
        String authorities = authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));
        
        return Jwts.builder()
            .subject(username)
            .claim("authorities", authorities)
            .issuedAt(now)
            .expiration(expiryDate)
            .signWith(secretKey)
            .compact();
    }
    
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
            .verifyWith(secretKey)
            .build()
            .parseSignedClaims(token)
            .getPayload();
        
        return claims.getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                .verifyWith(secretKey)
                .build()
                .parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            log.error("Invalid JWT token: {}", e.getMessage());
            return false;
        }
    }
}

JWT Authentication Filter

Datei: src/main/java/com/javafleet/personmanagement/security/jwt/JwtAuthenticationFilter.java

package com.javafleet.personmanagement.security.jwt;

import com.javafleet.personmanagement.security.CustomUserDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider tokenProvider;
    private final CustomUserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        try {
            // 1. JWT aus Request holen
            String jwt = getJwtFromRequest(request);
            
            // 2. Token validieren
            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                
                // 3. Username aus Token extrahieren
                String username = tokenProvider.getUsernameFromToken(jwt);
                
                // 4. User aus DB laden
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                // 5. Authentication-Objekt erstellen
                UsernamePasswordAuthenticationToken authentication = 
                    new UsernamePasswordAuthenticationToken(
                        userDetails, 
                        null, 
                        userDetails.getAuthorities()
                    );
                authentication.setDetails(
                    new WebAuthenticationDetailsSource().buildDetails(request)
                );
                
                // 6. In SecurityContext setzen
                SecurityContextHolder.getContext().setAuthentication(authentication);
                
                log.debug("Set Authentication for user: {}", username);
            }
        } catch (Exception ex) {
            log.error("Could not set user authentication in security context", ex);
        }
        
        // 7. Weiter zur nächsten Filter
        filterChain.doFilter(request, response);
    }
    
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        
        // Format: "Bearer <token>"
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);  // "Bearer " entfernen
        }
        
        return null;
    }
}

Was macht dieser Filter?

Erinnerst du dich an Tag 4? Dort haben wir über die SecurityFilterChain gelernt – eine Kette von Filtern!

JwtAuthenticationFilter ist ein EIGENER Filter:

Request mit JWT Token
  ↓
[1] JwtAuthenticationFilter  ← UNSER Filter!
  ↓ Extrahiert Token aus Header
  ↓ Validiert Token
  ↓ Lädt User aus DB
  ↓ Setzt Authentication in SecurityContext
  ↓
[2] FilterSecurityInterceptor
  ↓ Prüft Authorization (hasRole, etc.)
  ↓
Controller

OncePerRequestFilter stellt sicher: Filter wird nur EINMAL pro Request ausgeführt!

Auth Controller für Login

Datei: src/main/java/com/javafleet/personmanagement/controller/AuthApiController.java

package com.javafleet.personmanagement.controller;

import com.javafleet.personmanagement.security.jwt.JwtTokenProvider;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthApiController {
    
    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        
        // 1. Authenticate mit Username + Password
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );
        
        // 2. Set Authentication in SecurityContext
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 3. Generate JWT Token
        String jwt = tokenProvider.generateToken(authentication);
        
        // 4. Return Token
        return ResponseEntity.ok(new JwtResponse(jwt));
    }
    
    @Data
    public static class LoginRequest {
        private String username;
        private String password;
    }
    
    @Data
    @RequiredArgsConstructor
    public static class JwtResponse {
        private final String token;
        private final String type = "Bearer";
    }
}

Was passiert hier?

1. Authentication:

Authentication auth = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(username, password)
);

Das macht:

  • Ruft CustomUserDetailsService.loadUserByUsername() auf
  • Vergleicht Password mit PasswordEncoder.matches()
  • Bei Erfolg: Authentication-Objekt
  • Bei Fehler: BadCredentialsException

2. JWT generieren:

String jwt = tokenProvider.generateToken(authentication);

Erstellt Token mit:

  • Username
  • Authorities
  • Expiry Date
  • Signature

3. Token zurückgeben:

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "type": "Bearer"
}

SecurityConfig für JWT

Datei: src/main/java/com/javafleet/personmanagement/security/SecurityConfig.java

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    
    private final CustomUserDetailsService userDetailsService;
    private final JwtAuthenticationFilter jwtAuthFilter;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // JWT braucht kein CSRF
            
            .authorizeHttpRequests(auth -> auth
                // Public endpoints
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/", "/login", "/register").permitAll()
                
                // Admin endpoints
                .requestMatchers("/admin/**").hasRole("ADMIN")
                
                // API endpoints
                .requestMatchers("/api/**").authenticated()
                
                .anyRequest().authenticated()
            )
            
            // JWT Filter hinzufügen
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
            
            // Session Management: STATELESS!
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            
            .formLogin(form -> form.loginPage("/login").permitAll())
            .logout(logout -> logout.permitAll())
            .httpBasic(basic -> {});
        
        return http.build();
    }
}

Was ist neu?

1. JWT Filter registrieren:

.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)

Das fügt unseren Filter VOR dem Standard-Login-Filter ein!

2. Stateless Session:

.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

Das bedeutet:

  • ❌ Keine Server-Side Session
  • ❌ Kein JSESSIONID Cookie
  • ✅ Jeder Request braucht JWT Token

application.properties

# JWT Configuration
app.jwt.secret=MyVerySecretKeyForJWTTokenGenerationThatMustBeLongEnoughForHS512Algorithm
app.jwt.expiration=86400000

Wichtig:

  • app.jwt.secret muss mindestens 256 Bit lang sein (32+ Zeichen)
  • app.jwt.expiration in Millisekunden (86400000 = 24h)

Testen mit JWT

1. Login und Token holen:

curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

Response:

{
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjoiUk9MRV9BRE1JTixSRUFEX1BSSVZJTEVHRVMsV1JJVEVfUFJJVklMRUdFUyxERUxFVEVfUFJJVklMRUdFUyIsImlhdCI6MTY5ODQ4MzIwMCwiZXhwIjoxNjk4NTY5NjAwfQ.dGhpc19pc19hX2Zha2Vfc2lnbmF0dXJlX2Zvcl9kZW1v",
  "type": "Bearer"
}

2. Token kopieren und API aufrufen:

TOKEN="eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjoiUk9MRV9BRE1JTixSRUFEX1BSSVZJTEVHRVMsV1JJVEVfUFJJVklMRUdFUyxERUxFVEVfUFJJVklMRUdFUyIsImlhdCI6MTY5ODQ4MzIwMCwiZXhwIjoxNjk4NTY5NjAwfQ.dGhpc19pc19hX2Zha2Vfc2lnbmF0dXJlX2Zvcl9kZW1v"

curl http://localhost:8080/api/persons \
  -H "Authorization: Bearer $TOKEN"

Response:

[
  {
    "id": 1,
    "firstname": "Alice",
    "lastname": "Miller",
    "email": "alice@example.com"
  }
]

✅ Funktioniert ohne Session/Cookie!

3. Token in Postman nutzen:

1. POST zu http://localhost:8080/api/auth/login
2. Body: {"username":"admin","password":"admin123"}
3. Token aus Response kopieren
4. In Postman: Authorization → Type: Bearer Token
5. Token einfügen
6. Alle Requests funktionieren! ✅

4. Token läuft ab:

# Nach 24h (oder was du konfiguriert hast):
curl http://localhost:8080/api/persons \
  -H "Authorization: Bearer $TOKEN"

# Response:
{
  "error": "Unauthorized",
  "message": "JWT token has expired"
}

User muss neu einloggen und neuen Token holen!

Token Refresh (Optional)

Problem: Nach 24h muss User sich neu einloggen. Nervig!

Lösung: Refresh Token Pattern!

Datei: src/main/java/com/javafleet/personmanagement/controller/AuthApiController.java

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthApiController {
    
    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider tokenProvider;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                loginRequest.getUsername(),
                loginRequest.getPassword()
            )
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        String accessToken = tokenProvider.generateToken(authentication);
        String refreshToken = tokenProvider.generateRefreshToken(authentication);
        
        return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refresh(@RequestBody RefreshRequest refreshRequest) {
        String refreshToken = refreshRequest.getRefreshToken();
        
        if (tokenProvider.validateToken(refreshToken)) {
            String username = tokenProvider.getUsernameFromToken(refreshToken);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            Authentication authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities()
            );
            
            String newAccessToken = tokenProvider.generateToken(authentication);
            
            return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
        }
        
        return ResponseEntity.status(401).body("Invalid refresh token");
    }
    
    @Data
    public static class LoginRequest {
        private String username;
        private String password;
    }
    
    @Data
    public static class RefreshRequest {
        private String refreshToken;
    }
    
    @Data
    public static class JwtResponse {
        private final String accessToken;
        private final String refreshToken;
        private final String type = "Bearer";
        
        public JwtResponse(String accessToken, String refreshToken) {
            this.accessToken = accessToken;
            this.refreshToken = refreshToken;
        }
    }
}

JwtTokenProvider erweitern:

@Component
@Slf4j
public class JwtTokenProvider {
    
    private final SecretKey secretKey;
    
    @Value("${app.jwt.expiration:3600000}")  // 1h Access Token
    private long jwtExpirationMs;
    
    @Value("${app.jwt.refresh-expiration:604800000}")  // 7 Tage Refresh Token
    private long refreshExpirationMs;
    
    // ... bestehende Methoden
    
    public String generateRefreshToken(Authentication authentication) {
        String username = authentication.getName();
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + refreshExpirationMs);
        
        return Jwts.builder()
            .subject(username)
            .claim("type", "refresh")
            .issuedAt(now)
            .expiration(expiryDate)
            .signWith(secretKey)
            .compact();
    }
}

Pattern:

1. Login:
   → Access Token (1h)
   → Refresh Token (7 Tage)

2. Nach 1h: Access Token expired
   → Client sendet Refresh Token zu /api/auth/refresh
   → Server gibt NEUEN Access Token
   → Refresh Token bleibt gültig

3. Nach 7 Tagen: Refresh Token expired
   → User muss neu einloggen

Vorteile:

  • Access Token ist kurzlebig (sicherer)
  • User muss nicht alle 1h neu einloggen
  • Refresh Token kann invalidiert werden (Logout)

🎉 AHA-Moment #8: „JWT macht meine API stateless! Kein Session-Storage mehr – der Token enthält ALLE Infos die ich brauche. Jeder Server kann den Token validieren ohne DB-Lookup. Das ist perfekt für REST APIs und Microservices!“

📝 Wichtige Ressourcen zur Security


✅ Checkpoint: Hast du Tag 5 geschafft?

Grundlagen (🟢):

  • [ ] Du verstehst den Unterschied zwischen Authentication (Tag 4) und Authorization (Tag 5)
  • [ ] Du kannst URLs mit hasRole() schützen
  • [ ] Du kennst den Unterschied zwischen hasRole() und hasAuthority()
  • [ ] Du hast @PreAuthorize auf Methoden angewendet
  • [ ] Du verstehst wie @EnableMethodSecurity funktioniert
  • [ ] Du kannst den aktuellen User mit @AuthenticationPrincipal holen

Professional (🟡):

  • [ ] Du hast Custom Security Expressions erstellt
  • [ ] Du verstehst Ownership-basierte Authorization
  • [ ] Du kannst @PostAuthorize für Response-Filterung nutzen
  • [ ] Du beherrschst SpEL Expressions in Security
  • [ ] Du kannst komplexe Authorization-Rules implementieren
  • [ ] Du verstehst wie SecurityContext und ThreadLocal funktionieren

Bonus (🔵):

  • [ ] Du hast JWT Token Authentication implementiert
  • [ ] Du verstehst den Unterschied zwischen Stateful (Session) und Stateless (JWT)
  • [ ] Du kannst Access Token und Refresh Token nutzen
  • [ ] Du hast eine moderne REST API Security gebaut

Alles ✅? Morgen geht’s mit Caching weiter!

Nicht alles funktioniert?

  • Überprüfe ob @EnableMethodSecurity aktiviert ist
  • Kontrolliere ob Custom Security Expressions als @Component registriert sind
  • Stelle sicher dass JWT Secret lang genug ist (32+ Zeichen)
  • Check ob JwtAuthenticationFilter registriert ist
  • Lade das komplette Projekt unten herunter

🔥 Elyndras Real Talk:

Hey Developer,

Authorization war für mich lange ein Rätsel. Ich habe URLs geschützt, dachte „fertig“ – und dann hatte ich Security-Löcher wie einen Schweizer Käse.

Der Wendepunkt: Als ich verstanden habe dass URL-Security NICHT genug ist. Services können von überall aufgerufen werden – Scheduled Tasks, Message Listeners, andere Services. Ohne @PreAuthorize auf der Methode? Keine Security!

Das JWT-Ding: Ich war anfangs skeptisch. „Sessions funktionieren doch?“ Aber dann haben wir Microservices gebaut und BAM – Session-Sharing zwischen Services war die Hölle. Mit JWT? Jeder Service validiert selbst. Game changer!

Ein Fehler den ich gemacht habe: Ich habe mal @PostFilter auf eine Methode angewendet die 100.000 Datensätze aus der DB lädt. Der Filter hat dann 99.900 davon verworfen. PERFORMANCE-KILLER! Lesson learned: @PostFilter ist für kleine Listen, bei großen Daten die Query anpassen!

Mein Lieblings-Feature: Custom Security Expressions! @securityExpressions.isOwner(#id) liest sich wie Plain English und versteckt komplexe Business-Logic. Marcus sagt ich bin besessen davon – er hat recht! 😄

Real-Talk: Ich habe WOCHENLANG gebraucht um SpEL wirklich zu verstehen. Diese #parameter und returnObject Syntax war verwirrend. Aber jetzt? Ich schreibe Security-Rules in 2 Minuten die früher einen halben Tag gedauert haben.

Ein Tipp: Fang mit einfachen hasRole() Rules an. Dann steigere dich zu Custom Expressions. Nicht alles auf einmal – Security-Code muss man VERSTEHEN, nicht nur kopieren!

Morgen: Caching! Wir machen deine App SCHNELL. Stell dir vor: Datenbank-Queries die 500ms dauern? → 5ms mit Caching! Das wird cool! 🚀

Keep securing! 🔒

Elyndra


❓ FAQ (Häufige Fragen)

Q: Wann nutze ich hasRole() vs hasAuthority()?
A: hasRole() für breite Kategorien (ADMIN, USER), hasAuthority() für spezifische Permissions (DELETE_PRIVILEGES). Ein User kann beides haben!

Q: Ist JWT sicherer als Sessions?
A: Nicht unbedingt sicherer, aber anders! Sessions sind gut für Websites (können invalidiert werden), JWT ist gut für APIs (stateless). Wähle basierend auf Use-Case!

Q: Wie invalidiere ich einen JWT Token?
A: JWT ist stateless – du kannst ihn nicht server-side invalidieren! Lösungen: 1) Kurze Expiry (1h), 2) Token Blacklist in Redis, 3) Refresh Token widerrufen.

Q: @PreAuthorize vs URL-basierte Security – was ist besser?
A: BEIDES nutzen! URLs für Basis-Schutz, @PreAuthorize für Method-Level Security. Defense in Depth!

Q: Kann ich @PreAuthorize auf private Methoden anwenden?
A: Nein! Spring Security nutzt Proxies – funktioniert nur auf public Methoden in Spring Beans!

Q: Was ist besser: @Secured oder @PreAuthorize?
A: @PreAuthorize! Es unterstützt SpEL Expressions. @Secured ist legacy und kann nur Roles prüfen.

Q: Wie teste ich Security-Rules?
A: Mit Spring Security Test! @WithMockUser, @WithUserDetails. Das kommt in Tag 8!

Q: JWT Secret – wie lang muss er sein?
A: Minimum 256 Bit (32 Zeichen) für HS256. Besser: 512 Bit (64 Zeichen). Nutze einen zufällig generierten String, nicht „password123“!


📅 Nächster Kurstag: Tag 6

Morgen im Kurs / Nächster Blogbeitrag:

„Tag 6: Caching & Serialisierung – Performance auf Steroiden“

Was du lernen wirst:

  • Spring Cache Abstraction
  • @Cacheable, @CacheEvict, @CachePut
  • Redis als Cache Backend
  • Cache Strategies (Read-Through, Write-Through)
  • Serialisierung mit Jackson
  • Custom Serializers für komplexe Objekte
  • Cache Patterns & Best Practices

Warum wichtig? Database-Queries sind langsam! Mit Caching machst du deine App 10-100x schneller. Redis ist der Industry-Standard für distributed Caching.

Voraussetzung: Tag 1-5 abgeschlossen – besonders JPA und Security musst du verstehen!

👉 Zum Blogbeitrag Tag 6 (erscheint morgen)


📚 Deine Fortschritts-Übersicht

TagThemaStatus
✅ 1Auto-Configuration & Custom StarterABGESCHLOSSEN! 🎉
✅ 2Spring Data JPA BasicsABGESCHLOSSEN! 🎉
✅ 3JPA Relationships & QueriesABGESCHLOSSEN! 🎉
✅ 4Spring Security – Part 1ABGESCHLOSSEN! 🎉
✅ 5Spring Security – Part 2ABGESCHLOSSEN! 🎉
→ 6Caching & SerialisierungAls nächstes
7Messaging & EmailNoch offen
8Testing & DokumentationNoch offen
9Spring Boot ActuatorNoch offen
10Template Engines & MicroservicesNoch offen

Du hast 50% des Kurses geschafft! 💪 HALFWAY POINT!

Alle Blogbeiträge dieser Serie:
👉 Spring Boot Aufbau-Kurs – Komplette Übersicht


📥 Download & Ressourcen

Projekt zum Download:
👉 person-management-security-part2-v1.0.zip (Stand: 20.10.2025)

Was ist im ZIP enthalten:

  • ✅ Komplettes Maven-Projekt mit Method Security
  • ✅ URL-basierte und Method-basierte Authorization
  • ✅ Custom Security Expressions
  • ✅ Ownership-basierte Authorization
  • ✅ JWT Token Authentication (Optional)
  • ✅ Refresh Token Mechanismus
  • ✅ Test-User mit verschiedenen Roles und Authorities
  • ✅ Postman Collection für API-Tests

Projekt starten:

# ZIP entpacken
cd person-management
mvn spring-boot:run

# Browser öffnen
http://localhost:8080

# JWT Login testen
curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

Probleme? Issue melden oder schreib mir: elyndra@java-developer.online


📖 Zusammenfassung: Was du heute gelernt hast

Authorization Grundlagen:

  • ✅ Unterschied zwischen Authentication (wer?) und Authorization (was?)
  • ✅ URL-basierte Security mit hasRole() und hasAuthority()
  • ✅ Role vs Authority – breite Kategorien vs spezifische Permissions
  • ✅ @ElementCollection für User Authorities

Method Security:

  • ✅ @EnableMethodSecurity aktiviert Method-Level Security
  • ✅ @PreAuthorize prüft VOR Methoden-Ausführung
  • ✅ @PostAuthorize prüft NACH Methoden-Ausführung und kann Response filtern
  • ✅ SpEL Expressions für komplexe Rules

Advanced Authorization:

  • ✅ Custom Security Expressions (@securityExpressions.isOwner())
  • ✅ Ownership-basierte Authorization (User darf nur EIGENE Daten)
  • ✅ @AuthenticationPrincipal und @CurrentUser für aktuellen User
  • ✅ SecurityContext und ThreadLocal verstehen

JWT Token Authentication:

  • ✅ Stateless vs Stateful Authentication
  • ✅ JWT Struktur (Header, Payload, Signature)
  • ✅ JwtTokenProvider für Token-Generierung
  • ✅ JwtAuthenticationFilter für Token-Validierung
  • ✅ Access Token + Refresh Token Pattern

Best Practices:

  • ✅ Defense in Depth: URL-Security + Method-Security
  • ✅ Kurze Expiry für Access Tokens (1h)
  • ✅ Refresh Tokens für bessere User Experience
  • ✅ Custom Expressions für lesbare Security-Rules
  • ✅ Admin darf immer alles – Owner-Checks nur für normale User

Das war Tag 5 vom Spring Boot Aufbau-Kurs!

Du kannst jetzt:

  • ✅ URLs und Methoden für verschiedene Roles schützen
  • ✅ Den Unterschied zwischen Role und Authority nutzen
  • ✅ Custom Security Expressions schreiben
  • ✅ Ownership-basierte Authorization implementieren
  • ✅ SpEL Expressions meistern
  • ✅ JWT Token Authentication bauen
  • ✅ Stateless REST APIs absichern
  • ✅ Access Token + Refresh Token Mechanismen nutzen

Morgen machen wir deine App SCHNELL mit Caching! 🚀

Keep coding, keep learning! 💙


Tag 6 erscheint morgen. Bis dahin: Happy Coding!

„Authorization ist die Kunst zu sagen: Du darfst das, aber nicht jenes!“ – Elyndra Valen


Tags: #SpringBoot #SpringSecurity #Authorization #MethodSecurity #JWT #PreAuthorize #Tutorial #Tag5

Autor

  • Elyndra Valen

    28 Jahre alt, wurde kürzlich zur Senior Entwicklerin befördert nach 4 Jahren intensiver Java-Entwicklung. Elyndra kennt die wichtigsten Frameworks und Patterns, beginnt aber gerade erst, die tieferen Zusammenhänge und Architektur-Entscheidungen zu verstehen. Sie ist die Brücke zwischen Junior- und Senior-Welt im Team.