The Registration Process With Spring Security – 春天安全的注册过程

最后修改: 2014年 9月 27日

1. Overview

1.概述

In this tutorial, we’ll implement a basic registration process with Spring Security. We’ll be building on top of the concepts we explored in the previous article, where we looked at login.

在本教程中,我们将使用 Spring Security 实现一个基本的注册流程。我们将在前一篇文章中所探讨的概念的基础上展开,在该文章中我们探讨了登录。

The goal here is to add a full registration process that allows a user to sign up, as well as validates and persists user data.

这里的目标是添加一个完整的注册过程,允许用户注册,以及验证和持久化用户数据。

2. The Registration Page

2.注册页面

First, we’ll implement a simple registration page displaying the following fields:

首先,我们将实现一个简单的注册页面,显示以下字段

  • name (first and last name)
  • email
  • password (and password confirmation field)

The following example shows a simple registration.html page:

下面的例子显示了一个简单的registration.html页面。

Example 2.1.

示例2.1.

<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
    <div>
        <label th:text="#{label.user.firstName}">first</label>
        <input th:field="*{firstName}"/>
        <p th:each="error: ${#fields.errors('firstName')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.lastName}">last</label>
        <input th:field="*{lastName}"/>
        <p th:each="error : ${#fields.errors('lastName')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.email}">email</label>
        <input type="email" th:field="*{email}"/>
        <p th:each="error : ${#fields.errors('email')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.password}">password</label>
        <input type="password" th:field="*{password}"/>
        <p th:each="error : ${#fields.errors('password')}" 
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.confirmPass}">confirm</label>
        <input type="password" th:field="*{matchingPassword}"/>
    </div>
    <button type="submit" th:text="#{label.form.submit}">submit</button>
</form>

<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>

3. The User DTO Object

3. 用户DTO对象

We need a Data Transfer Object to send all of the registration information to our Spring backend. The DTO object should have all the information we’ll require later on when we create and populate our User object:

我们需要一个数据传输对象来发送所有的注册信息到我们的Spring后端。这个DTO对象应该拥有我们以后在创建和填充User对象时需要的所有信息。

public class UserDto {
    @NotNull
    @NotEmpty
    private String firstName;
    
    @NotNull
    @NotEmpty
    private String lastName;
    
    @NotNull
    @NotEmpty
    private String password;
    private String matchingPassword;
    
    @NotNull
    @NotEmpty
    private String email;
    
    // standard getters and setters
}

Note that we used standard javax.validation annotations on the fields of the DTO object. Later on, we’ll also implement our own custom validation annotations to validate the format of the email address, as well as the password confirmation (see Section 5).

注意,我们在DTO对象的字段上使用了标准的javax.validation注解。稍后,我们还将实现我们自己的自定义验证注解,以验证电子邮件地址的格式,以及密码确认(见第5节)。

4. The Registration Controller

4. 登记控制器

A Sign-Up link on the login page will take the user to the registration page. The back end for that page lives in the registration controller and is mapped to “/user/registration”:

login页面上的Sign-Up链接将把用户带到注册页面。该页面的后端位于注册控制器中,被映射到“/user/registration”

Example 4.1. The showRegistration Method

示例4.1.showRegistration方法

@GetMapping("/user/registration")
public String showRegistrationForm(WebRequest request, Model model) {
    UserDto userDto = new UserDto();
    model.addAttribute("user", userDto);
    return "registration";
}

When the controller receives the request “/user/registration,” it creates the new UserDto object, which backs the registration form, binds it, and returns.

当控制器收到请求“/user/registration”,它创建新的UserDto对象,它支持registration表单,绑定它,并返回。

5. Validating Registration Data

5.验证注册数据

Next, we’ll look at the validations that the controller will perform when registering a new account:

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

  1. All required fields are filled (No empty or null fields).
  2. The email address is valid (well-formed).
  3. The password confirmation field matches the password field.
  4. The account doesn’t already exist.

5.1. The Built-In Validation

5.1.内置的验证

For the simple checks, we’ll use the out of the box bean validation annotations on the DTO object. These are annotations like @NotNull, @NotEmpty, etc.

对于简单的检查,我们将使用DTO对象上的开箱即用的bean验证注解。这些注解包括@NotNull, @NotEmpty等。

Then, to trigger the validation process, we’ll simply annotate the object in the controller layer with the @Valid annotation:

然后,为了触发验证过程,我们将简单地在控制器层中用@Valid注解来注释该对象。

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

