Spring Boot 12 min read

Spring Security Database Authentication: Custom UserDetailsService Guide

Implement database authentication in Spring Security. Learn UserDetailsService, password encoding, account locking, and multi-tenant authentication.

MR

Moshiour Rahman

Advertisement

From In-Memory to Database Authentication

The default in-memory authentication is fine for testing, but production applications need database-backed user management. This guide covers everything from basic setup to advanced multi-tenant scenarios.

Project Setup

Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Application Configuration

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp
    username: postgres
    password: secret
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    properties:
      hibernate:
        format_sql: true

# Custom security properties
security:
  password:
    min-length: 8
    require-uppercase: true
    require-number: true
  account:
    max-failed-attempts: 5
    lock-duration-minutes: 30

Database Schema

Entity Relationship

┌─────────────┐       ┌─────────────┐       ┌─────────────┐
│    User     │──────<│  UserRole   │>──────│    Role     │
└─────────────┘       └─────────────┘       └─────────────┘


                                            ┌──────┴──────┐
                                            │             │
                                     ┌──────▼─────┐ ┌─────▼──────┐
                                     │RolePermission│ │ Permission │
                                     └─────────────┘ └────────────┘

User Entity

@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 100)
    private String username;

    @Column(unique = true, nullable = false, length = 255)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(name = "first_name", length = 50)
    private String firstName;

    @Column(name = "last_name", length = 50)
    private String lastName;

    @Column(nullable = false)
    private boolean enabled = true;

    @Column(name = "account_non_expired", nullable = false)
    private boolean accountNonExpired = true;

    @Column(name = "account_non_locked", nullable = false)
    private boolean accountNonLocked = true;

    @Column(name = "credentials_non_expired", nullable = false)
    private boolean credentialsNonExpired = true;

    @Column(name = "failed_login_attempts")
    private int failedLoginAttempts = 0;

    @Column(name = "lock_time")
    private LocalDateTime lockTime;

    @Column(name = "password_changed_at")
    private LocalDateTime passwordChangedAt;

    @Column(name = "created_at", nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
        passwordChangedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected void onUpdate() {
        updatedAt = LocalDateTime.now();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<>();

        // Add roles
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));

            // Add permissions from each role
            for (Permission permission : role.getPermissions()) {
                authorities.add(new SimpleGrantedAuthority(permission.getName()));
            }
        }

        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

Role Entity

@Entity
@Table(name = "roles")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Role {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String name;

    @Column(length = 255)
    private String description;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "role_permissions",
        joinColumns = @JoinColumn(name = "role_id"),
        inverseJoinColumns = @JoinColumn(name = "permission_id")
    )
    private Set<Permission> permissions = new HashSet<>();
}

Permission Entity

@Entity
@Table(name = "permissions")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Permission {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 100)
    private String name;

    @Column(length = 255)
    private String description;
}

SQL Migration (Flyway/Liquibase)

-- V1__create_security_tables.sql

CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    username VARCHAR(100) UNIQUE NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    enabled BOOLEAN NOT NULL DEFAULT true,
    account_non_expired BOOLEAN NOT NULL DEFAULT true,
    account_non_locked BOOLEAN NOT NULL DEFAULT true,
    credentials_non_expired BOOLEAN NOT NULL DEFAULT true,
    failed_login_attempts INTEGER DEFAULT 0,
    lock_time TIMESTAMP,
    password_changed_at TIMESTAMP,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP
);

CREATE TABLE roles (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL,
    description VARCHAR(255)
);

CREATE TABLE permissions (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(100) UNIQUE NOT NULL,
    description VARCHAR(255)
);

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permissions (
    role_id BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    permission_id BIGINT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

-- Indexes
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);

-- Default roles and permissions
INSERT INTO roles (name, description) VALUES
    ('ADMIN', 'Administrator with full access'),
    ('USER', 'Standard user'),
    ('MODERATOR', 'Content moderator');

INSERT INTO permissions (name, description) VALUES
    ('READ_USERS', 'Can read user data'),
    ('WRITE_USERS', 'Can create/update users'),
    ('DELETE_USERS', 'Can delete users'),
    ('READ_CONTENT', 'Can read content'),
    ('WRITE_CONTENT', 'Can create/update content'),
    ('DELETE_CONTENT', 'Can delete content'),
    ('MANAGE_ROLES', 'Can manage roles and permissions');

-- Assign permissions to roles
INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p WHERE r.name = 'ADMIN';

INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'USER' AND p.name IN ('READ_CONTENT', 'WRITE_CONTENT');

INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id FROM roles r, permissions p
WHERE r.name = 'MODERATOR' AND p.name IN ('READ_CONTENT', 'WRITE_CONTENT', 'DELETE_CONTENT', 'READ_USERS');

