Five abstractions carry an authenticated identity from login to your business code, and they each have one job. Knowing how they fit together is the difference between "I add @EnableWebSecurity" and actually understanding the framework.
UserDetailsService
A single-method strategy for loading a user by username from wherever they live (DB, LDAP, in-memory):
@Bean
UserDetailsService users(UserRepository repo) {
return username -> repo.findByUsername(username)
.map(u -> User.withUsername(u.getUsername())
.password(u.getPasswordHash()) // already encoded
.authorities(u.getRoles())
.build())
.orElseThrow(() -> new UsernameNotFoundException(username));
}
It returns a UserDetails or throws UsernameNotFoundException. It does not check the password — that's the provider's job.
UserDetails
The user record Spring Security cares about: getUsername(), getPassword() (the encoded hash), getAuthorities(), plus account-status flags (isEnabled, isAccountNonLocked, …). It is the principal once authenticated.
Authentication
Represents the request/result of authenticating. Before login it may hold raw credentials (a token); after success it holds the principal (the UserDetails), cleared credentials, the granted authorities, and isAuthenticated() == true. Implementations include UsernamePasswordAuthenticationToken and JwtAuthenticationToken.
SecurityContext
A thin holder around the current Authentication. One per "context of execution" — normally one per request thread.
SecurityContextHolder
The static accessor for the current SecurityContext, backed by a strategy (a thread-local by default). This is how any code reaches the current user:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
boolean admin = auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
How they chain
UserDetailsService → loads UserDetails → an AuthenticationProvider verifies it and builds an authenticated Authentication → it's placed in a SecurityContext → stored via SecurityContextHolder for the rest of the request.