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)

📚 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?
| Aspekt | Alt | Neu |
|---|---|---|
| Klassen-Struktur | extends WebSecurityConfigurerAdapter | Keine Vererbung, nur @Configuration |
| HTTP-Konfiguration | configure(HttpSecurity) override | @Bean SecurityFilterChain |
| Auth-Konfiguration | configure(AuthenticationManagerBuilder) | @Bean UserDetailsService |
| AuthenticationManager | authenticationManagerBean() override | Injection via AuthenticationConfiguration |
| Request Matching | antMatchers(), mvcMatchers() | requestMatchers() |
| Autorisierung | authorizeRequests() | 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 wiemvcMatchers(). 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?
- Unit Tests:
@WithMockUser,@WithSecurityContext - Integration Tests:
@SpringBootTest+MockMvc - Manuelle Tests: Für kritische Flows (Login, Logout, Token-Refresh)
- 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:
- Sandbox-Projekt aufsetzen
- Migrationsplan für dein Team erstellen
- Stufenweise migrieren (nicht alles auf einmal!)
- 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
📚 Das könnte dich auch interessieren
🙏 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! 👋

