Spring Boot实现用户注册验证全过程

0. 阅读完本文你将会学会

  • 如何实现一个基本的注册验证过程
  • 如何自定义一个注解

1. 概述

在这篇文章中,我们将使用Spring Boot实现一个基本的邮箱注册账户以及验证的过程。

我们的目标是添加一个完整的注册过程,允许用户注册,验证,并持久化用户数据。

2. 创建User DTO Object

首先,我们需要一个DTO来囊括用户的注册信息。这个对象应该包含我们在注册和验证过程中所需要的基本信息。

例2.1 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;
    }
}


复制代码

请注意我们在DTO对象的字段上使用了标准的javax.validation注解——@NotBlank

@NotBlank@NotEmpty@NotNull的区别

@NotNull: 适用于CharSequence, Collection, Map 和 Array 对象,不能是null,但可以是空集(size = 0)。

@NotEmpty: 适用于CharSequence, Collection, Map 和 Array 对象,不能是null并且相关对象的size大于0。
@NotBlank: 该注解只能作用于String类型。String非null且去除两端空白字符后的长度(trimmed length)大于0。

在下面的章节里,我们还将自定义注解来验证电子邮件地址的格式以及确认二次密码。

3. 实现一个注册Controller

登录页面上的注册链接将用户带到注册页面:

例3.1 RegistrationController的定义

扫描二维码关注公众号,回复: 13666611 查看本文章
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";
    }

}

复制代码

RegistrationController收到请求/user/registration时,它创建了新的UserDto对象,将其绑定在Model上,并返回了注册页面registration.html

Model 对象负责在控制器Controller和展现数据的视图View之间传递数据。

实际上,放到 Model 属性中的数据将会复制到 Servlet Response 的属性中,这样视图就能在这里找到它们了。

从广义上来说,Model 指的是 MVC框架 中的 M,即 Model(模型)。从狭义上讲,Model 就是个 key-value 集合。

4. 验证注册数据

接下来,让我们看看控制器在注册新账户时将执行的验证:

  • 所有必须填写的字段都已填写且没有空字段
  • 该电子邮件地址是有效的
  • 密码确认字段与密码字段相符
  • 该账户不存在

4.1 内置的验证

对于简单的检查,我们将使用@NotBlank来验证DTO对象。

为了触发验证过程,我们将在Controller中用@Valid注解来验证对象。

例4.1 registerUserAccount

public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
  HttpServletRequest request, Errors errors) {
    //...
}
复制代码

4.2 自定义验证以检查电子邮件的有效性

下一步,让我们验证电子邮件地址,以保证它的格式是正确的。我们将为此建立一个自定义验证器,以及一个自定义验证注解--IsEmailValid

下面是电子邮件验证注解IsEmailValid和自定义验证器EmailValidator:

为什么不使用Hibernate内置的@Email?

因为Hibernate中的@Email会验证通过XXX@XXX之类的邮箱,其实这是不符合规定的。

感兴趣的读者朋友可以移步此处Hibernate validator: @Email accepts ask@stackoverflow as valid?

例4.2.1 IsEmailVaild注解的定义

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 {};
}

复制代码

@Target的作用是说明了该注解所修饰的对象范围

@Retention的作用是说明了被它所注解的注解保留多久

@Constraint的作用是说明自定义注解的方法

@Documented的作用是说明了被这个注解修饰的注解可以被例如javadoc此类的工具文档化

关于如何自定义一个Java Annotation,感兴趣的朋友可以看看我的另一篇文章。

例4.2.2 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();
    }
}


复制代码

现在让我们在我们的UserDto实现上使用新注解。

@NotBlank
@IsEmailVaild
private String email;
复制代码

4.3 使用自定义验证来确认密码

我们还需要一个自定义注解和验证器,以确保UserDto中的passwordrepeatedPassword字段相匹配。

例4.3.1 IsPasswordMatching注解的定义

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 {};

}

复制代码

请注意,@Target注解表明这是一个Type级别的注解。这是因为我们需要整个UserDto对象来执行验证。

例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());
    }

}
复制代码

现在,将@IsPasswordMatching注解应用到我们的UserDto对象。

@IsPasswordMatching
public class UserDto {
    //...
}
复制代码

4.4 检查该账户是否已经存在

我们要实现的第四个检查是验证该电子邮件帐户在数据库中是否已经存在。

这是在表单被验证后进行的,我们把这项验证放在了UserService。

例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;
    }
}
复制代码

使用@Transactional开启事务注解,至于为什么@Transactional加在Service层而不是DAO层?

如果我们的事务注解@Transactional加在DAO层,那么只要做增删改,就要提交一次事务,那么事务的特性就发挥不出来,尤其是事务的一致性。当出现并发问题的时候,用户从数据库查到的数据都会有所偏差。

一般的时候,我们的Service层可以调用多个DAO层,我们只需要在Service层加一个事务注解@Transactional,这样我们就可以一个事务处理多个请求,事务的特性也会充分地发挥出来。

UserService依靠UserRepository类来检查数据库中是否已存在拥有相同邮箱的用户账户。当然在本文中我们不会涉及到UserRepository的实现。

5. 持久化处理

然后我们继续实现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);
}
复制代码

在上面的代码中我们可以发现:

  1. 我们创建了ModelAndView对象,该对象既可以保存数据也可以返回一个View。

常见的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. 在注册的过程中如果产生任何报错,将会返回到注册页面。

6. 安全登录

在本节内容中,我们将实现一个自定义的UserDetailsService,从持久层检查登录的凭证。

6.1 自定义UserDetailsService

让我们从自定义UserDetailsService开始。

例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;
    }
}
复制代码

6.2 开启New Authentication Provider

然后,为了真正地能够开启自定义的MyUserDetailsService,我们还需要在SecurityConfig配置文件中加入以下代码:

@Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authProvider());
    }
复制代码

限于篇幅,我们就不在这里详细展开SecurityConfig配置文件。

7. 结语

至此我们完成了一个由Spring Boot实现的基本的用户注册过程。项目中的页面以及部分类没有在文章中体现,需要的小伙伴可以关注我的公众号花园野人,回复zhuce获取项目代码。

猜你喜欢

转载自juejin.im/post/7051279571341017101