Von Code Sentinel & Elyndra Valen, Java Fleet Systems Consulting

Schwierigkeit: 🔴 Fortgeschritten
Lesezeit: 30 Minuten
Voraussetzungen: Teil 1 + 2 gelesen, Spring Security 6.x Erfahrung
Serie: Spring Boot 4 Migration (Teil 3 von 3)

Security

📚 Was bisher geschah – Spring Boot 4 Migration

Bereits veröffentlicht:

  • Teil 1: Was bringt Spring Boot 4? – Neuerungen, Breaking Changes, Entscheidungshilfe
  • Teil 2: Migration Step-by-Step – pom.xml, Compile-Fehler, OpenRewrite

Heute: Teil 3 – Spring Security 7 Migration

Wichtig: Dieser Artikel baut auf Teil 2 auf. Wenn deine Basis-Migration noch nicht abgeschlossen ist, starte dort.


⚡ Das Wichtigste in 30 Sekunden

Dein Problem: Spring Security 7 kommt mit Spring Boot 4. Security-Konfiguration war schon immer komplex – und jetzt gibt’s Breaking Changes.

Die Lösung: Systematische Migration deiner Security-Config mit klaren Patterns.

Heute lernst du:

  • ✅ Was sich in Spring Security 7 geändert hat
  • ✅ SecurityFilterChain richtig konfigurieren
  • ✅ Die häufigsten Security-Migrationsfehler vermeiden
  • ✅ OAuth2 und JWT-Konfiguration anpassen

Für wen ist dieser Artikel?

  • 🌿 Mit Security-Erfahrung: Du bekommst die konkreten Änderungen
  • 🌳 Security-Profis: Im Bonus: Method Security, CORS, CSRF Deep Dive

Zeit-Investment: 30 Minuten lesen, je nach Projekt 2-8 Stunden Migration


👋 Code Sentinel: „Security ist mein Terrain!“

Hey! 👋

Code Sentinel hier. Security ist mein Spezialgebiet – und ja, ich weiß, dass Spring Security-Migrationen gefürchtet sind. Aber hier ist die gute Nachricht:

Wenn du bereits auf der Lambda-DSL bist (Security 6.x), sind die Änderungen minimal.

Die meisten Breaking Changes betreffen Legacy-Code, der schon seit Security 5.7 deprecated war. Wenn du damals aufgeräumt hast, ist heute dein Erntetag.

Elyndra: „Und wenn nicht, ist heute der Tag zum Aufräumen. Wir zeigen dir jeden Schritt.“

Los geht’s – aber vorsichtig! 🛡️


🟢 GRUNDLAGEN

Was ist neu in Spring Security 7?

Spring Security 7 kommt mit Spring Framework 7 und Spring Boot 4. Die wichtigsten Änderungen:

1. Finale Entfernung deprecateder APIs

Alles, was in Security 5.7-6.x deprecated war, ist jetzt weg:

  • WebSecurityConfigurerAdapter
  • antMatchers(), mvcMatchers()
  • Alte authorizeRequests() API ❌

2. JSpecify Null-Safety

Security-APIs haben jetzt portfolio-weite Null-Annotations:

@Nullable Authentication getAuthentication();
@NonNull SecurityContext getContext();

3. Verbesserte Defaults

  • Strengere CSRF-Defaults
  • Verbesserte Session-Fixation-Protection
  • Modernere Cipher-Suites für HTTPS

4. Observability-Integration

Bessere Integration mit Micrometer für Security-Metriken:

  • Login-Versuche
  • Autorisierungs-Entscheidungen
  • Session-Events

Quick-Check: Wie schlimm wird’s für mich?

