If you're working on a Spring Security (and especially an OAuth) implementation, definitely have a look at the Learn Spring Security course:
>> LEARN SPRING SECURITYMocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.
Get started with mocking and improve your application tests using our Mockito guide:
Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.
Get started with understanding multi-threaded applications with our Java Concurrency guide:
Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:
Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.
But these can also be overused and fall into some common pitfalls.
To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:
Get started with Spring and Spring Boot, through the Learn Spring course:
>> LEARN SPRINGExplore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:
Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.
I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.
You can explore the course here:
Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.
Get started with Spring Data JPA through the guided reference course:
Refactor Java code safely β and automatically β with OpenRewrite.
Refactoring big codebases by hand is slow, risky, and easy to put off. Thatβs where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.
Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions β one for newcomers and one for experienced users. Youβll see how recipes work, how to apply them across projects, and how to modernize code with confidence.
Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.
β’ Spring Security Registration β Resend Verification Email
β’ Registration with Spring Security β Password Encoding
β’ The Registration API becomes RESTful
β’ Spring Security β Reset Your Password
β’ Registration β Password Strength and Rules
β’ Updating Your Password
β’ Notify User of Login From New Device or Location
1. Overview
In this tutorial, weβll implement a basic registration process with Spring Security. Weβll be building on top of the concepts we explored in the previous article, where we looked at login.
The goal here is to add a full registration process that allows a user to sign up, as well as validates and persists user data.
Further reading:
Servlet 3 Async Support with Spring MVC and Spring Security
Spring Security with Thymeleaf
Spring Security - Cache Control Headers
2. The Registration Page
First, weβll implement a simple registration page displaying the following fields:
- name (first and last name)
- password (and password confirmation field)
The following example shows a simple registration.html page:
Example 2.1.
<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
<div>
<label th:text="#{label.user.firstName}">first</label>
<input th:field="*{firstName}"/>
<p th:each="error: ${#fields.errors('firstName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.lastName}">last</label>
<input th:field="*{lastName}"/>
<p th:each="error : ${#fields.errors('lastName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.email}">email</label>
<input type="email" th:field="*{email}"/>
<p th:each="error : ${#fields.errors('email')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.password}">password</label>
<input type="password" th:field="*{password}"/>
<p th:each="error : ${#fields.errors('password')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.confirmPass}">confirm</label>
<input type="password" th:field="*{matchingPassword}"/>
</div>
<button type="submit" th:text="#{label.form.submit}">submit</button>
</form>
<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>
3. The User DTO Object
We need a Data Transfer Object to send all of the registration information to our Spring backend. The DTO object should have all the information weβll require later on when we create and populate our User object:
public class UserDto {
@NotNull
@NotEmpty
private String firstName;
@NotNull
@NotEmpty
private String lastName;
@NotNull
@NotEmpty
private String password;
private String matchingPassword;
@NotNull
@NotEmpty
private String email;
// standard getters and setters
}
Note that we used standard javax.validation annotations on the fields of the DTO object. Later on, weβll also implement our own custom validation annotations to validate the format of the email address, as well as the password confirmation (see Section 5).
4. The Registration Controller
A Sign-Up link on the login page will take the user to the registration page. The back end for that page lives in the registration controller and is mapped to β/user/registrationβ:
Example 4.1. The showRegistration Method
@GetMapping("/user/registration")
public String showRegistrationForm(WebRequest request, Model model) {
UserDto userDto = new UserDto();
model.addAttribute("user", userDto);
return "registration";
}
When the controller receives the request β/user/registration,β it creates the new UserDto object, which backs the registration form, binds it, and returns.
5. Validating Registration Data
Next, weβll look at the validations that the controller will perform when registering a new account:
- All required fields are filled (No empty or null fields).
- The email address is valid (well-formed).
- The password confirmation field matches the password field.
- The account doesnβt already exist.
5.1. The Built-In Validation
For the simple checks, weβll use the out of the box bean validation annotations on the DTO object. These are annotations like @NotNull, @NotEmpty, etc.
Then, to trigger the validation process, weβll simply annotate the object in the controller layer with the @Valid annotation:
public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
//...
}
5.2. Custom Validation to Check Email Validity
Then weβll validate the email address and make sure itβs well-formed. For this, weβll build a custom validator, as well as a custom validation annotation; weβll call it @ValidEmail.
Itβs important to note that weβre rolling our own custom annotation instead of Hibernateβs @Email because Hibernate considers the old intranet addresses format, myaddress@myserver, as valid (see Stackoverflow article), which isnβt good.
So hereβs the email validation annotation and the custom validator:
Example 5.2.1. The Custom Annotation for Email Validation
@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Note that we defined the annotation at the FIELD level, since thatβs where it applies conceptually.
Example 5.2.2. The Custom EmailValidator:
public class EmailValidator
implements ConstraintValidator<ValidEmail, String> {
private Pattern pattern;
private Matcher matcher;
private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
(.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
(.[A-Za-z]{2,})$";
@Override
public void initialize(ValidEmail constraintAnnotation) {
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context){
return (validateEmail(email));
}
private boolean validateEmail(String email) {
pattern = Pattern.compile(EMAIL_PATTERN);
matcher = pattern.matcher(email);
return matcher.matches();
}
}
Then weβll use the new annotation on our UserDto implementation:
@ValidEmail
@NotNull
@NotEmpty
private String email;
5.3. Using Custom Validation for Password Confirmation
We also need a custom annotation and validator to make sure that the password and matchingPassword fields match up:
Example 5.3.1. The Custom Annotation for Validating Password Confirmation
@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Note that the @Target annotation indicates that this is a TYPE level annotation. This is because we need the entire UserDto object to perform the validation.
The custom validator that will be called by this annotation is shown below:
Example 5.3.2. The PasswordMatchesValidator Custom Validator
public class PasswordMatchesValidator
implements ConstraintValidator<PasswordMatches, Object> {
@Override
public void initialize(PasswordMatches constraintAnnotation) {
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getMatchingPassword());
}
}
Then the @PasswordMatches annotation should be applied to our UserDto object:
@PasswordMatches
public class UserDto {
//...
}
All custom validations are, of course, evaluated along with all standard annotations when the entire validation process runs.
5.4. Check That the Account Doesnβt Already Exist
The fourth check weβll implement is verifying that the email account doesnβt already exist in the database.
This is performed after the form has been validated, and itβs done with the help of the UserService implementation.
Example 5.4.1. The Controllerβs registerUserAccount Method Calls the UserService Object
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request,
Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
// rest of the implementation
}
Example 5.4.2. UserService Checks for Duplicate Emails
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Override
public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
if (emailExists(userDto.getEmail())) {
throw new UserAlreadyExistException("There is an account with that email address: "
+ userDto.getEmail());
}
// the rest of the registration operation
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}
The UserService relies on the UserRepository class to check if a user with a given email address already exists in the database.
The actual implementation of the UserRepository in the persistence layer isnβt relevant for the current article; however, one quick way is to use Spring Data to generate the repository layer.
6. Persisting Data and Finishing-Up Form Processing
Next, weβll implement the registration logic in our controller layer:
Example 6.1. The RegisterAccount Method in the Controller
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request,
Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
return new ModelAndView("successRegister", "user", userDto);
}
Things to notice in the code above:
- The controller is returning a ModelAndView object, which is the convenient class for sending model data (user) tied to the view.
- The controller will redirect to the registration form if there are any errors set at validation time.
7. The UserService β Register Operation
Finally, weβll finish the implementation of the registration operation in the UserService:
Example 7.1. The IUserService Interface
public interface IUserService {
User registerNewUserAccount(UserDto userDto);
}
Example 7.2. The UserService Class
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Override
public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
if (emailExists(userDto.getEmail())) {
throw new UserAlreadyExistException("There is an account with that email address: "
+ userDto.getEmail());
}
User user = new User();
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setPassword(userDto.getPassword());
user.setEmail(userDto.getEmail());
user.setRoles(Arrays.asList("ROLE_USER"));
return repository.save(user);
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}
8. Loading User Details for Security Login
In our previous article, the login used hard-coded credentials. Weβll change that by using the newly registered user information and credentials. Furthermore, weβll implement a custom UserDetailsService to check the credentials for login from the persistence layer.
8.1. The Custom UserDetailsService
Weβll start with the custom user details service implementation:
@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException("No user found with username: " + email);
}
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked, getAuthorities(user.getRoles()));
}
private static List<GrantedAuthority> getAuthorities (List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}
8.2. Enable the New Authentication Provider
To enable the new user service in the Spring Security configuration, weβll simply need to add a reference to the UserDetailsService inside the authentication-manager element, and add the UserDetailsService bean:
Example 8.2. The Authentication Manager and the UserDetailsService
<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
<beans:bean id="userDetailsService" class="com.baeldung.security.MyUserDetailsService" />
Another option is via Java configuration:
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
9. Conclusion
In this article, we demonstrated a complete and almost production-ready registration process implemented with Spring Security and Spring MVC. Next, weβll discuss the process of activating the newly registered account by verifying the email of the new user.
