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:
- 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);
- 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