5.2. Custom Validation to Check Email Validity

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

Then we’ll validate the email address and make sure it’s well-formed. For this, we’ll build a custom validator, as well as a custom validation annotation; we’ll call it @ValidEmail.

然后我们将验证电子邮件地址,并确保其格式良好。为此,我们将建立一个自定义验证器,以及一个自定义验证注释;我们将称之为@ValidEmail

It’s important to note that we’re rolling our own custom annotation instead of Hibernate’s @Email because Hibernate considers the old intranet addresses format, myaddress@myserver, as valid (see Stackoverflow article), which isn’t good.

需要注意的是,我们推出了自己的自定义注解,而不是Hibernate的@Email,因为Hibernate认为旧的内网地址格式myaddress@myserver是有效的(见Stackoverflow文章),这并不好。

So here’s the email validation annotation and the custom validator:

所以这里是电子邮件验证注释和自定义验证器。

Example 5.2.1. The Custom Annotation for Email Validation

示例 5.2.1.用于电子邮件验证的自定义注释

@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
    String message() default "Invalid email";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Note that we defined the annotation at the FIELD level, since that’s where it applies conceptually.

请注意,我们在FIELD层定义了注解,因为那是它在概念上的应用。

Example 5.2.2. The Custom EmailValidator:

示例5.2.2.自定义EmailValidator:

public class EmailValidator 
  implements ConstraintValidator<ValidEmail, String> {
    
    private Pattern pattern;
    private Matcher matcher;
    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,})$"; 
    @Override
    public void initialize(ValidEmail constraintAnnotation) {
    }
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context){
        return (validateEmail(email));
    } 
    private boolean validateEmail(String email) {
        pattern = Pattern.compile(EMAIL_PATTERN);
        matcher = pattern.matcher(email);
        return matcher.matches();
    }
}

Then we’ll use the new annotation on our UserDto implementation:

然后我们将在我们的UserDto实现中使用新注解

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Using Custom Validation for Password Confirmation

5.3.使用自定义验证进行密码确认

We also need a custom annotation and validator to make sure that the password and matchingPassword fields match up:

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

Example 5.3.1. The Custom Annotation for Validating Password Confirmation

示例5.3.1.用于验证密码确认的自定义注释

@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
    String message() default "Passwords don't match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

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.

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

The custom validator that will be called by this annotation is shown below:

将由该注解调用的自定义验证器如下所示。

Example 5.3.2. The PasswordMatchesValidator Custom Validator

示例5.3.2.PasswordMatchesValidator自定义验证器

public class PasswordMatchesValidator
  implements ConstraintValidator<PasswordMatches, Object> {
    
    @Override
    public void initialize(PasswordMatches constraintAnnotation) {
    }
    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context){
        UserDto user = (UserDto) obj;
        return user.getPassword().equals(user.getMatchingPassword());
    }
}

Then the @PasswordMatches annotation should be applied to our UserDto object:

然后,@PasswordMatches注解应该被应用到我们的UserDto对象。

@PasswordMatches
public class UserDto {
    //...
}

All custom validations are, of course, evaluated along with all standard annotations when the entire validation process runs.

当然,在整个验证过程运行时,所有的自定义验证都会和所有的标准注释一起被评估。

5.4. Check That the Account Doesn’t Already Exist

5.4.检查该账户是否已经存在

The fourth check we’ll implement is verifying that the email account doesn’t already exist in the database.

我们要实现的第四个检查是验证email账户在数据库中是否已经存在。

This is performed after the form has been validated, and it’s done with the help of the UserService implementation.

这是在表单被验证后进行的,它是在UserService 实现的帮助下完成的。

Example 5.4.1. The Controller’s registerUserAccount Method Calls the UserService Object

示例 5.4.1.控制器的registerUserAccount方法调用UserService对象

@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto userDto,
  HttpServletRequest request,
  Errors errors) {
    
    try {
        User registered = userService.registerNewUserAccount(userDto);
    } catch (UserAlreadyExistException uaeEx) {
        mav.addObject("message", "An account for that username/email already exists.");
        return mav;
    }

    // rest of the implementation
}

Example 5.4.2. UserService Checks for Duplicate Emails

示例 5.4.2.用户服务检查重复的电子邮件

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
    
    @Override
    public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
        if (emailExists(userDto.getEmail())) {
            throw new UserAlreadyExistException("There is an account with that email address: "
              + userDto.getEmail());
        }

        // the rest of the registration operation
    }
    private boolean emailExists(String email) {
        return userRepository.findByEmail(email) != null;
    }
}

