User Registration and Login using Spring Security

Spring Security is a powerful authentication and authorization framework for Java applications. It provides comprehensive security services for Spring-based applications, including features like user authentication, password encoding, session management, and protection against common security vulnerabilities.
In the previous article, Develop Spring Boot Application with PostgreSQL and Thymeleaf, we implemented event creation functionality. Now, we will extend our Event Registration System by implementing user registration and login functionality using Spring Security to secure our application.
By the end of this article, we will have implemented complete user authentication functionality where users can register with their email and password, login securely, and access protected resources. We'll also implement proper password encoding and session management.

Spring Security Configuration

First, we need to configure Spring Security in our application. Create a SecurityConfig class to define our security configuration. This configuration will handle user authentication, password encoding, and define which endpoints require authentication.
config / SecurityConfig.java
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**", "/css/**", "/js/**", "/").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form .loginPage("/auth/login") .loginProcessingUrl("/auth/login") .defaultSuccessUrl("/event/list", true) .failureUrl("/auth/login?error=true") .permitAll() ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/auth/login?logout=true") .permitAll() ).csrf(AbstractHttpConfigurer::disable); // Disable CSRF for simplicity return http.build(); } }
The security configuration above:
  • Password Encoder: Uses BCrypt hashing algorithm to securely encode passwords.
  • Security Filter Chain: Defines URL patterns that require authentication and configures login/logout behavior.
  • CSRF Protection: Disabled for simplicity, but should be enabled in production with proper configuration.

User Entities and Repositories

First, create the UserRole entity to manage user roles:
entities / UserRole.java
@Entity @Table(name = "user_roles") @Getter @Setter public class UserRole { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String userRoleName; @OneToMany(mappedBy = "userRole", cascade = CascadeType.ALL) private List<User> user; }
Now create the User entity for database mapping.
entities / User.java
@Entity @Table(name = "users") @Getter @Setter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String firstName; private String lastName; @Column(unique = true) private String emailAddress; private String password; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "userRoleId", referencedColumnName = "id") private UserRole userRole; }
Create repository interfaces for database operations:
repositories / UserRoleRepository.java
@Repository public interface UserRoleRepository extends JpaRepository<UserRole, Integer> { Optional<UserRole> findByUserRoleName(String userRoleName); }
repositories / UserRepository.java
@Repository public interface UserRepository extends JpaRepository<User, Integer> { Optional<User> findByEmailAddress(String emailAddress); boolean existsByEmailAddress(String emailAddress); }

Custom UserPrincipal Class

Create a CustomUserPrincipal class that implements UserDetailsto handle Spring Security authentication. This class wraps our User entity and provides security-specific functionality.
security / CustomUserPrincipal.java
@Getter @Setter @AllArgsConstructor public class CustomUserPrincipal implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(new SimpleGrantedAuthority("ROLE_" + user.getUserRole().getUserRoleName())); } @Override public String getUsername() { return user.getEmailAddress(); } @Override public String getPassword() { return user.getPassword(); } // Additional methods to access user data public String getFullName() { return user.getFirstName() + " " + user.getLastName(); } public Integer getId() { return user.getId(); } }

Custom UserDetailsService Implementation

Spring Security requires a UserDetailsService implementation to load user details during authentication. Create a custom implementation that retrieves user information from our database.
services / CustomUserDetailsService.java
@Service @AllArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByEmailAddress(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return new CustomUserPrincipal(user); } }

User Registration DTO

Create a Data Transfer Object (DTO) for user registration with validation annotations:
dto / UserRegistrationDto.java
@Getter @Setter public class UserRegistrationDto { @NotBlank(message = "First name is required") private String firstName; private String lastName; @NotBlank(message = "Email is required") @Email(message = "Please provide a valid email address") private String emailAddress; @NotBlank(message = "Password is required") @Size(min = 8, message = "Password must be at least 8 characters long") private String password; }

User Registration Service

Implement a service class to handle user registration. This service will validate user input, encode passwords, and save new users to the database.
services / UserService.java
@Service @AllArgsConstructor public class UserService { private final UserRepository userRepository; private final UserRoleRepository userRoleRepository; private final PasswordEncoder passwordEncoder; public User registerUser(UserRegistrationDto registrationDto) { // Check if email already exists if (userRepository.existsByEmailAddress(registrationDto.getEmailAddress())) { throw new RuntimeException("Email address already registered"); } // Get default user role UserRole userRole = userRoleRepository.findByUserRoleName("USER") .orElseThrow(() -> new RuntimeException("Default user role not found")); // Create new user User user = new User(); user.setFirstName(registrationDto.getFirstName()); user.setLastName(registrationDto.getLastName()); user.setEmailAddress(registrationDto.getEmailAddress()); user.setPassword(passwordEncoder.encode(registrationDto.getPassword())); user.setUserRole(userRole); return userRepository.save(user); } }
The registration service above:
  • Email Validation: Checks if the email address is already registered.
  • Password Encoding: Uses BCrypt to securely hash passwords before storing.
  • Default Role: Assigns new users to the "USER" role by default.
  • User Creation: Saves the new user to the database with encoded password.

Authentication Controller

Create a controller to handle user registration and login requests. This controller will manage form submissions and redirect users appropriately.
controllers / AuthController.java
@Controller @RequestMapping("/auth") @AllArgsConstructor public class AuthController { private final UserService userService; @GetMapping("/register") public String showRegistrationForm(Model model) { model.addAttribute("user", new UserRegistrationDto()); return "auth/register"; } @PostMapping("/register") public String registerUser(@ModelAttribute @Valid UserRegistrationDto registrationDto, BindingResult result, Model model) { if (result.hasErrors()) { return "auth/register"; } try { userService.registerUser(registrationDto); return "redirect:/auth/login?registered=true"; } catch (RuntimeException e) { model.addAttribute("error", e.getMessage()); return "auth/register"; } } @GetMapping("/login") public String showLoginForm(@RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout, @RequestParam(value = "registered", required = false) String registered, Model model) { if (error != null) { model.addAttribute("error", "Invalid email or password"); } if (logout != null) { model.addAttribute("message", "You have been logged out successfully"); } if (registered != null) { model.addAttribute("message", "Registration successful! Please login."); } return "auth/login"; } }

Registration and Login Forms

Create HTML forms for user registration and login using Thymeleaf templates. First, let's create the registration form:
resources / templates / auth / register.html
<form class="pt-6 space-y-4" th:action="@{/auth/register}" th:object="${user}" method="post"> <div class="grid md:grid-cols-2 md:gap-6"> <div> <label for="firstName" class="block mb-2 text-sm font-medium text-gray-900"> First Name <span class="text-red-400">*</span> </label> <input type="text" id="firstName" th:field="*{firstName}" class="form-input" required/> <div th:if="${#fields.hasErrors('firstName')}" class="text-red-600 text-sm mt-1" th:errors="*{firstName}"></div> </div> <div class="pt-4 md:pt-0"> <label for="lastName" class="block mb-2 text-sm font-medium text-gray-900"> Last Name </label> <input type="text" id="lastName" th:field="*{lastName}" class="form-input"/> <div th:if="${#fields.hasErrors('lastName')}" class="text-red-600 text-sm mt-1" th:errors="*{lastName}"></div> </div> </div> <div> <label for="emailAddress" class="block mb-2 text-sm font-medium text-gray-900"> Email Address <span class="text-red-400">*</span> </label> <input type="email" id="emailAddress" th:field="*{emailAddress}" class="form-input" required/> <div th:if="${#fields.hasErrors('emailAddress')}" class="text-red-600 text-sm mt-1" th:errors="*{emailAddress}"></div> </div> <div> <label for="password" class="block mb-2 text-sm font-medium text-gray-900"> Password <span class="text-red-400">*</span> </label> <input type="password" id="password" th:field="*{password}" class="form-input" required minlength="8"/> <div th:if="${#fields.hasErrors('password')}" class="text-red-600 text-sm mt-1" th:errors="*{password}"></div> </div> <div th:if="${error}" class="text-red-600 text-sm" th:text="${error}"></div> <button type="submit" class="btn-primary"> Register </button> <div class="text-sm text-center pt-4"> Already have an account? <a th:href="@{/auth/login}" class="text-blue-600 hover:underline"> Login here </a> </div> </form>
Now, create the login form. Spring Security will automatically handle the login process when we submit to /login:
resources / templates / auth / login.html
<form class="pt-6 space-y-4" th:action="@{/auth/login}" method="post"> <div> <label for="username" class="block mb-2 text-sm font-medium text-gray-900"> Email Address <span class="text-red-400">*</span> </label> <input type="email" id="username" name="username" class="form-input" required/> </div> <div> <label for="password" class="block mb-2 text-sm font-medium text-gray-900"> Password <span class="text-red-400">*</span> </label> <input type="password" id="password" name="password" class="form-input" required/> </div> <div th:if="${error}" class="text-red-600 text-sm" th:text="${error}"></div> <div th:if="${message}" class="text-green-600 text-sm" th:text="${message}"></div> <button type="submit" class="btn-primary"> Login </button> <div class="text-sm text-center pt-4"> Don't have an account? <a th:href="@{/auth/register}" class="text-blue-600 hover:underline"> Register here </a> </div> </form>
The forms above include:
  • CSRF Protection: Hidden CSRF tokens are automatically added by Thymeleaf for security.
  • Validation: Client-side validation for required fields and email format.
  • Error Handling: Display error messages for failed authentication attempts.
  • Responsive Design: Forms are styled to work well on different screen sizes.

Database Schema Updates

We need to update our database schema to support user roles. Add the following SQL to insert default user roles:
resources / data.sql
INSERT INTO user_roles (user_role_name) VALUES ('USER') ON CONFLICT DO NOTHING; INSERT INTO user_roles (user_role_name) VALUES ('ADMIN') ON CONFLICT DO NOTHING;
Make sure to set spring.sql.init.mode to always in your application.yml to ensure the data.sql script runs on startup.

Testing the Authentication Flow

After implementing all the components above, you can test the complete authentication flow:
  1. Start the application and navigate to /auth/register
  2. Register a new user with valid email and password
  3. Login with the registered credentials at /auth/login
  4. Access protected resources - you should now be authenticated
  5. Logout using /logout endpoint

Accessing Current User in Controllers

With the CustomUserPrincipal approach, you can easily access the current logged-in user in your controllers:
controllers / EventController.java
@Controller @RequestMapping("/event") @AllArgsConstructor public class EventController { private final EventService eventService; @GetMapping("/create") public String showCreateEventForm(Model model, Authentication authentication) { // Access current user using Authentication CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal(); User currentUser = userPrincipal.getUser(); model.addAttribute("event", new Event()); model.addAttribute("userName", userPrincipal.getFullName()); return "event/create"; } @PostMapping("/create") public String createEvent(@ModelAttribute Event event, @AuthenticationPrincipal CustomUserPrincipal userPrincipal) { // Access current user using @AuthenticationPrincipal annotation User currentUser = userPrincipal.getUser(); // Set the event creator event.setCreatedBy(currentUser); eventService.createEvent(event); return "redirect:/event/list"; } @GetMapping("/my-events") public String showMyEvents(Model model, @AuthenticationPrincipal CustomUserPrincipal userPrincipal) { User currentUser = userPrincipal.getUser(); List<Event> userEvents = eventService.getEventsByUser(currentUser); model.addAttribute("events", userEvents); model.addAttribute("userName", userPrincipal.getFullName()); return "event/my-events"; } }
This approach gives you direct access to both the User entity data and security-specific information, making it easy to work with authenticated users throughout your application.
You can enhance this implementation by adding:
  • Email Verification: Send confirmation emails before activating accounts
  • Password Reset: Allow users to reset forgotten passwords
  • Role-Based Access: Implement different access levels for admin and regular users
  • Account Locking: Lock accounts after multiple failed login attempts
  • Remember Me: Add "Remember Me" functionality for persistent sessions
You have successfully implemented user registration and login functionality using Spring Security. The users can now register, login, and access protected resources securely. You can refer to this GitHub repository for the complete implementation and reference, https://github.com/MahediSabuj/event-registration/tree/v1.0.1
In the next article, we will enhance the Event Registration System by implementing role-based access control and event participation features.
Write your Comment