Spring Boot implements the whole process of user registration and verification

0. After reading this article, you will learn

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

1. General

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, authenticate, and persist user data.

2. Create User DTO Object

First, we need a DTO to include the user's registration information. This object should contain the basic information we need in the registration and verification process.

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 code

Note that we use the standard javax. XML on the fields of the DTO object Validation annotation - @ NotBlank.

@The difference between NotBlank, @ NotEmpty, @ NotNull

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

@NotEmpty: applicable to CharSequence, Collection, Map and Array objects. It cannot be null and the size of related objects is greater than 0.
@NotBlank: this annotation can only act on string types. String is not null, and the trimmed length after removing white space characters at both ends is greater than 0.

In the following sections, we will also customize annotations to verify the format of e-mail address and confirm the secondary password.

3. Implement a registered 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 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 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 attribute will be copied into the Servlet Response attribute, so that the view can find them here.

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

4. Verify registration data

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

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

4.1 built in verification

For a simple check, we will use @ NotBlank to validate the DTO object.

To trigger the validation process, we will validate 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) {
    //...
}
Copy code

4.2 custom validation to check email validity

Next, let's verify the email address to ensure that it is formatted correctly. We will build a custom validator and a custom validation annotation -- IsEmailValid.

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

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

Because @ Email in Hibernate will pass the verification XXX@XXX Such mailboxes, in fact, this is not in line with the regulations.

Interested readers can move here Hibernate validator: @Email accepts ask@stackoverflow as valid?.

Example 4.2.1 definition of isemailfailed annotation

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 code

@Target is used to specify the object range modified by the annotation

@The function of Retention is to explain how long the annotation annotated by it is retained

@Constraint is used to describe the method of custom annotation

@The function of Documented is to explain that the annotations modified by this annotation can be Documented by tools such as javadoc

About how to customize a Java Annotation, interested friends can see another article of mine.

Example 4.2.2 definition of emailvalidator

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 code

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

@NotBlank
@IsEmailVaild
private String email;
Copy 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 UserDto match.

Example 4.3.1 definition of ispasswordmatching annotation

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

Example 4.3.2 definition of 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 code

Now, apply the @ IsPasswordMatching annotation to our UserDto object.

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

4.4 check whether the account already exists

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

This is done after the form is verified. We put this verification in 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 code

Use @ Transactional to open transaction annotations. Why is @ Transactional added to the Service layer instead of the DAO layer?

If our transaction annotation @ Transactional is added to the DAO layer, the transaction must be submitted once as long as the addition, deletion and modification are made, so the transaction characteristics cannot be brought into play, especially the consistency of transactions. When concurrency problems occur, the data found by users from the database will deviate.

In general, our Service layer can call multiple DAO layers. We only need to add a transaction annotation @ Transactional in the Service layer, so that we can process multiple requests in one transaction, and the transaction characteristics will be brought into full play.

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

5. Persistent 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 = 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 code

In the above code, we can find:

  1. We created a ModelAndView object, which can either save 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 is reported during the registration process, it will return to the registration page.

6. Secure login

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

6.1 customize UserDetailsService

Let's start by customizing the 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 = 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 code

6.2 open New Authentication Provider

Then, in order to really enable the customized 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 code

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


Author: Yi Jun
Link: https://juejin.cn/post/7051279571341017101
 

Keywords: Java Spring Spring Boot

Added by fothers on Mon, 10 Jan 2022 14:15:59 +0200