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!