Avoiding Static Access to SecurityContext with @AuthenticationPrincipal in Spring
Author
Marcus HeldIn almost every web application, logic related to the user must be executed. To access this, his data is assigned based on his authentication. With Spring Security, the authentication framework in the Spring stack, we have various ways to accomplish this essential task.
Static Access and its Drawbacks
In many applications and examples, it is common to statically retrieve the SecurityContext
in Spring to access the current UserDetails
. However, this has some disadvantages:
- Coupling: Static access leads to tight coupling between the code and the Spring Security Framework. Tests become more difficult, and the code design suffers as a result.
- Thread Safety: Since the information is stored in the thread, there is a risk that it will be overwritten in a multithreaded environment.
- Casting: It is necessary to cast the implementation at runtime. This means potential errors are not detected by the compiler.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserDetails user = (UserDetails) authentication.getPrincipal();
The Elegant Way: @AuthenticationPrincipal
Spring offers a better solution, allowing you to avoid a close connection between your controller class and the SecurityContext
. The @AuthenticationPrincipal
annotation helps you type-safely retrieve your UserDetails
implementation. It looks like this:
@Controller
public class UserController {
@RequestMapping("/user")
public String getUser(@AuthenticationPrincipal User user) {
// ...
}
}
When can I use @AuthenticationPrincipal?
To successfully use the @AuthenticationPrincipal
annotation, you must implement the UserDetails
interface and register it in Spring Security. Here are the steps you need to follow:
1. Implementing the UserDetails
Interface
The UserPrincipal
class must implement the UserDetails
interface. This ensures that Spring Security correctly recognizes the class and retrieves the authenticated user details correctly.
If you use JPA for persistence, you could model your User
class as an entity:
@Entity
public class User implements UserDetails {
@Id
private UUID id = UUID.randomUUID();
private String username;
private String password;
private String role;
private boolean enabled;
// Implement the methods of the 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. Configuration of a Custom UserDetailsService
Spring Security uses a UserDetailsService
to instantiate the UserDetails
class. You can create your own service that implements the UserDetailsService
interface, and configure it as a bean.
@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 with Spring Security Configuration
Finally, you must inform Spring Security to use your CustomUserDetailsService
. To do this, you implement WebSecurityConfigurerAdapter
and register your UserDetailsService
in the configure()
method.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
Why This Approach is Better
With this approach:
- You get easy access to user information without directly using the
SecurityContext
. - You increase the testability of your code, as you can mock the controller methods more easily.
- You adhere to the Single Responsibility Principle and avoid casting in the API layer.