The UserService relies on the UserRepository class to check if a user with a given email address already exists in the database.

UserService依靠UserRepository类来检查数据库中是否已经存在一个具有给定电子邮件地址的用户。

The actual implementation of the UserRepository in the persistence layer isn’t relevant for the current article; however, one quick way is to use Spring Data to generate the repository layer.

持久层中UserRepository的实际实现与本文无关;但是,一种快速的方法是使用Spring Data来生成资源库层

6. Persisting Data and Finishing-Up Form Processing

6.坚持数据和完成表格处理

Next, we’ll implement the registration logic in our controller layer:

接下来,我们将在控制器层实现注册逻辑。

Example 6.1. The RegisterAccount Method in the Controller

示例 6.1.控制器中的RegisterAccount方法

@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto userDto,
  HttpServletRequest request,
  Errors errors) {
    
    try {
        User registered = userService.registerNewUserAccount(userDto);
    } catch (UserAlreadyExistException uaeEx) {
        mav.addObject("message", "An account for that username/email already exists.");
        return mav;
    }

    return new ModelAndView("successRegister", "user", userDto);
}

Things to notice in the code above:

上面的代码中需要注意的事项。

  1. The controller is returning a ModelAndView object, which is the convenient class for sending model data (user) tied to the view.
  2. The controller will redirect to the registration form if there are any errors set at validation time.

7. The UserService – Register Operation

7. The UserService – Register Operation

Finally, we’ll finish the implementation of the registration operation in the UserService:

最后,我们将完成UserService中注册操作的实现。

Example 7.1. The IUserService Interface

示例 7.1.IUserService 接口

public interface IUserService {
    User registerNewUserAccount(UserDto userDto);
}

Example 7.2. The UserService Class

示例7.2。UserService

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
    
    @Override
    public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
        if (emailExists(userDto.getEmail())) {
            throw new UserAlreadyExistException("There is an account with that email address: "
              + userDto.getEmail());
        }

        User user = new User();
        user.setFirstName(userDto.getFirstName());
        user.setLastName(userDto.getLastName());
        user.setPassword(userDto.getPassword());
        user.setEmail(userDto.getEmail());
        user.setRoles(Arrays.asList("ROLE_USER"));

        return repository.save(user);
    }

    private boolean emailExists(String email) {
        return userRepository.findByEmail(email) != null;
    }
}

8. Loading User Details for Security Login

8.为安全登录加载用户详细信息

In our previous article, the login used hard-coded credentials. We’ll change that by using the newly registered user information and credentials. Furthermore, we’ll implement a custom UserDetailsService to check the credentials for login from the persistence layer.

在我们的前一篇文章中,登录使用了硬编码的凭据。我们将通过使用新注册的用户信息和凭证来改变这种情况。此外,我们将实现一个自定义的UserDetailsService,以便从持久层检查用于登录的凭据。

8.1. The Custom UserDetailsService

8.1.自定义 UserDetailsService

We’ll start with the custom user details service implementation:

我们将从自定义用户细节服务的实现开始。

@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
    
    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;
    }
}

8.2. Enable the New Authentication Provider

8.2.启用新的认证提供者

To enable the new user service in the Spring Security configuration, we’ll simply need to add a reference to the UserDetailsService inside the authentication-manager element, and add the UserDetailsService bean:

为了在Spring Security配置中启用新的用户服务,我们只需要在 authentication-manager元素中添加对UserDetailsService的引用,并添加UserDetailsService bean。

Example 8.2. The Authentication Manager and the UserDetailsService

示例 8.2.认证管理器和UserDetailsService

<authentication-manager>
    <authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
 
<beans:bean id="userDetailsService" class="com.baeldung.security.MyUserDetailsService" />

Another option is via Java configuration:

另一个选择是通过Java配置。

@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

9. Conclusion

9.结论

In this article, we demonstrated a complete and almost production-ready registration process implemented with Spring Security and Spring MVC. Next, we’ll discuss the process of activating the newly registered account by verifying the email of the new user.

在这篇文章中,我们展示了一个完整的、几乎是可生产的注册流程,该流程由Spring Security和Spring MVC实现。接下来,我们将讨论通过验证新用户的电子邮件来激活新注册账户的过程。

The implementation of this Spring Security REST article can be found over on GitHub.

该Spring Security REST文章的实现可以在GitHub上找到over on GitHub.

Next »

Registration – Activate a New Account by Email

« Previous

Spring Security Registration Tutorial