Skip to main content

12.2 Authentication Implementation — BCrypt and UserDetailsService

Passwords must never be stored in plain text in a database. Additionally, we need to bridge Spring Security with the user information stored in the database.

1. PasswordEncoder and BCrypt​

BCrypt is a widely used algorithm for password hashing. It provides strong security and automatically generates a Salt to defend against rainbow table attacks.

Registering PasswordEncoder Bean​

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

2. UserDetails and UserDetailsService​

Interfaces must be implemented so that Spring Security can understand user information.

  • UserDetails: An object containing user information such as username, password, and authorities.
  • UserDetailsService: A service that retrieves user information from a repository (like a database) and returns UserDetails.

Example of Custom UserDetailsService Implementation​

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.map(user -> User.builder()
.username(user.getEmail())
.password(user.getPassword()) // Must be BCrypt hashed in DB
.roles(user.getRole().name())
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
}

3. Password Encryption during Signup​

The PasswordEncoder must be used during the user registration process.

public void signup(UserDto dto) {
String encodedPassword = passwordEncoder.encode(dto.getPassword());
User user = User.builder()
.email(dto.getEmail())
.password(encodedPassword)
.role(Role.USER)
.build();
userRepository.save(user);
}

🎯 Key Points​

  • Passwords must be encrypted using a one-way hash algorithm like BCrypt.
  • UserDetailsService acts as the connection point between Spring Security and the database.
  • Validate user existence and return a UserDetails object in the loadUserByUsername method.

2. Authentication Lifecycle and Practical Example​

Authentication in Spring Security is achieved through the collaboration of several components. Here, we'll trace the full lifecycle of the most common Form Login (ID/PW) method.

1. Full Authentication Process (Mermaid)​

sequenceDiagram
participant User as User(Client)
participant Filter as AuthenticationFilter
participant Manager as AuthenticationManager
participant Provider as AuthenticationProvider
participant Service as UserDetailsService
participant DB as Database

User->>Filter: 1. Login Request (username, password)
Filter->>Filter: 2. Create UsernamePasswordAuthenticationToken (unauthenticated)
Filter->>Manager: 3. Call authenticate(token)
Manager->>Provider: 4. Delegate authentication to a supporting Provider
Provider->>Service: 5. loadUserByUsername(username)
Service->>DB: 6. Query user info
DB-->>Service: Return User Entity
Service-->>Provider: 7. Return UserDetails object
Provider->>Provider: 8. Verify password (BCrypt comparison)
Provider-->>Manager: 9. Return authenticated Authentication object
Manager-->>Filter: 10. Authentication complete
Filter->>Filter: 11. Store info in SecurityContextHolder
Filter-->>User: 12. Success response/redirect

2. Roles of Key Components​

  1. AuthenticationFilter: Intercepts the HTTP request, creates an unauthenticated Authentication object, and passes it to the Manager.
  2. AuthenticationManager: The main interface overseeing authentication. (Typically, the ProviderManager implementation is used.)
  3. AuthenticationProvider: Performs the actual business logic (e.g., ID/PW validation). Multiple providers can be registered to handle various authentication types (LDAP, DB, Social, etc.).
  4. UserDetailsService: Acts as the "data provider" that retrieves user information from a storage like a database.
  5. SecurityContextHolder: A storage area that keeps authenticated information accessible throughout the application.

3. Practical Example: Login API Implementation (JWT)​

Following modern trends, here is a simple controller example that issues a JWT token after successful authentication.

LoginRequest DTO​

public record LoginRequest(String email, String password) {}

AuthController​

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthenticationManager authenticationManager;
private final TokenProvider tokenProvider;

@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest request) {
// 1. Create an unauthenticated token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.email(), request.password());

// 2. Start actual authentication (triggers Manager -> Provider -> Service flow)
// Throws exception if authentication fails
Authentication authentication = authenticationManager.authenticate(authenticationToken);

// 3. Store authentication info in context (optional but recommended)
SecurityContextHolder.getContext().setAuthentication(authentication);

// 4. Generate and return a JWT token based on authenticated info
String jwt = tokenProvider.createToken(authentication);
return ResponseEntity.ok(jwt);
}
}

4. Utilizing Authentication Info (@AuthenticationPrincipal)​

Used when you want to directly access the logged-in user's information from a controller.

@GetMapping("/api/me")
public ResponseEntity<String> getMyInfo(@AuthenticationPrincipal UserDetails userDetails) {
return ResponseEntity.ok("Current User: " + userDetails.getUsername());
}

🎯 Key Points​

  • Authentication goes through the stages of Filter -> Manager -> Provider -> Service.
  • The AuthenticationManager is the central role, while the Provider handles actual verification.
  • Once successful, user info is held in the SecurityContextHolder, making it accessible globally.