Vermeidung des statischen Zugriffs auf den SecurityContext mit @AuthenticationPrincipal in Spring
Autor
Marcus HeldIn nahezu jeder Webanwendung muss Logik mit Bezug auf den User ausgeführt werden. Um auf diesen zuzugreifen werden seine Daten anhand seiner Authentifizierung zugeordnet. Mit Spring Security, dem Authentication-Framework im Spring Stack, haben wir verschiedene Möglichkeiten, diese essenzielle Aufgabe zu bewältigen.
Statische Zugriffe und seine Nachteile
In vielen Anwendungen und Beispielen ist es üblich, den SecurityContext
in Spring statisch abzurufen, um auf die aktuellen UserDetails
zuzugreifen. Das hatte jedoch einige Nachteile:
- Kopplung: Statischer Zugriff führt zu einer engen Kopplung zwischen dem Code und dem Spring Security Framework. Tests werden schwieriger, und das Code-Design leidet darunter.
- Thread-Sicherheit: Da die Informationen im Thread gespeichert sind, besteht die Gefahr, dass sie in einem multithreaded Environment überschrieben werden.
- Casting: Es ist notwendig die Implementierung zur Runtime zu casten. Dadurch werden potenzielle Fehler nicht vom Compiler entdeckt.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails user = (UserDetails) authentication.getPrincipal();
Der elegante Weg: @AuthenticationPrincipal
Spring bietet eine bessere Lösung, mit der du eine engere Verbindung zwischen deiner Controller-Klasse und dem SecurityContext
vermeiden kannst. Die @AuthenticationPrincipal
Annotation hilft dir typsicher deine UserDetails
Implementierung abzurufen. Das sieht dann folgendermaßen aus:
@Controller
public class UserController {
@RequestMapping("/user")
public String getUser(@AuthenticationPrincipal User user) {
// ...
}
}
Wann kann ich @AuthenticationPrincipal benutzen?
Um die @AuthenticationPrincipal
Annotation erfolgreich zu nutzen, musst du das UserDetails
Interface implementieren und in Spring Security registrieren. Hier sind die Schritte, die du befolgen musst:
1. Implementierung des UserDetails
Interface
Die UserPrincipal
Klasse muss das UserDetails
Interface implementieren. Dadurch wird sichergestellt, dass Spring Security die Klasse korrekt erkennt und die Authenticated User Details korrekt abruft.
Wenn du JPA zur Persistierung nutzt, könntest du deine User
Klasse als Entität modellieren:
@Entity
public class User implements UserDetails {
@Id
private UUID id = UUID.randomUUID();
private String username;
private String password;
private String role;
private boolean enabled;
// Implementiere die Methoden des UserDetails Interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(() -> role);
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
}
2. Konfiguration eines UserDetailsService
Spring Security verwendet ein UserDetailsService
, um die UserDetails
Klasse zu instanziieren. Du kannst einen eigenen Service erstellen, der das UserDetailsService
Interface implementiert, und ihn als Bean konfigurieren.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
3. Integration mit Spring Security Configuration
Zuletzt musst du Spring Security mitteilen, dass es deinen CustomUserDetailsService
verwenden soll. Dabei implementierst du WebSecurityConfigurerAdapter
und registrierst deinen UserDetailsService
in der configure()
Methode.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
Warum dieser Ansatz besser ist
Mit diesem Ansatz:
- Erhältst du einfachen Zugriff auf die User Information, ohne den
SecurityContext
direkt zu verwenden. - Erhöhst du die Testbarkeit deines Codes, da du die Controller-Methoden einfacher mocken kannst.
- Hältst du dich das Single-Responsibility-Prinzip und vermeidest casting im API Layer.