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();
}
@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.