Authentication & Authorization

UserDetailsService, password encoding with BCrypt, role-based authorization, and method security with @PreAuthorize.

UserDetailsService

Spring Security authenticates against its own UserDetails abstraction, not against your entity classes directly. You implement UserDetailsService to bridge the two — loading a user (however your application stores one) and adapting it into the shape Spring Security expects:

Java
@Service
public class AppUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public AppUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("No user with email: " + email));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getEmail())
            .password(user.getHashedPassword())
            .authorities(user.getRoles().stream().map(SimpleGrantedAuthority::new).toList())
            .build();
    }
}

Spring Security calls this automatically during authentication — you never call it yourself.

Password encoding

Passwords are never stored or compared in plain text. BCryptPasswordEncoder hashes with a random per-password salt baked into the output, so two identical passwords produce different hashes:

Java
@Configuration
public class PasswordConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
Java
@Service
public class RegistrationService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    public RegistrationService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
    }

    public User register(String email, String rawPassword) {
        String hashed = passwordEncoder.encode(rawPassword);
        return userRepository.save(new User(email, hashed));
    }
}

Authentication compares a submitted password against the stored hash with passwordEncoder.matches(raw, hashed) — Spring Security's authentication provider does this for you once UserDetailsService and PasswordEncoder beans are both registered; you don't call matches directly in ordinary login flows.

Role-based authorization

Roles/authorities can be checked at the URL level (in the SecurityFilterChain) or at the method level.

Java
http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/reports/**").hasAnyRole("ADMIN", "MANAGER")
    .anyRequest().authenticated()
);

.hasRole("ADMIN") is shorthand that expects the authority to be stored as ROLE_ADMIN — Spring Security adds the ROLE_ prefix automatically when you use hasRole (but not when checking a raw authority with hasAuthority).

Method security with @PreAuthorize

URL-level rules are coarse — they don't know about a specific resource being requested. Method security expresses authorization rules directly on service or controller methods, evaluated with a Spring Expression Language (SpEL) condition before the method body ever runs:

Java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
}
Java
@Service
public class DocumentService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteDocument(Long id) {
        // only reachable if the caller has ROLE_ADMIN
    }

    @PreAuthorize("#ownerId == authentication.principal.id or hasRole('ADMIN')")
    public Document getDocument(Long documentId, Long ownerId) {
        // reachable by the resource's own owner, OR an admin
        return documentRepository.findById(documentId).orElseThrow();
    }
}

@PreAuthorize runs before the method executes and can reference method parameters directly (#ownerId above) — useful for exactly the kind of per-resource, ownership-based check that a URL pattern alone can't express. @PostAuthorize is the equivalent check evaluated after the method returns, letting the expression inspect the returned object itself (returnObject.ownerId == authentication.principal.id).

Common mistakes

  • Comparing raw passwords with .equals() instead of passwordEncoder.matches(...) — plaintext comparison defeats the entire purpose of hashing and is a critical security bug.
  • Forgetting @EnableMethodSecurity — without it, @PreAuthorize/@PostAuthorize annotations are silently ignored and every method runs unguarded.
  • Using hasRole("ADMIN") when the stored authority is literally "ADMIN" rather than "ROLE_ADMIN" (or vice versa) — hasRole always adds the ROLE_ prefix implicitly, so a mismatch here silently denies everyone.
  • Relying only on URL-level authorization for resource-ownership checks (e.g. "users can only edit their own profile") — a URL pattern like /api/users/{id} can't express "only if {id} belongs to the caller"; that needs method security or an explicit check in the handler.