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