Repository Layer

UserRepository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByUsername(String username);

    Optional<User> findByEmail(String email);

    Optional<User> findByUsernameOrEmail(String username, String email);

    boolean existsByUsername(String username);

    boolean existsByEmail(String email);

    @Modifying
    @Query("UPDATE User u SET u.failedLoginAttempts = ?2 WHERE u.username = ?1")
    void updateFailedAttempts(String username, int failedAttempts);

    @Modifying
    @Query("UPDATE User u SET u.accountNonLocked = false, u.lockTime = ?2 WHERE u.username = ?1")
    void lockAccount(String username, LocalDateTime lockTime);

    @Modifying
    @Query("UPDATE User u SET u.accountNonLocked = true, u.lockTime = null, u.failedLoginAttempts = 0 WHERE u.username = ?1")
    void unlockAccount(String username);

    @Query("SELECT u FROM User u WHERE u.accountNonLocked = false AND u.lockTime < ?1")
    List<User> findExpiredLockedAccounts(LocalDateTime expirationTime);
}

RoleRepository

@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

    Optional<Role> findByName(String name);

    @Query("SELECT r FROM Role r LEFT JOIN FETCH r.permissions WHERE r.name = ?1")
    Optional<Role> findByNameWithPermissions(String name);
}

Custom UserDetailsService

Basic Implementation

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        log.debug("Loading user by username: {}", username);

        return userRepository.findByUsernameOrEmail(username, username)
            .orElseThrow(() -> {
                log.warn("User not found: {}", username);
                return new UsernameNotFoundException(
                    "User not found with username or email: " + username
                );
            });
    }
}

Enhanced Implementation with Caching

@Service
@RequiredArgsConstructor
@Slf4j
public class CachingUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    private final CacheManager cacheManager;

    private static final String USER_CACHE = "users";

    @Override
    @Transactional(readOnly = true)
    @Cacheable(value = USER_CACHE, key = "#username")
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {

        log.debug("Loading user from database: {}", username);

        return userRepository.findByUsernameOrEmail(username, username)
            .orElseThrow(() ->
                new UsernameNotFoundException("User not found: " + username));
    }

    @CacheEvict(value = USER_CACHE, key = "#username")
    public void evictUserCache(String username) {
        log.debug("Evicting cache for user: {}", username);
    }

    @CacheEvict(value = USER_CACHE, allEntries = true)
    public void evictAllUsersCache() {
        log.debug("Evicting all user cache entries");
    }
}

Account Locking & Failed Attempts

Login Attempt Service

@Service
@RequiredArgsConstructor
@Slf4j
public class LoginAttemptService {

    private final UserRepository userRepository;

    @Value("${security.account.max-failed-attempts:5}")
    private int maxFailedAttempts;

    @Value("${security.account.lock-duration-minutes:30}")
    private int lockDurationMinutes;

    @Transactional
    public void loginSucceeded(String username) {
        userRepository.findByUsername(username).ifPresent(user -> {
            if (user.getFailedLoginAttempts() > 0) {
                user.setFailedLoginAttempts(0);
                userRepository.save(user);
                log.info("Reset failed attempts for user: {}", username);
            }
        });
    }

    @Transactional
    public void loginFailed(String username) {
        userRepository.findByUsername(username).ifPresent(user -> {
            int attempts = user.getFailedLoginAttempts() + 1;
            user.setFailedLoginAttempts(attempts);

            if (attempts >= maxFailedAttempts) {
                user.setAccountNonLocked(false);
                user.setLockTime(LocalDateTime.now());
                log.warn("Account locked due to {} failed attempts: {}",
                    attempts, username);
            }

            userRepository.save(user);
        });
    }

    @Transactional
    public boolean unlockIfExpired(User user) {
        if (user.getLockTime() == null) {
            return true;
        }

        LocalDateTime unlockTime = user.getLockTime()
            .plusMinutes(lockDurationMinutes);

        if (LocalDateTime.now().isAfter(unlockTime)) {
            user.setAccountNonLocked(true);
            user.setLockTime(null);
            user.setFailedLoginAttempts(0);
            userRepository.save(user);
            log.info("Account automatically unlocked: {}", user.getUsername());
            return true;
        }

        return false;
    }

    public long getRemainingLockTime(User user) {
        if (user.getLockTime() == null) {
            return 0;
        }

        LocalDateTime unlockTime = user.getLockTime()
            .plusMinutes(lockDurationMinutes);

        return Duration.between(LocalDateTime.now(), unlockTime).toMinutes();
    }
}

Authentication Event Listeners

@Component
@RequiredArgsConstructor
@Slf4j
public class AuthenticationEventListener {

