Module 6: Security & Authentication - Protecting Your App

Security is not a feature; it's a foundation. Let's protect our application and user data with Spring Security.

6.1 Introduction to Spring Security

Spring Security is the de facto standard for securing Spring-based applications. It's a powerful and highly customizable framework that handles the two core pillars of security:

  • Authentication (AuthN): This answers the question, "Who are you?". It's the process of verifying a user's identity, typically by checking their username and password against stored credentials.
  • Authorization (AuthZ): This answers the question, "What are you allowed to do?". Once a user is authenticated, authorization determines if they have the necessary permissions to access a specific resource or perform an action.

The moment you add the spring-boot-starter-security dependency to your `pom.xml`, Spring Security auto-configures a default security policy. Every endpoint in your application immediately becomes protected, and you'll be redirected to a default login page if you try to access them. Our job is to customize this behavior to fit our application's needs.

6.2 Password Encryption with BCrypt

Security Rule #1: NEVER Store Passwords in Plain Text

Storing passwords as plain text (e.g., "password123") in your database is the single biggest security mistake you can make. If your database is ever compromised, every user's password will be exposed.

The solution is one-way cryptographic hashing. We use an algorithm to transform a password into a long, fixed-length string called a hash. This process is irreversible; you cannot get the original password back from the hash.

BCrypt: The Industry Standard

We will use BCrypt, a strong hashing function designed specifically for passwords. It's intentionally slow and includes a "salt" (random data mixed in with the password before hashing), which makes it highly resistant to common attacks like brute-force and rainbow tables.

Implementing the `PasswordEncoder`

Spring Security makes this easy. We just need to tell it which hashing algorithm to use by defining a `PasswordEncoder` bean in a configuration class.


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Use the BCrypt hashing algorithm
        return new BCryptPasswordEncoder();
    }
    
    // ... other security configurations will go here
}

Now, whenever we save a new user, we'll use this bean to encode their password before it ever touches the database.

6.3 Adding User Authentication (Login System)

To authenticate users, Spring Security needs a way to find their details. We provide this by implementing the UserDetailsService interface.

Step 1: The User Entity

First, we need a `User` entity to store credentials in our database. It needs to implement Spring Security's `UserDetails` interface.


@Entity
@Table(name = "users")
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String username;
    private String password; // This will be stored as a BCrypt hash
    private String role; // e.g., "ROLE_USER", "ROLE_ADMIN"
    
    // ... UserDetails interface methods (getAuthorities, isAccountNonExpired, etc.)
}

Step 2: Implement `UserDetailsService`

This service has one job: load a user from the database by their username.


@Service
public class JpaUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
    }
}

Step 3: The Registration Logic

In our `UserService`, when creating a new user, we must encode the password.


@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerUser(User user) {
        // Encode the plain-text password before saving
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setRole("ROLE_USER");
        return userRepository.save(user);
    }
}

6.4 Role-Based Access: Admin vs. Customer

Authorization is about controlling access. A regular customer shouldn't be able to see another customer's account details, and an admin needs broader permissions. We configure these rules in our `SecurityConfig` class by defining a `SecurityFilterChain` bean.


// --- Inside SecurityConfig.java ---
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable()) // Disable CSRF for simpler API testing
        .authorizeHttpRequests(auth -> auth
            // Allow anyone to access the registration and login endpoints
            .requestMatchers("/api/auth/**").permitAll()
            
            // Require ADMIN role for any endpoint under /api/admin
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            
            // Require USER or ADMIN role for account-related endpoints
            .requestMatchers("/api/accounts/**").hasAnyRole("USER", "ADMIN")
            
            // Any other request must be authenticated
            .anyRequest().authenticated()
        )
        .httpBasic(Customizer.withDefaults()); // Use HTTP Basic Authentication for now

    return http.build();
}
Method-Level Security: For even more granular control, you can use annotations like @PreAuthorize directly on your service methods. This allows you to write complex rules, such as ensuring a user can only view their own account details unless they are an admin.