┌─────────────────────────────────────────────────────────┐
│  DEINE SITUATION                    │  AUFWAND         │
├─────────────────────────────────────┼──────────────────┤
│  Bereits Lambda-DSL (Security 6.x)  │  🟢 Minimal      │
│  SecurityFilterChain Beans          │  🟢 Minimal      │
├─────────────────────────────────────┼──────────────────┤
│  Noch antMatchers/mvcMatchers       │  🟡 Moderat      │
│  authorizeRequests() statt          │  🟡 Moderat      │
│  authorizeHttpRequests()            │                  │
├─────────────────────────────────────┼──────────────────┤
│  WebSecurityConfigurerAdapter       │  🔴 Signifikant  │
│  Custom AuthenticationProvider      │  🔴 Signifikant  │
│  Komplexe Filter-Chains             │  🔴 Signifikant  │
└─────────────────────────────────────┴──────────────────┘

🟡 PROFESSIONALS

Die große Migration: WebSecurityConfigurerAdapter → SecurityFilterChain

Das ist der häufigste Migrationsfall. Wenn du noch WebSecurityConfigurerAdapter verwendest, ist jetzt der Moment.

Vorher (Legacy – funktioniert nicht mehr!)

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/api/**").authenticated()
                .antMatchers("/admin/**").hasRole("ADMIN")
            .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
            .and()
            .logout()
                .permitAll();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password("{noop}password").roles("USER")
            .and()
            .withUser("admin").password("{noop}admin").roles("ADMIN");
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

Nachher (Modern – Spring Security 7)

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .requestMatchers("/api/**").authenticated()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            );
        
        return http.build();
    }
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password("{noop}password")
            .roles("USER")
            .build();
        
        UserDetails admin = User.builder()
            .username("admin")
            .password("{noop}admin")
            .roles("ADMIN")
            .build();
        
        return new InMemoryUserDetailsManager(user, admin);
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

Elyndra: „Lass uns die Unterschiede verstehen – nicht nur kopieren.“

Was hat sich geändert?

AspektAltNeu
Klassen-Strukturextends WebSecurityConfigurerAdapterKeine Vererbung, nur @Configuration
HTTP-Konfigurationconfigure(HttpSecurity) override@Bean SecurityFilterChain
Auth-Konfigurationconfigure(AuthenticationManagerBuilder)@Bean UserDetailsService
AuthenticationManagerauthenticationManagerBean() overrideInjection via AuthenticationConfiguration
Request MatchingantMatchers(), mvcMatchers()requestMatchers()
AutorisierungauthorizeRequests()authorizeHttpRequests()

Matcher-Migration im Detail

antMatchers → requestMatchers

// ❌ ALT
.antMatchers("/api/**").authenticated()
.antMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
.antMatchers("/static/**").permitAll()

// ✅ NEU
.requestMatchers("/api/**").authenticated()
.requestMatchers(HttpMethod.POST, "/users").hasRole("ADMIN")
.requestMatchers("/static/**").permitAll()

mvcMatchers → requestMatchers

// ❌ ALT
.mvcMatchers("/users/{id}").hasRole("USER")

// ✅ NEU
.requestMatchers("/users/{id}").hasRole("USER")

💡 Was ist der Unterschied?

In Security 6/7 erkennt requestMatchers() automatisch, ob Spring MVC verfügbar ist. Wenn ja, verhält es sich wie mvcMatchers(). Das manuelle Unterscheiden ist nicht mehr nötig.

regexMatchers → requestMatchers mit RegexRequestMatcher

// ❌ ALT
.regexMatchers("/api/v[0-9]+/.*").authenticated()

// ✅ NEU
.requestMatchers(new RegexRequestMatcher("/api/v[0-9]+/.*", null)).authenticated()

authorizeRequests → authorizeHttpRequests

Das ist mehr als nur ein Namensänderung:

// ❌ ALT – authorizeRequests()
http.authorizeRequests()
    .antMatchers("/public/**").permitAll()
    .anyRequest().authenticated();

// ✅ NEU – authorizeHttpRequests()
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/public/**").permitAll()
    .anyRequest().authenticated()
);

Wichtiger Unterschied:

authorizeHttpRequests() verwendet AuthorizationManager statt AccessDecisionManager. Das bedeutet:

  • Bessere Performance (keine Vote-Aggregation)
  • Einfacheres Debugging
  • Bessere Testbarkeit

CSRF-Konfiguration

CSRF ist in Security 7 strenger konfiguriert:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // CSRF für APIs deaktivieren (wenn stateless)
        .csrf(csrf -> csrf
            .ignoringRequestMatchers("/api/**")
        )
        
        // ODER: CSRF komplett deaktivieren (nur für stateless APIs!)
        .csrf(csrf -> csrf.disable())
        
        // ODER: Custom CSRF Token Repository
        .csrf(csrf -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        );
    
    return http.build();
}

Code Sentinel: „CSRF nur deaktivieren wenn du weißt, was du tust. Für stateless JWT-APIs ist es okay. Für Session-basierte Apps: NICHT deaktivieren!“


Session-Management

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            // Für stateless APIs
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            
            // ODER: Für Session-basierte Apps
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .maximumSessions(1)
            .expiredUrl("/session-expired")
        );
    
    return http.build();
}

OAuth2 / JWT Konfiguration

Wenn du OAuth2 Resource Server verwendest:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/public/**").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt
                .jwtAuthenticationConverter(jwtAuthenticationConverter())
            )
        );
    
    return http.build();
}

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
    converter.setAuthoritiesClaimName("roles");
    converter.setAuthorityPrefix("ROLE_");
    
    JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
    jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
    return jwtConverter;
}

application.yml:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://your-auth-server.com
          # ODER
          jwk-set-uri: https://your-auth-server.com/.well-known/jwks.json

🔵 BONUS

Method Security

Method Security bleibt weitgehend gleich, aber es gibt Verbesserungen:

@Configuration
@EnableMethodSecurity  // Ersetzt @EnableGlobalMethodSecurity
public class MethodSecurityConfig {
}
@Service
public class UserService {
    
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) {
        // Nur Admins
    }
    
    @PreAuthorize("#username == authentication.name")
    public UserProfile getProfile(String username) {
        // Nur eigenes Profil
    }
    
    @PostAuthorize("returnObject.owner == authentication.name")
    public Document getDocument(Long id) {
        // Prüfung nach Ausführung
    }
}

Neu in Security 7:

  • Bessere SpEL-Unterstützung
  • Authorization-Events für Observability
  • Verbesserte Fehlermeldungen

CORS-Konfiguration

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .cors(cors -> cors
            .configurationSource(corsConfigurationSource())
        );
    
    return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("https://frontend.example.com"));
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
    config.setAllowedHeaders(List.of("*"));
    config.setAllowCredentials(true);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/api/**", config);
    return source;
}

Multiple Security Filter Chains

Für komplexe Setups mit verschiedenen Security-Anforderungen:

@Configuration
@EnableWebSecurity
public class MultiSecurityConfig {
    
    // API-Security (stateless, JWT)
    @Bean
    @Order(1)
    public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
        
        return http.build();
    }
    
    // Web-Security (session-basiert, Form-Login)
    @Bean
    @Order(2)
    public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .logout(Customizer.withDefaults());
        
        return http.build();
    }
}

Code Sentinel: „Die Reihenfolge ist wichtig! @Order bestimmt, welche Chain zuerst geprüft wird. Spezifischere Matcher (wie /api/**) sollten höhere Priorität haben.“


Security-Tests

@SpringBootTest
@AutoConfigureMockMvc
class SecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    void publicEndpoint_shouldBeAccessible() throws Exception {
        mockMvc.perform(get("/public/info"))
            .andExpect(status().isOk());
    }
    
    @Test
    void protectedEndpoint_withoutAuth_shouldReturn401() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    void protectedEndpoint_withUser_shouldBeAccessible() throws Exception {
        mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    void adminEndpoint_withAdmin_shouldBeAccessible() throws Exception {
        mockMvc.perform(get("/admin/dashboard"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    void adminEndpoint_withUser_shouldReturn403() throws Exception {
        mockMvc.perform(get("/admin/dashboard"))
            .andExpect(status().isForbidden());
    }
}

💬 Real Talk: Security-Review

Java Fleet Meeting-Raum, Donnerstag 10:00 Uhr. Code Sentinel hat ein Security-Review angesetzt.


Code Sentinel: „Okay, Security-Migration. Bevor wir deployen, gehen wir das zusammen durch.“

Nova: „Ich hab alles umgestellt auf requestMatchers und authorizeHttpRequests. Tests sind grün!“

Code Sentinel: „Gut. Zeig mir die Config.“

(Nova teilt ihren Bildschirm)

Code Sentinel: (scrollt) „Okay… okay… stopp. CSRF ist komplett disabled?“

Nova: „Ja, ist doch eine API.“

Code Sentinel: „Welche Art von API?“

Nova: „REST… mit JWT.“

Code Sentinel: „Stateless?“

Nova: „Ja?“

Code Sentinel: „Dann ist es okay. Aber dokumentier das. Kommentar im Code, warum CSRF disabled ist.“

Elyndra: (schaut von ihrem Laptop auf) „Ich würde noch empfehlen: ignoringRequestMatchers statt disable. So ist es expliziter.“

Code Sentinel: (nickt) „Bessere Idee. Nova, änder das. Und hier…“ (zeigt auf eine Stelle) „…permitAll() für /api/internal/**?“

Nova: „Das sind interne Endpoints für Health-Checks.“

Code Sentinel: „Wer ruft die auf?“

Nova: „Kubernetes. Für Liveness und Readiness Probes.“

Code Sentinel: „Okay. Aber ‚internal‘ im Pfad und permitAll ist ein Widerspruch. Entweder /health/** nennen oder IP-Whitelist.“

Elyndra: „Oder Actuator nutzen. Der hat das schon eingebaut.“

Nova: (macht sich Notizen) „Okay, verstanden. Also: CSRF explizit für API-Pfade ignorieren, Actuator für Health-Checks, Kommentare für Security-Entscheidungen.“

Code Sentinel: (grinst) „Du lernst schnell. Noch eine Sache: Tests für die Security?“

Nova: „Ja! @WithMockUser für alle geschützten Endpoints.“

Code Sentinel: „Auch negative Tests? 403 für User auf Admin-Endpoints?“

Nova: „…ich ergänze das.“

Code Sentinel: „Gut. Dann bin ich zufrieden.“


❓ Häufig gestellte Fragen

Frage 1: Ich nutze noch WebSecurityConfigurerAdapter. Wie schlimm ist das?

Das hätte schon vor 3 Jahren weg sein sollen (deprecated seit Security 5.7). Aber: Die Migration ist machbar. Es ist hauptsächlich Syntax-Umstellung, keine Logik-Änderung.

Rechne mit 2-4 Stunden für eine typische Konfiguration.


Frage 2: Funktionieren meine JWT-Token noch?

Ja. Das Token-Format ändert sich nicht. Nur die Konfiguration für die Validierung könnte sich leicht ändern – hauptsächlich Methodennamen und Lambda-Syntax.


Frage 3: Was ist mit @PreAuthorize und Method Security?

Bleibt weitgehend gleich. Die Annotation @EnableGlobalMethodSecurity heißt jetzt @EnableMethodSecurity, aber die Funktionalität ist identisch.


Frage 4: CORS funktioniert nicht mehr – was tun?

Häufiger Fehler: CORS muss VOR der Security-Chain konfiguriert werden.

http.cors(Customizer.withDefaults())  // Muss früh kommen!
    .authorizeHttpRequests(...)

Und vergiss nicht den CorsConfigurationSource Bean.


Frage 5: Meine Custom-Filter funktionieren nicht mehr

Filter-Order kann sich geändert haben. Check die Position:

http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class)

Nutze Logging, um die Filter-Chain zu debuggen:

logging:
  level:
    org.springframework.security: DEBUG

Frage 6: Wie teste ich Security-Changes richtig?

  1. Unit Tests: @WithMockUser, @WithSecurityContext
  2. Integration Tests: @SpringBootTest + MockMvc
  3. Manuelle Tests: Für kritische Flows (Login, Logout, Token-Refresh)
  4. Penetration Tests: Für Production-kritische Apps

Frage 7: Bernd meinte, er hat mal ein Security-Update ohne Tests deployed. Wie ist das ausgegangen?

(Code Sentinel atmet tief durch)

Wir nennen es „Der Vorfall“. Eine falsch konfigurierte permitAll()-Regel hat für 47 Minuten Admin-Endpoints öffentlich gemacht.

Zum Glück hat das Monitoring angeschlagen, bevor etwas passiert ist. Aber Bernd trinkt seitdem keinen Kaffee mehr während Security-Deployments.

Real talk: Security-Changes ohne Tests sind russisches Roulette. Nicht „könnte schiefgehen“ – WIRD schiefgehen. Nur eine Frage der Zeit.

Bernds Regel seitdem: „Jeder Security-Change braucht mindestens einen positiven UND einen negativen Test. Keine Ausnahmen. Niemals.“ 🔐


📦 Downloads

🛡️ spring-security-7-migration.zip

  • Vorher/Nachher Security-Config
  • Vollständige Test-Suite
  • OAuth2/JWT Beispiel
  • CORS-Konfiguration
📁 spring-security-7-migration/
├── 📁 legacy-security/
│   └── SecurityConfig.java (WebSecurityConfigurerAdapter)
├── 📁 modern-security/
│   ├── SecurityConfig.java (SecurityFilterChain)
│   ├── JwtConfig.java
│   └── CorsConfig.java
├── 📁 tests/
│   └── SecurityTest.java
└── README.md

📋 security-migration-checklist.pdf

  • Alle Security-Changes auf einen Blick
  • Checkbox für systematische Migration
  • Common Pitfalls

🔗 Externe Links

Offizielle Docs:

Tutorials:


🎯 Serie abgeschlossen!

Das war’s – du hast die komplette Spring Boot 4 Migration Serie durchgearbeitet! 🎉

Was du jetzt kannst:

  • ✅ Entscheiden, wann du migrieren solltest
  • ✅ Die Migration Schritt für Schritt durchführen
  • ✅ Spring Security 7 korrekt konfigurieren
  • ✅ Typische Fehler erkennen und beheben

Nächste Schritte:

  1. Sandbox-Projekt aufsetzen
  2. Migrationsplan für dein Team erstellen
  3. Stufenweise migrieren (nicht alles auf einmal!)
  4. Feedback geben – was hat gefehlt?

Code Sentinel ist Technical Project Manager und Security-Experte bei Java Fleet. Er hat schon mehr Migrationen überlebt, als er zählen kann – und alle mit Rollback-Plan.

Elyndra Valen ist Senior Software Architect bei Java Fleet. Sie sorgt dafür, dass du nicht nur migrierst, sondern auch verstehst, was du tust.

Fragen? Feedback?
code.sentinel@java-developer.online | elyndra.valen@java-developer.online


Teil 3 von 3 der Spring Boot 4 Migration Serie
java-developer.online • Dezember 2025


🙏 Danke fürs Lesen!

Diese Serie war ein Gemeinschaftsprojekt von Code Sentinel und Elyndra. Wenn sie dir geholfen hat:

  • ⭐ Teile sie mit Kollegen, die vor der gleichen Migration stehen
  • 💬 Gib uns Feedback – was können wir besser machen?
  • 🐛 Melde Bugs oder fehlende Szenarien

Bis zur nächsten Migration! 👋

Autoren

  • Code Sentinel

    32 Jahre alt, Technical Project Manager und Security-Experte bei Java Fleet Systems Consulting. Code ist ein erfahrener Entwickler, der in die Projektleitung aufgestiegen ist, aber immer noch tief in der Technik verwurzelt bleibt. Seine Mission: Sicherstellen, dass Projekte termingerecht, sicher und wartbar geliefert werden.

  • 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.