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

 更新时间:2022年01月18日 11:08:28   作者:翊君  
最近在设计自己的博客系统,涉及到用户注册与登录验证,所以下面这篇文章主要给大家介绍了关于Spring Boot用户注册验证的实现全过程,文中通过实例代码介绍的非常详细,需要的朋友可以参考下

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的定义

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中的password和repeatedPassword字段相匹配。

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

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

  • 我们创建了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);

  • 在注册的过程中如果产生任何报错,将会返回到注册页面。

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实现的基本的用户注册过程。

到此这篇关于Spring Boot用户注册验证实现的文章就介绍到这了,更多相关Spring Boot用户注册验证内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java中的CountDownLatch源码解析

    Java中的CountDownLatch源码解析

    这篇文章主要介绍了Java中的CountDownLatch源码解析,CountDownLatch类是一个同步辅助装置,允许一个或多个线程去等待直到另外的线程完成了一组操作,需要的朋友可以参考下
    2023-12-12
  • Java缓冲字符流的实现示例

    Java缓冲字符流的实现示例

    本文详细介绍了Java缓冲字符流的使用,通过使用BufferedReader和BufferedWriter,可以更高效地处理文本数据,提高读写效率,感兴趣的可以了解一下
    2024-12-12
  • Aspectj框架实战案例详解

    Aspectj框架实战案例详解

    这篇文章主要介绍了Aspectj框架实战,结合具体案例形式详细分析了Aspectj框架具体配置、使用、编译等相关操作技巧,需要的朋友可以参考下
    2020-01-01
  • RestTemplate发送HTTP GET请求使用方法详解

    RestTemplate发送HTTP GET请求使用方法详解

    这篇文章主要为大家介绍了关于RestTemplate发送HTTP GET请求的使用方法详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家<BR>33+多多进步
    2022-03-03
  • 如何使用@Slf4j和logback-spring.xml搭建日志框架

    如何使用@Slf4j和logback-spring.xml搭建日志框架

    这篇文章主要介绍了如何使用@Slf4j和logback-spring.xml搭建日志框架问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-06-06
  • java xml转为json的n种方法

    java xml转为json的n种方法

    本文给大家分享java xml转为json的两种方法,每种方法通过实例代码给大家介绍的非常详细,小编感觉第一种方法要比第二种方法好些,具体实现代码跟随小编一起看看吧
    2021-08-08
  • Java实现文件检索系统的示例代码

    Java实现文件检索系统的示例代码

    这篇文章主要为大家详细介绍了如何刘Java语言实现简易的文件检索系统,文中的示例代码讲解详细,对我们学习Java开发有一定的帮助,需要的可以参考一下
    2022-07-07
  • 详解MyBatis延迟加载是如何实现的

    详解MyBatis延迟加载是如何实现的

    MyBatis 的延迟加载(懒加载)特性允许在需要使用关联对象数据时才进行加载,而不是在执行主查询时就加载所有相关数据,我们将通过以下几个方面来深入了解MyBatis的延迟加载实现机制,需要的朋友可以参考下
    2024-07-07
  • SpringBoot实现任意位置获取HttpServletRequest对象

    SpringBoot实现任意位置获取HttpServletRequest对象

    这篇文章主要介绍了SpringBoot实现任意位置获取HttpServletRequest对象,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • Java 创建URL的常见问题及解决方案

    Java 创建URL的常见问题及解决方案

    这篇文章主要介绍了Java 创建URL的常见问题及解决方案的相关资料,需要的朋友可以参考下
    2016-10-10

最新评论