Spring Boot用户注册验证的实现全过程记录
作者:翊君 发布时间:2023-01-03 01:58:34
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实现的基本的用户注册过程。
来源:https://juejin.cn/post/7051279571341017101
猜你喜欢
- 今天启动springboot项目时失败了解决检查原因发现是启动类的MapperScan("")的值写到类名了,改成类所在
- 本博文参考自https://www.jb51.net/article/100269.htmwww.jb51.net/article/1002
- 在用maven打包时,出现过如下两个错误:错误1:程序包javax.servlet不存在,程序包javax.servlet.http不存在错
- 概述:Flutter 标签类控件大全ChipFlutter内置了多个标签类控件,但本质上它们都是同一个控件,只不过是属性参数不同而已,在学习
- 本文实例讲述了C#序列化与反序列化的方法。分享给大家供大家参考。具体分析如下:把“对象”转换为“字节序列”的过程称为对象的序列化。 
- 前言大家看标题,可能会有点儿懵,什么是ViewPagers,因为在很久之前,我们使用的都是ViewPager,但是现在更多的是在用ViewP
- 一、题目描述题目实现:网络资源的断点续传功能。二、解题思路获取要下载的资源网址显示网络资源的大小上次读取到的字节位置以及未读取的字节数输入下
- 本文实例形式展示了C#中异步调用的实现方法,并对其原理进行了较为深入的分析,现以教程的方式分享给大家供大家参考之用。具体如下:首先我们来看一
- 1、spring原理内部最核心的就是IOC了,动态注入,让一个对象的创建不用new了,可以自动的生产,这其实就是利用java里的反射,反射其
- 本文实例讲述了C#实现Base64处理的加密解密,编码解码。分享给大家供大家参考,具体如下:using System;using Syste
- 下文笔者讲述maven引入本地jar包时,运行报错"java.lang.NoClassDefFoundError"的处理
- 前言对于Java程序员,可以说对于ArrayList和LinkedList可谓是十分熟悉了对于ArrayList和LinkedList,他们
- 在页面显示的时候,有时候文字无法显示完全,就只能显示部分文字,但是直接截取就只能截取等长字符串,英文和中文很难处理所以就写了下面方法,截取等
- 屏幕切换指的是在同一个Activity内屏幕间的切换,ViewFlipper继承了Framelayout类,ViewAnimator类的作用
- 如下所示:class B {public B() { super(); System.out.println(&qu
- 封装:就是把一些属性和方法封装到一个类里。 继承:就如子类继承父类的一些属性和方法。 多态:就如一个父类有多个不同特色的子类。 这里我就不多
- 相信大家都有这样的一个需求,选择相应开始时间和结束时间,对数据进行筛选,下面就将使用TimePickerView实现这么一个功能。一、先导入
- 简介在 io 包中,提供了两个与平台无关的数据操作流:数据输出流(DataOutputStream)、数据输入流 (DataInputStr
- Logback简介1、logback和log4j是同一个作者,logback可以看作是log4j的升级版2、logback分为三个模块, l
- 本文实例讲述了Java获取UTC时间的方法。分享给大家供大家参考,具体如下:取得本地时间:java.util.Calendar cal =