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

网友投稿 592 2022-11-07

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

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

目录1. 概述2. 创建User DTO Object3. 实现一个注册Controller4. 验证注册数据4.1 内置的验证4.2 自定义验证以检查电子邮件的有效性4.3 使用自定义验证来确认密码4.4 检查该账户是否已经存在5. 持久化处理6. 安全登录6.1 自定义UserDetailsService6.2 开启New Authentication Provider7. 结语

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内置的@Emailhttp://?

因为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 {

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 {

@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 (usFQRIymKJEwer == 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 getAuthorities (List roles) {

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

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:传输层 可靠传输 连续ARQ协议和滑动窗口协议
下一篇:Docker 镜像管理
相关文章

 发表评论

暂时没有评论,来抢沙发吧~