User Registration and Login using Spring Security
Spring Security Configuration
SecurityConfig class to define our security configuration. This configuration will handle user authentication, password encoding, and define which endpoints require authentication.config / SecurityConfig.java
public class SecurityConfig {
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
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();
}
}- 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
entities / UserRole.java
public class UserRole {
private Integer id;
private String userRoleName;
private List<User> user;
}entities / User.java
public class User {
private Integer id;
private String firstName;
private String lastName;
private String emailAddress;
private String password;
private UserRole userRole;
}repositories / UserRoleRepository.java
public interface UserRoleRepository extends JpaRepository<UserRole, Integer> {
Optional<UserRole> findByUserRoleName(String userRoleName);
}repositories / UserRepository.java
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByEmailAddress(String emailAddress);
boolean existsByEmailAddress(String emailAddress);
}Custom UserPrincipal Class
CustomUserPrincipal class that implements UserDetailsto handle Spring Security authentication. This class wraps our User entity and provides security-specific functionality.security / CustomUserPrincipal.java
public class CustomUserPrincipal implements UserDetails {
private User user;
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + user.getUserRole().getUserRoleName()));
}
public String getUsername() {
return user.getEmailAddress();
}
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
UserDetailsService implementation to load user details during authentication. Create a custom implementation that retrieves user information from our database.services / CustomUserDetailsService.java
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
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
dto / UserRegistrationDto.java
public class UserRegistrationDto {
private String firstName;
private String lastName;
private String emailAddress;
private String password;
}User Registration Service
services / UserService.java
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);
}
}- 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
controllers / AuthController.java
public class AuthController {
private final UserService userService;
public String showRegistrationForm(Model model) {
model.addAttribute("user", new UserRegistrationDto());
return "auth/register";
}
public String registerUser( 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";
}
}
public String showLoginForm( String error,
String logout,
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
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>/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>- 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
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;spring.sql.init.mode to always in your application.yml to ensure the data.sql script runs on startup.Testing the Authentication Flow
- Start the application and navigate to
/auth/register - Register a new user with valid email and password
- Login with the registered credentials at
/auth/login - Access protected resources - you should now be authenticated
- Logout using
/logoutendpoint
Accessing Current User in Controllers
controllers / EventController.java
public class EventController {
private final EventService eventService;
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";
}
public String createEvent( Event event,
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";
}
public String showMyEvents(Model model,
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";
}
}- 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