    private final LoginAttemptService loginAttemptService;
    private final CachingUserDetailsService userDetailsService;

    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        log.info("Successful login: {}", username);
        loginAttemptService.loginSucceeded(username);
    }

    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        String username = (String) event.getAuthentication().getPrincipal();
        log.warn("Failed login attempt for: {}", username);
        loginAttemptService.loginFailed(username);
        userDetailsService.evictUserCache(username);
    }
}

Custom Authentication Provider with Locking

@Component
@RequiredArgsConstructor
public class LockingAwareAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final LoginAttemptService loginAttemptService;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        if (userDetails instanceof User user) {
            // Check if account is locked
            if (!user.isAccountNonLocked()) {
                if (!loginAttemptService.unlockIfExpired(user)) {
                    long remainingMinutes = loginAttemptService.getRemainingLockTime(user);
                    throw new LockedException(
                        "Account is locked. Try again in " + remainingMinutes + " minutes."
                    );
                }
            }

            // Validate password
            if (!passwordEncoder.matches(password, user.getPassword())) {
                throw new BadCredentialsException("Invalid password");
            }

            // Check other account states
            if (!user.isEnabled()) {
                throw new DisabledException("Account is disabled");
            }
            if (!user.isAccountNonExpired()) {
                throw new AccountExpiredException("Account has expired");
            }
            if (!user.isCredentialsNonExpired()) {
                throw new CredentialsExpiredException("Password has expired");
            }
        }

        return new UsernamePasswordAuthenticationToken(
            userDetails,
            null,
            userDetails.getAuthorities()
        );
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class
            .isAssignableFrom(authentication);
    }
}

Password Policies

Password Validation

@Component
@RequiredArgsConstructor
public class PasswordPolicyValidator {

    @Value("${security.password.min-length:8}")
    private int minLength;

    @Value("${security.password.require-uppercase:true}")
    private boolean requireUppercase;

    @Value("${security.password.require-lowercase:true}")
    private boolean requireLowercase;

    @Value("${security.password.require-number:true}")
    private boolean requireNumber;

    @Value("${security.password.require-special:false}")
    private boolean requireSpecial;

    public ValidationResult validate(String password) {
        List<String> errors = new ArrayList<>();

        if (password == null || password.length() < minLength) {
            errors.add("Password must be at least " + minLength + " characters");
        }

        if (requireUppercase && !password.matches(".*[A-Z].*")) {
            errors.add("Password must contain at least one uppercase letter");
        }

        if (requireLowercase && !password.matches(".*[a-z].*")) {
            errors.add("Password must contain at least one lowercase letter");
        }

        if (requireNumber && !password.matches(".*\\d.*")) {
            errors.add("Password must contain at least one number");
        }

        if (requireSpecial && !password.matches(".*[!@#$%^&*(),.?\":{}|<>].*")) {
            errors.add("Password must contain at least one special character");
        }

        return new ValidationResult(errors.isEmpty(), errors);
    }

    @Data
    @AllArgsConstructor
    public static class ValidationResult {
        private boolean valid;
        private List<String> errors;
    }
}

Password History (Prevent Reuse)

@Entity
@Table(name = "password_history")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PasswordHistory {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_id", nullable = false)
    private Long userId;

    @Column(nullable = false)
    private String password;

    @Column(name = "created_at", nullable = false)
    private LocalDateTime createdAt;
}

@Repository
public interface PasswordHistoryRepository extends JpaRepository<PasswordHistory, Long> {

    @Query("SELECT ph FROM PasswordHistory ph WHERE ph.userId = ?1 ORDER BY ph.createdAt DESC")
    List<PasswordHistory> findByUserIdOrderByCreatedAtDesc(Long userId, Pageable pageable);

    default List<PasswordHistory> findLastNPasswords(Long userId, int count) {
        return findByUserIdOrderByCreatedAtDesc(userId, PageRequest.of(0, count));
    }
}

@Service
@RequiredArgsConstructor
public class PasswordHistoryService {

    private final PasswordHistoryRepository passwordHistoryRepository;
    private final PasswordEncoder passwordEncoder;

    @Value("${security.password.history-count:5}")
    private int historyCount;

    public boolean isPasswordReused(Long userId, String newPassword) {
        List<PasswordHistory> history = passwordHistoryRepository
            .findLastNPasswords(userId, historyCount);

        return history.stream()
            .anyMatch(ph -> passwordEncoder.matches(newPassword, ph.getPassword()));
    }

    @Transactional
    public void recordPassword(Long userId, String encodedPassword) {
        PasswordHistory history = PasswordHistory.builder()
            .userId(userId)
            .password(encodedPassword)
            .createdAt(LocalDateTime.now())
            .build();

        passwordHistoryRepository.save(history);
    }
}

User Registration Service

@Service
@RequiredArgsConstructor
@Slf4j
public class UserRegistrationService {

