Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.
Week 14
If you run your application and visit http://localhost:8080, it will redirect you to /login because this is Spring Security's default behavior when it detects that you haven't explicitly configured your security rules.
The Bouncer (Filter Chain): When you added Spring Security, it installed a Security Filter Chain in front of every single request coming into Tomcat. Think of this as a security guard, or a bouncer, at the entrance.
The Default Rule: By default, the security filter is configured to protect every endpoint in your application. The rule is: "If you are not authenticated (logged in), you cannot pass."
The login form you see is a temporary, functional page provided by Spring Security itself for debugging.
To customize login form and enable 404 errors, you need to override Spring Security's default settings to tell it which URLs are public (like /api/auth/register) and which require a login.
Open your config.SecurityConfig class. You will need to implement a new @Bean method called securityFilterChain.
package edu.kirkwood.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration // Tells Spring this class contains bean definitions
@EnableWebSecurity // Enables Spring Security's web support
public class SecurityConfig {
@Bean // This method's return value will be managed by Spring's container
public PasswordEncoder passwordEncoder() {
// BCrypt is a standard, strong hashing algorithm for passwords.
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable Cross-Site Request Forgery for API development
.authorizeHttpRequests(authorize -> authorize
// This allows unmapped paths to result in 404, and allows all web viewing.
.requestMatchers(HttpMethod.GET).permitAll()
// API ENDPOINTS: Allow POST for user registration
.requestMatchers(HttpMethod.POST, "/api/auth/register").permitAll()
// PROTECTED CATCH-ALL (This protects unlisted POST/PUT/DELETE, etc.)
.anyRequest().authenticated()
)
// Ensure all auto-challenge mechanisms are disabled
.httpBasic(AbstractHttpConfigurer::disable) // Disable the login popup
.formLogin(AbstractHttpConfigurer::disable); // Stop formLogin redirect
return http.build();
}
}The SecurityFilterChain bean adjusts the "bouncer" rules for every request coming into your application.
csrf(csrf -> csrf.disable()): Cross-Site Request Forgery (CSRF) protection is usually required for web apps, but for stateless APIs (which typically use token authentication), it's often disabled to avoid complexity.
.requestMatchers("xxx").permitAll(): This tells the security filter: "This endpoint is public. Let anyone access it without logging in."
.anyRequest().authenticated(): This is the catch-all rule: "If the URL wasn't matched by any permitAll() rule, the user must be logged in."
.httpBasic(AbstractHttpConfigurer::disable): Disables the login popup
.formLogin(AbstractHttpConfigurer::disable): Stops formLogin redirect
.formLogin(formLogin -> {}): This line keeps the default behavior of redirecting to the /login page if an unauthenticated user tries to access a protected resource.
When you visit http://localhost:8080/api/auth/register with your browser, you are sending an HTTP GET request (browsers always send GET by default). Your AuthController only has a @PostMapping defined for that path.
When the Security Filter allows the request through (because you said permitAll()), it hits the DispatcherServlet, which says: "I have no method to handle a GET request to that path," and it internally forwards to the /error path. Since the error handler doesn't know how to render a page, it shows the "Whitelabel Error Page."
With the AuthController in place, you are now prepared to test the full user registration flow.
You'll need a tool like Insomnia, a simple browser extension, or Postman to send a POST request with a JSON body.
I was able to successfully test the user registration functionality using Insomnia and the Rested Firefox Extension.
Make a new HTTP request.
Change GET to POST.
Enter "http://localhost:8080/api/auth/register" as the URL.
Enter the following data as the body. Change the firstName and email as necessary.
{
"firstName": "Alex",
"lastName": "Student",
"email": "alex.student@kirkwood.edu",
"password": "StrongPassword123"
}
Go to the Headers tab. Ensure that a header named "Content-Type" with the value "application/json" is present.
Click Send. If a 201 status displays, check your database.
Note how the user's password was created in plain text.
Open UserServiceImpl.java and find the registerNewUser method.
First, inject a PasswordEncoder object along with the UserRepository and RoleRepository.
package edu.kirkwood.service.impl;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.UserRepository;
import edu.kirkwood.repository.RoleRepository;
import edu.kirkwood.service.UserService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service // Tells Spring to manage this class as a Service component
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
// Dependency Injection
// Spring automatically finds the necessary Repository implementations...
public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
// and injects them into the constructor when it creates this service.
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public User registerNewUser(User user) {
// TODO: complete the following steps
// 1. Password Hashing (Crucial for security!)
// 2. Default Role Assignment (assigning the "STUDENT" role)
// 3. Final save using userRepository.save(user);
return userRepository.save(user);
}
}Pass the user's string password to the PasswordEncoder object's encode method. Pass the value returned to the User object's setPassword method.
Find the default role that will be assigned to users. Add that role to a Set<Role> and call the User object's setRoles method.
package edu.kirkwood.service.impl;
import edu.kirkwood.model.Role;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.UserRepository;
import edu.kirkwood.repository.RoleRepository;
import edu.kirkwood.service.UserService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.Set;
@Service // Tells Spring to manage this class as a Service component
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
// Dependency Injection
// Spring automatically finds the necessary Repository implementations...
public UserServiceImpl(UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder) {
// and injects them into the constructor when it creates this service.
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public User registerNewUser(User user) {
// TODO: complete the following steps
// 1. Password Hashing (Crucial for security!)
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 2. Default Role Assignment (assigning the "STUDENT" role)
Role studentRole = roleRepository.findByName("STUDENT")
.orElseThrow(() -> new RuntimeException("Error: Default role not found."));
Set<Role> roles = new HashSet<>();
roles.add(studentRole);
user.setRoles(roles);
// 3. Final save using userRepository.save(user);
return userRepository.save(user);
}
}Making another POST request will result in a The 400 Bad Request with a null message
In IntelliJ's console log, you will see the queries that were run.
The exception is occurring because your Role entity is defined using Lombok's @Data annotation, which includes equals() and hashCode() methods that rely on all fields.
When Hibernate tries to load the complex, cyclical relationships (Users in Role and Roles in User), it gets stuck in a loop trying to compute the hash codes, leading to a stack overflow that your controller catches with a null message.
A solution is to disable the Lombok methods that rely on the relationships to prevent the loading loop.
Add the @EqualsAndHashCode.Exclude annotation to your roles relationship in the User model.
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(/* Content omitted */)
@EqualsAndHashCode.Exclude // <--- ADD THIS
private Set<Role> roles;
Do the same in the Role.
@ManyToMany(mappedBy = "roles")
@EqualsAndHashCode.Exclude // <--- ADD THIS
private Set<User> users;
Make another Post request. If a 201 status displays, check your database. A new user and user_role should be created.
For a user to log in, Spring Security needs a way to find a user by their email (or unique username) and compare the stored password hash. This is done by implementing the UserDetailsService interface.
Create a new sub-package named edu.kirkwood.service.security.
Create a class named "UserDetailsServiceImpl.java" inside it.
This class will link your UserRepository to Spring Security's login process using dependency injection.
package edu.kirkwood.service.security;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
// 1. Find the User from the database using your custom repository method
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email));
// 2. Convert your custom User model into the UserDetails object that Spring Security understands
return org.springframework.security.core.userdetails.User.builder()
.username(user.getEmail())
.password(user.getPassword()) // Spring Security will compare this HASH with the login password
.roles(user.getRoles().stream()
.map(role -> role.getName())
.toArray(String[]::new)) // Converts your Role set into Spring's required format
.build();
}
}Next, we need to explicitly tell Spring Security: "When a user tries to log in, use this new UserDetailsServiceImpl service."
Open SecurityConfig.java to use this service for loading users during the login attempt.
Add a new @Bean method called authenticationProvider and inject the two components: UserDetailsService (your custom service) and the PasswordEncoder.
package edu.kirkwood.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration // Tells Spring this class contains bean definitions
@EnableWebSecurity // Enables Spring Security's web support
public class SecurityConfig {
@Bean // This method's return value will be managed by Spring's container
public PasswordEncoder passwordEncoder() {
// BCrypt is a standard, strong hashing algorithm for passwords.
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder)
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 1. Tell the provider WHERE to find user details
authProvider.setUserDetailsService(userDetailsService);
// 2. Tell the provider HOW to check the password
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
/* Code omitted */
}
}The DaoAuthenticationProvider is the "engine" that handles the authentication process:
A user attempts to log in (sends email and password).
The AuthenticationProvider is activated.
It calls userDetailsService.loadUserByUsername(email) to fetch the user record (and the stored hashed password) from your MySQL database.
It calls passwordEncoder.matches(rawPassword, storedHash) to securely compare the input password against the stored hash.
Because you defined this bean, Spring Security now knows to use your database and your logic for verifying user credentials.
We next need to set up the API endpoint that will trigger the authentication engine we just built.
Add this new @Bean method to your existing SecurityConfig.java class.
The AuthenticationManager is the central interface that processes the login attempt. We must make it available for dependency injection in your controller.
package edu.kirkwood.config;
// other imports
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
public class SecurityConfig {
// Other methods omitted
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
// Make the AuthenticationManager (which knows about your UserDetailsService)
// available for injection in your controllers.
return config.getAuthenticationManager();
}
}We next need to set up the API endpoint that will trigger the authentication engine we just built.
Add this new @Bean method to your existing SecurityConfig.java class.
The AuthenticationManager is the central interface that processes the login attempt. We must make it available for dependency injection in your controller.
package edu.kirkwood.config;
// other imports
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
public class SecurityConfig {
// Other methods omitted
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
// Make the AuthenticationManager (which knows about your UserDetailsService)
// available for injection in your controllers.
return config.getAuthenticationManager();
}
}In API development, it's a good practice to use a separate class (a Data Transfer Object (DTO)) for the data coming in, instead of using your sensitive User entity.
Create a new package named edu.kirkwood.controller.requests.
Create a class named LoginRequest.java.
package edu.kirkwood.controller.requests;
import lombok.Data;
@Data
public class LoginRequest {
private String email;
private String password;
}Update AuthController to use the injected AuthenticationManager to process the credentials.
package edu.kirkwood.controller;
// other imports
import org.springframework.security.authentication.AuthenticationManager;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final AuthenticationManager authenticationManager;
// Updated Constructor (Now injecting both the Service and the Manager)
public AuthController(UserService userService, AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
// Your existing @PostMapping("/register") method
}Add the @PostMapping("/login") method to your AuthController to handle the incoming request.
package edu.kirkwood.controller;
// other imports
import edu.kirkwood.controller.requests.LoginRequest;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final UserService userService;
private final AuthenticationManager authenticationManager;
// Updated Constructor (Now injecting both the Service and the Manager)
public AuthController(UserService userService, AuthenticationManager authenticationManager) {
this.userService = userService;
this.authenticationManager = authenticationManager;
}
// Your existing @PostMapping("/register") method
@PostMapping("/login")
public ResponseEntity<String> authenticateUser(@RequestBody LoginRequest loginRequest) {
// 1. Create a token with the user's plain text credentials
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
);
// 2. Process authentication using the manager (which uses your UserDetailsService)
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 3. Optional: Set the authenticated user in the security context (needed for session-based security)
// Since your app is stateless, you would typically generate a JWT token here.
// For testing, we'll confirm success.
// If the line above didn't throw an exception, authentication succeeded.
return new ResponseEntity<>("User logged in successfully!", HttpStatus.OK);
}
}Update the SecurityConfig class securityFilterChain method to permit POST requests to the "/api/auth/login" endpoint.
package edu.kirkwood.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@Configuration // Tells Spring this class contains bean definitions
@EnableWebSecurity // Enables Spring Security's web support
public class SecurityConfig {
@Bean // This method's return value will be managed by Spring's container
public PasswordEncoder passwordEncoder() {
// BCrypt is a standard, strong hashing algorithm for passwords.
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 1. Tell the provider WHERE to find user details
authProvider.setUserDetailsService(userDetailsService);
// 2. Tell the provider HOW to check the password
authProvider.setPasswordEncoder(passwordEncoder);
return authProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable Cross-Site Request Forgery for API development
.authorizeHttpRequests(authorize -> authorize
// This allows unmapped paths to result in 404, and allows all web viewing.
.requestMatchers(HttpMethod.GET).permitAll()
// API ENDPOINTS: Allow POST for user registration and login
.requestMatchers(HttpMethod.POST, "/api/auth/register").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
// PROTECTED CATCH-ALL (This protects unlisted POST/PUT/DELETE, etc.)
.anyRequest().authenticated()
)
// Ensure all auto-challenge mechanisms are disabled
.httpBasic(AbstractHttpConfigurer::disable) // Disable the login popup
.formLogin(AbstractHttpConfigurer::disable); // Stop formLogin redirect
return http.build();
}
@Bean // NEW BEAN
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
// This line makes the AuthenticationManager (which knows about your UserDetailsService)
// available for injection in your controllers.
return config.getAuthenticationManager();
}
}If you send a POST request with valid credentials to http://localhost:8080/api/auth/login, you will get a 200 OK response.
Use this as the JSON body.
{
"email": "alex.student@kirkwood.edu",
"password": "StrongPassword123"
}
If the password is wrong, Spring Security will throw a BadCredentialsException.
@PostMapping("/login") in the AuthController triggers the AuthenticationManager.authenticate() method.
The DaoAuthenticationProvider is used in SecurityConfig.
The UserDetailsServiceImpl loadUserByUsername method finds the user in your MySQL database.
The BCryptPasswordEncoder compares the raw password to the stored hash.
The <scope>test</scope> tag tells Maven and Spring Boot that this dependency is only needed when running tests and compiling test code, but not when the final application is deployed.
During Development/Testing (Yes): When you run a test in IntelliJ, the test environment will have access to spring-security-test.
During Production/Runtime (No): When you package your application into its final .jar file for deployment (like to Google Cloud Run), Maven will exclude this dependency. Your live production application doesn't need test utilities, which keeps the deployment package smaller and cleaner.
Writing unit tests ensures the core security and business logic (registration and authentication) are robust and won't break as you expand your application.
We will focus on testing the Service Layer (UserServiceImpl) and the Security Service Layer (UserDetailsServiceImpl) in isolation, using Mockito to simulate the database responses from your repositories.
Add the mockito dependency to your pom.xml file.
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
Create a UserServiceImplTest class. Generate a setUp method.
The goal is to test that the service correctly hashes the password and assigns the STUDENT role when a new user registers.
We use @Mock to simulate the dependencies (the repositories and password encoder) and @InjectMocks to inject those mocks into the service we are testing.
package edu.kirkwood.service.impl;
import edu.kirkwood.model.Role;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.RoleRepository;
import edu.kirkwood.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
@Mock
private UserRepository userRepository;
@Mock
private RoleRepository roleRepository;
@Mock
private PasswordEncoder passwordEncoder;
@InjectMocks
private UserServiceImpl userService;
private User testUser;
private Role studentRole;
@BeforeEach
void setUp() {
testUser = new User();
testUser.setEmail("test@kirkwood.edu");
testUser.setPassword("rawPassword");
studentRole = new Role();
studentRole.setName("STUDENT");
}
}The goal is to test that the service correctly hashes the password and assigns the STUDENT role when a new user registers.
package edu.kirkwood.service.impl;
import edu.kirkwood.model.Role;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.RoleRepository;
import edu.kirkwood.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
// Code omitted
@Test
void registerNewUser() {
// --- 1. ARRANGE Mock Behavior (When these methods are called, return this) ---
// Simulate password hashing: encoder.encode() should return the hashed string
when(passwordEncoder.encode(testUser.getPassword())).thenReturn("hashedPassword");
// Simulate role lookup: roleRepository.findByName() should return the STUDENT role
when(roleRepository.findByName("STUDENT")).thenReturn(Optional.of(studentRole));
// Simulate save: userRepository.save() should return the user object that was passed to it
when(userRepository.save(any(User.class))).thenReturn(testUser);
// --- 2. ACT by calling the method to test ---
User registeredUser = userService.registerNewUser(testUser);
// --- 3. ASSERT by verifying the results ---
// Check that the user object returned is not null
assertNotNull(registeredUser);
// Check that the password was indeed hashed
assertEquals("hashedPassword", registeredUser.getPassword(), "Password must be hashed.");
// Check that the STUDENT role was assigned
assertTrue(registeredUser.getRoles().contains(studentRole), "User must have the STUDENT role.");
// --- 4. Verify Mock Interactions (Check the service called its dependencies correctly) ---
// Verify that the encoder was called once
verify(passwordEncoder, times(1)).encode("rawPassword");
// Verify that the role repository was called once
verify(roleRepository, times(1)).findByName("STUDENT");
// Verify that the user was saved once
verify(userRepository, times(1)).save(testUser);
}
}The goal is to test that the service correctly throws an error if no role was found.
// Code omitted
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
// Code omitted
@Test
void registerNewUser_ThrowsErrorIfRoleNotFound() {
// Simulate role lookup failing: findByName() returns empty
when(roleRepository.findByName("STUDENT")).thenReturn(Optional.empty());
// Assert that calling the service method throws the expected RuntimeException
RuntimeException exception = assertThrows(RuntimeException.class, () -> userService.registerNewUser(testUser));
// Verify the error message
assertEquals("Error: Default role not found.", exception.getMessage());
// Verify that the user was NOT saved
verify(userRepository, never()).save(any(User.class));
}
}The goal is to test that the service correctly throws an error if no role was found.
// Code omitted
@ExtendWith(MockitoExtension.class)
class UserServiceImplTest {
// Code omitted
@Test
void registerNewUser_ThrowsErrorIfRoleNotFound() {
// Simulate role lookup failing: findByName() returns empty
when(roleRepository.findByName("STUDENT")).thenReturn(Optional.empty());
// Assert that calling the service method throws the expected RuntimeException
RuntimeException exception = assertThrows(RuntimeException.class, () -> userService.registerNewUser(testUser));
// Verify the error message
assertEquals("Error: Default role not found.", exception.getMessage());
// Verify that the user was NOT saved
verify(userRepository, never()).save(any(User.class));
}
}The goal is to test the login functionality.
package edu.kirkwood.service.security;
import edu.kirkwood.model.Role;
import edu.kirkwood.model.User;
import edu.kirkwood.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserDetailsServiceImplTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserDetailsServiceImpl userDetailsService;
private User testUser;
@BeforeEach
void setUp() {
Role studentRole = new Role(); //
studentRole.setName("STUDENT"); //
testUser = new User(); //
testUser.setEmail("auth@kirkwood.edu"); //
testUser.setPassword("hashedPassword"); //
testUser.setRoles(Set.of(studentRole)); //
}
}Spring Boot Mail Starter: You'll need this for the reset my password story. This dependency makes it simple to connect to an email server to send the password reset email to your users.
send messages and receive and reply stories. It allows for real-time, two-way communication, which is great for chat or instant notifications (like a game score update).Validation: This helps you ensure data is correct before it even gets to your database. For example, you can make sure a user's email is a valid email address or that a password meets a minimum length, all with simple annotations (e.g., @Email, @Size).
ThymeLeaf: You can use Spring to build the HTML pages directly. The most common dependency for this is Thymeleaf. This is a great way to start because everything is in one project.
An alternative would be to have Spring Boot app act as an API (sending data as JSON), and you build a separate Client-Side frontend framework using a JavaScript framework like React, Angular, or Vue.
It's extremely easy to switch later. It gets right to the heart of why Spring Boot is so powerful. Think of your project in layers:
Data Layer (JPA/MySQL): Manages saving and fetching data.
Service Layer (Business Logic): Handles tasks like "register a user" or "calculate league standings."
Controller Layer (Web Requests): Takes in web requests and calls the right service.
Since you know JSP, Thymeleaf will feel very natural.
When you use Thymeleaf (like JSP), your controllers end the process by saying, "Okay, I got the data, now go show the user-profile.html page."
When you switch to a JS framework (like React), your controllers do the exact same work but end the process by saying, "Okay, I got the data, now I'll just hand this data (as JSON) to the React app, which will handle showing it."
Right now, you're running your app locally. This is your development sandbox.
On http://localhost:8080:
Only you can access it, on your own machine.
It only runs when you click the "run" button in IntelliJ.
It's perfect for development, testing, and debugging.
Deploying means putting your application on a powerful, public computer (a server) that is on 24/7.
Deployed on https://recleagues.com:
Anyone on the internet can access it.
It runs continuously in the "cloud".
Services like Microsoft Azure, Google Cloud Run, AWS, or Heroku provide these servers and handle things like security, scaling (handling thousands of users at once), and reliability.
Google Cloud Run is the "engine" that will actually run your code in the cloud. It's a service designed to run applications like this. It handles all the hard work of starting it, monitoring it, and even scaling it up (e.g., if 1,000 students try to register at once).
Firebase is the user-friendly dashboard and deployment tool for Cloud Run.
You upload your Spring Boot project to Firebase
Firebase takes your code, packages it up, and tells Cloud Run to run it.
Firebase then gives you a simple, public URL (like https://recleagues-1234.web.app) that you can share with the world.
Package Your App: First, you run a command like mvn clean package. This compiles all your Java code and bundles it, along with your embedded Tomcat server, into a single, executable .jar file. This is your all-in-one "app-in-a-box."
Create a "Container": A tool (like Docker) creates a container image.
Think of a container as a standardized shipping container. It doesn't matter what's inside (milk, cars, or your app), the outside is always the same shape and can be handled by any standard port.
This container image holds everything your app needs to run:
The specific version of Java (e.g., JDK 21).
Your executable .jar file.
Any other system files or settings.
It then tells Google Cloud Run to start one container using this new image.
Cloud Run grabs the image, starts it, and connects it to the public internet.
This is why we didn't have to install Java on the cloud server. The correct Java version was already inside the container, right next to your app.
By Marc Hauschildt
Web Technologies and Computer Software Development Instructor at Kirkwood Community College in Cedar Rapids, IA.