SoFunction
Updated on 2025-03-09

Record the entire process of Spring Boot user registration verification

1. Overview

In this article, we will use Spring Boot to implement a basic mailbox account registration and verification process.

Our goal is to add a complete registration process that allows users to register, verify, and persist user data.

2. Create User DTO Object

First of all, we need a DTO to include user registration information. This object should contain the basic information we need during the registration and verification process.

Example 2.1 Definition of UserDto

package ;

import ;
import ;
import ;

public class UserDto {

    @NotBlank
    private String username;

    @NotBlank
    private String password;

    @NotBlank
    private String repeatedPassword;

    @NotBlank
    private String email;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
         = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
         = password;
    }

    public String getRepeatedPassword() {
        return repeatedPassword;
    }

    public void setRepeatedPassword(String repeatedPassword) {
         = repeatedPassword;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
         = email;
    }
}

Please note that we use the standard annotation on the fields of the DTO object - @NotBlank.

The difference between @NotBlank, @NotEmpty, @NotNull

@NotNull: Applicable to CharSequence, Collection, Map and Array objects, cannot be null, but can be an empty set (size = 0).

@NotEmpty: Applicable to CharSequence, Collection, Map and Array objects, cannot be null and the size of the related object is greater than 0.
@NotBlank: This annotation can only be used on String types. The length of the String is not null and the trimmed length after removing the whitespace characters at both ends is greater than 0.

In the following chapters, we will also customize the annotations to verify the format of the email address and confirm the secondary password.

3. Implement a registered controller

The registration link on the login page brings the user to the registration page:

Example 3.1 Definition of RegistrationController

package ;

import ;
import ;
import ;
import ;

@Controller
public class RegistrationController {

    @GetMapping("/user/registration")
    public String showRegistrationForm(Model model) {
        ("user", new UserDto());
        return "registration";
    }

}

When the RegistrationController receives the request /user/registration, it creates a new UserDto object, binds it to the Model, and returns to the registration page.

The Model object is responsible for passing data between the controller Controller and the view view that presents the data.

In fact, the data placed in the Model property will be copied into the Servlet Response property so that the view can find them here.

Broadly speaking, Model refers to M in the MVC framework, that is, Model (model). In a narrow sense, a Model is a key-value collection.

4. Verify registration data

Next, let's look at the verification the controller will perform when registering a new account:

  • All fields that must be filled in are filled in and there are no empty fields
  • This email address is valid
  • Password confirmation field matches password field
  • This account does not exist

4.1 Built-in verification

For simple checks, we will use @NotBlank to verify the DTO object.

To trigger the verification process, we will verify the object with the @Valid annotation in the Controller.

Example 4.1 registerUserAccount

public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
  HttpServletRequest request, Errors errors) {
    //...
}

4.2 Custom verification to check the validity of emails

Next, let's verify the email address to ensure it is in the correct format. We will create a custom validator for this, as well as a custom validation annotation - IsEmailValid.

Here are the email verification annotations IsEmailValid and custom validator EmailValidator:

Why not use Hibernate's built-in @Email?

Because @Email in Hibernate will verify that it passes emails such as XXX@XXX, this actually does not comply with the regulations.

Interested readers can move hereHibernate validator: @Email accepts ask@* as valid?

Example 4.2.1 Definition of IsEmailVaild annotation

package ;

import static .ANNOTATION_TYPE;
import static ;
import static ;
import static ;

import ;
import ;
import ;

import ;
import ;

@Target({ TYPE, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = )
@Documented
public @interface IsEmailVaild {

    String message() default "Invalid Email";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

@Target's function is to illustrate the scope of objects modified by this annotation

@Retention means how long the annotation annotated by it is retained

@Constraint's function is to explain the method of custom annotation

@Documented's function is to indicate that the annotation modified by this annotation can be documented by tools such as javadoc.

Regarding how to customize a Java Annotation, interested friends can check out another article of mine.

Example 4.2.2 Definition of EmailValidator

package ;

import ;
import ;

import ;
import ;

public class EmailValidator implements ConstraintValidator<IsEmailVaild, String> {
    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,})$";
    private static final Pattern PATTERN = (EMAIL_PATTERN);

    @Override
    public void initialize(IsEmailVaild constraintAnnotation) {
    }

    @Override
    public boolean isValid(final String username, final ConstraintValidatorContext context) {
        return (validateEmail(username));
    }

    private boolean validateEmail(final String email) {
        Matcher matcher = (email);
        return ();
    }
}