    private final UserRepository userRepository;
    private final RoleRepository roleRepository;
    private final PasswordEncoder passwordEncoder;
    private final PasswordPolicyValidator passwordValidator;
    private final PasswordHistoryService passwordHistoryService;
    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public User registerUser(RegistrationRequest request) {
        // Validate unique constraints
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new UsernameAlreadyExistsException(
                "Username already exists: " + request.getUsername());
        }

        if (userRepository.existsByEmail(request.getEmail())) {
            throw new EmailAlreadyExistsException(
                "Email already exists: " + request.getEmail());
        }

        // Validate password policy
        var validation = passwordValidator.validate(request.getPassword());
        if (!validation.isValid()) {
            throw new InvalidPasswordException(validation.getErrors());
        }

        // Get default role
        Role userRole = roleRepository.findByName("USER")
            .orElseThrow(() -> new RuntimeException("Default role not found"));

        // Create user
        String encodedPassword = passwordEncoder.encode(request.getPassword());

        User user = User.builder()
            .username(request.getUsername())
            .email(request.getEmail())
            .password(encodedPassword)
            .firstName(request.getFirstName())
            .lastName(request.getLastName())
            .enabled(false)  // Require email verification
            .roles(Set.of(userRole))
            .build();

        user = userRepository.save(user);

        // Record password in history
        passwordHistoryService.recordPassword(user.getId(), encodedPassword);

        // Publish event for email verification
        eventPublisher.publishEvent(new UserRegisteredEvent(user));

        log.info("User registered: {}", user.getUsername());

        return user;
    }

    @Transactional
    public void changePassword(Long userId, PasswordChangeRequest request) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException("User not found"));

        // Verify current password
        if (!passwordEncoder.matches(request.getCurrentPassword(), user.getPassword())) {
            throw new BadCredentialsException("Current password is incorrect");
        }

        // Validate new password
        var validation = passwordValidator.validate(request.getNewPassword());
        if (!validation.isValid()) {
            throw new InvalidPasswordException(validation.getErrors());
        }

        // Check password history
        if (passwordHistoryService.isPasswordReused(userId, request.getNewPassword())) {
            throw new PasswordReusedException(
                "Cannot reuse any of your last " + 5 + " passwords");
        }

        // Update password
        String encodedPassword = passwordEncoder.encode(request.getNewPassword());
        user.setPassword(encodedPassword);
        user.setPasswordChangedAt(LocalDateTime.now());
        user.setCredentialsNonExpired(true);

        userRepository.save(user);
        passwordHistoryService.recordPassword(userId, encodedPassword);

        log.info("Password changed for user: {}", user.getUsername());
    }
}

Security Configuration

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final LockingAwareAuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/moderator/**").hasAnyRole("ADMIN", "MODERATOR")
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authenticationProvider(authenticationProvider)
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }

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

Scheduled Account Maintenance

@Component
@RequiredArgsConstructor
@Slf4j
public class AccountMaintenanceScheduler {

    private final UserRepository userRepository;

    @Value("${security.account.lock-duration-minutes:30}")
    private int lockDurationMinutes;

    @Value("${security.password.expiry-days:90}")
    private int passwordExpiryDays;

    @Scheduled(fixedRate = 300000)  // Every 5 minutes
    @Transactional
    public void unlockExpiredAccounts() {
        LocalDateTime expirationTime = LocalDateTime.now()
            .minusMinutes(lockDurationMinutes);

        List<User> expiredLocks = userRepository.findExpiredLockedAccounts(expirationTime);

        for (User user : expiredLocks) {
            user.setAccountNonLocked(true);
            user.setLockTime(null);
            user.setFailedLoginAttempts(0);
            userRepository.save(user);
            log.info("Unlocked expired account: {}", user.getUsername());
        }
    }

    @Scheduled(cron = "0 0 2 * * ?")  // Daily at 2 AM
    @Transactional
    public void expireOldPasswords() {
        LocalDateTime expiryDate = LocalDateTime.now().minusDays(passwordExpiryDays);

        // This would need a custom query
        log.info("Checking for expired passwords older than: {}", expiryDate);
    }
}

Summary

ComponentPurpose
UserDetailsServiceLoad users from database
PasswordEncoderHash and verify passwords
LoginAttemptServiceTrack failed attempts, lock accounts
PasswordPolicyValidatorEnforce password requirements
PasswordHistoryServicePrevent password reuse
Event ListenersReact to auth success/failure

Database authentication provides the foundation for enterprise security. In the next article, we’ll add OAuth2 social login for Google and GitHub.

Series Navigation

  1. Spring Security Architecture
  2. Database Authentication (This Article)
  3. OAuth2 & Social Login
  4. Method-Level Security
  5. CSRF, CORS & Security Headers
  6. Production Security Best Practices

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.