You will have learned by the end of this article

  • How to implement a basic registration verification process
  • How do I customize an annotation

1. An overview of the

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

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

2. Create a User DTO Object

First, we need a DTO to include the user’s registration information. This object should contain the basic information we need for registration and validation.

Example 2.1 Definition of UserDto

package com.savagegarden.web.dto; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; 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) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getRepeatedPassword() { return repeatedPassword; } public void setRepeatedPassword(String repeatedPassword) { this.repeatedPassword = repeatedPassword; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; }}Copy the code

Note that we use the standard Javax. Validation annotation — @notblank — on the fields of the DTO object.

@notblank, @notempty, @notnull

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

@notempty: applies to CharSequence, Collection, Map, and Array objects. Cannot be null and the size of the associated object is greater than 0. NotBlank: This annotation applies only to strings. The String is non-null, and the length of trimmed characters is greater than 0.

In the following sections, we will also customize annotations to validate the format of the E-mail address and confirm the second password.

3. Implement a registration Controller

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

Example 3.1 Definition of RegistrationController

package com.savagegarden.web.controller; import com.savagegarden.web.dto.UserDto; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @Controller public class RegistrationController { @GetMapping("/user/registration") public String showRegistrationForm(Model model) { model.addAttribute("user", new UserDto()); return "registration"; }}Copy the code

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

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

In fact, the data put into the Model properties will be copied to the Servlet Response properties so that the view can find them there.

Broadly speaking, Model refers to M in the MVC framework, the Model. In a narrow sense, a Model is a key-value set.

4. Verify registration data

Next, let’s look at the validation that the controller performs when registering a new account:

  • All required fields are filled in and there are no empty fields
  • The email address is valid
  • The password confirmation field is consistent with the password field
  • The account does not exist

4.1 Built-in validation

For a simple check, we’ll use @notblank to validate the DTO object.

To trigger the validation process, we will use the @valid annotation in the Controller to validate the object.

Example 4.1 registerUserAccount

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

4.2 Customize authentication to check email validity

Next, let’s validate the E-mail address to make sure it’s formatted correctly. We’ll create a custom validator for this, along with a custom validation annotation –IsEmailValid.

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

Why not use Hibernate’s built-in @email?

Because @email in Hibernate validates through a mailbox like XXX@XXX, this is not a rule.

Interested readers can go to Hibernate Validator: @email accepts ask@stackoverflow as valid? .

Example 4.2.1 IsEmailVaild annotation definition

package com.savagegarden.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({ TYPE, FIELD, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = EmailValidator.class) @Documented public @interface IsEmailVaild { String message() default "Invalid Email"; Class<? >[] groups() default {}; Class<? extends Payload>[] payload() default {}; }Copy the code

The @target function is to specify the scope of the object that the annotation modifies

The effect of @Retention is to indicate how long the annotations annotated by it remain

The @constraint is used to explain how to customize annotations

The purpose of @documented is to indicate that an annotation modified by this annotation can be Documented by a tool such as Javadoc

For those of you interested in how to customize a Java Annotation, check out my other article.

Example 4.2.2 EmailValidator definition

package com.savagegarden.validation; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; 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 = Pattern.compile(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 = PATTERN.matcher(email); return matcher.matches(); }}Copy the code

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

@NotBlank
@IsEmailVaild
private String email;
Copy the code

4.3 Use custom Authentication to Confirm passwords

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

Example 4.3.1 IsPasswordMatching definition of annotations

package com.savagegarden.validation; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import javax.validation.Constraint; import javax.validation.Payload; @Target({ TYPE, ANNOTATION_TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = PasswordMatchingValidator.class) @Documented public @interface IsPasswordMatching { String message() default "Passwords don't match"; Class<? >[] groups() default {}; Class<? extends Payload>[] payload() default {}; }Copy the code

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 definition of case 4.3.2 PasswordMatchingValidator

package com.savagegarden.validation; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import com.savagegarden.web.dto.UserDto; 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 user.getPassword().equals(user.getRepeatedPassword()); }}Copy the code

Now apply the @isPasswordMatching annotation to our UserDto object.

@IsPasswordMatching
public class UserDto {
    //...
}
Copy the code

4.4 Checking whether the Account already exists

The fourth check we implement is to verify that the E-mail account already exists in the database.

This is done after the form is validated, which we put in the UserService.

Example 4.4.1 UserService

package com.savagegarden.service.impl;

import com.savagegarden.error.user.UserExistException;
import com.savagegarden.persistence.dao.UserRepository;
import com.savagegarden.persistence.model.User;
import com.savagegarden.service.IUserService;
import com.savagegarden.web.dto.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@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(userDto.getEmail())) {
            throw new UserExistException("The email has already existed: "
                    + userDto.getEmail());
        }

        User user = new User();
        user.setUsername(userDto.getUsername());
        user.setPassword(passwordEncoder.encode(userDto.getPassword()));
        user.setEmail(userDto.getEmail());
        return userRepository.save(user);
    }
    private boolean hasEmailExisted(String email) {
        return userRepository.findByEmail(email) != null;
    }
}
Copy the code

Use @transactional to enable transaction annotations. Why does @Transactional apply to the Service layer instead of the DAO layer?

If Transactional annotation @Transactional was added to the DAO layer, the transaction would have to be committed once as long as it was added, deleted, and Transactional features, especially Transactional consistency, would be lost. When there is a concurrency problem, the data from the database will be inaccurate.

The Transactional Transactional annotation @Transactional in the Service layer allows you to handle multiple requests within a transaction.

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

5. Persistence

We then proceed to implement the persistence logic in RegistrationController.

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

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

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

In the code above we can find:

  1. We create a ModelAndView object that can either hold data or return a View.

Three common uses of ModelAndView

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

(2) mav.setViewName(String viewName);

mav.addObejct(String attributeName, Object attributeValue);

(3) new ModelAndView(String viewName);

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

6. Secure login

In this section, we implement a custom UserDetailsService that checks login credentials from the persistence layer.

6.1 Customizing UserDetailsService

Let’s start by customizing the UserDetailsService.

Cases of 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 = 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().toLowerCase(), 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; }}Copy the code

6.2 Enabling the New Authentication Provider

Then, in order to actually 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 {
        auth.authenticationProvider(authProvider());
    }
Copy the code

For lack of space, we won’t expand the SecurityConfig configuration file in detail here.

7. Conclusion

We have now completed a basic user registration process implemented by Spring Boot. The pages and some classes in the project are not reflected in the article. If you need friends, you can pay attention to my public account garden Savage and reply zhuce to obtain the project code.