Now let's use the new annotation on our UserDto implementation.

@NotBlank
@IsEmailVaild
private String email;

4.3 Use custom verification to confirm password

We also need a custom annotation and validator to ensure that the password and repeatedPassword fields in UserDto match.

Example 4.3.1 Definition of IsPasswordMatching annotation

package ;

import static .ANNOTATION_TYPE;
import static ;
import static ;

import ;
import ;
import ;

import ;
import ;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = )
@Documented
public @interface IsPasswordMatching {

    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 verification.

Example 4.3.2 Definition of PasswordMatchingValidator

package ;

import ;
import ;

import ;

public class PasswordMatchingValidator implements ConstraintValidator<IsPasswordMatching, Object> {

    @Override
    public void initialize(final IsPasswordMatching constraintAnnotation) {
        //
    }

    @Override
    public boolean isValid(final Object obj, final ConstraintValidatorContext context) {
        final UserDto user = (UserDto) obj;
        return ().equals(());
    }

}

Now apply the @IsPasswordMatching annotation to our UserDto object.

@IsPasswordMatching
public class UserDto {
    //...
}

4.4 Check whether the account already exists

The fourth check we want to implement is to verify that the email account already exists in the database.

This is done after the form is verified, and we put this verification in UserService.

Example 4.4.1 UserService

package ;

import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;

import ;

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public User registerNewUserAccount(UserDto userDto) throws UserExistException {
        if (hasEmailExisted(())) {
            throw new UserExistException("The email has already existed: "
                    + ());
        }

        User user = new User();
        (());
        ((()));
        (());
        return (user);
    }
    private boolean hasEmailExisted(String email) {
        return (email) != null;
    }
}

Use @Transactional to enable transaction annotation. As for why @Transactional is added to the Service layer instead of the DAO layer?

If our transaction annotation @Transactional is added to the DAO layer, then as long as we add, delete and modify, the transaction must be submitted, and the characteristics of the transaction cannot be exerted, especially the consistency of the transaction. When concurrency problems occur, the data users find from the database will be deviated.

Generally speaking, our Service layer can call multiple DAO layers. We only need to add a transaction annotation @Transactional to the Service layer, so that we can handle multiple requests in one transaction, and the characteristics of the transaction will be fully utilized.

UserService relies on the UserRepository class to check whether there is a user account with the same mailbox in the database. Of course, in this article we will not involve the implementation of UserRepository.

5. Persistence processing

Then we continue to implement the persistence logic in the RegistrationController.

@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
        @ModelAttribute("user") @Valid UserDto userDto,
        HttpServletRequest request,
        Errors errors) {

    try {
        User registered = (userDto);
    } catch (UserExistException uaeEx) {
        ModelAndView mav = new ModelAndView();
        ("message", "An account for that username/email already exists.");
        return mav;
    }

     return new ModelAndView("successRegister", "user", userDto);
}

In the above code we can find:

  • We have created a ModelAndView object that can save data or return a View.

Three common usages of ModelAndView

(1) new ModelAndView(String viewName, String attributeName, Object attributeValue);

(2) (String viewName);

(String attributeName, Object attributeValue);

(3) new ModelAndView(String viewName);

  • If any error occurs during the registration process, it will be returned to the registration page.

6. Secure login

In this section, we will implement a custom UserDetailsService to check login credentials from the persistence layer.

6.1 Custom UserDetailsService

Let's start with customizing UserDetailsService.

Example 6.1.1 MyUserDetailsService

@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = (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 (
                (), ().toLowerCase(), enabled, accountNonExpired,
                credentialsNonExpired, accountNonLocked, getAuthorities(()));
    }

    private static List<GrantedAuthority> getAuthorities (List<String> roles) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            (new SimpleGrantedAuthority(role));
        }
        return authorities;
    }
}

6.2 Turn on New Authentication Provider

Then, in order to truly enable the custom MyUserDetailsService, we also need to add the following code to the SecurityConfig configuration file:

@Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        (authProvider());
    }
Copy the code

Due to space limitations, we will not expand the SecurityConfig configuration file in detail here.

7. Conclusion

So far we have completed a basic user registration process implemented by Spring Boot.

This is the end of this article about Spring Boot user registration verification implementation. For more relevant Spring Boot user registration verification content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!