Перейти к основному содержимому

Процесс регистрации в Spring Security

· 8 мин. чтения

1. Обзор

В этой статье мы реализуем базовый процесс регистрации с помощью Spring Security. Это основано на концепциях, рассмотренных в предыдущей статье , где мы рассмотрели вход в систему.

Цель состоит в том, чтобы добавить полный процесс регистрации, который позволяет пользователю регистрироваться, проверять и сохранять пользовательские данные.

2. Страница регистрации

Во-первых, давайте реализуем простую страницу регистрации, отображающую следующие поля :

  • имя (имя и фамилия)
  • Эл. адрес
  • пароль (и поле подтверждения пароля)

В следующем примере показана простая страница Registration.html :

Пример 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. Пользовательский объект DTO

Нам нужен объект передачи данных , чтобы отправить всю регистрационную информацию на наш сервер 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
}

Обратите внимание, что мы использовали стандартные аннотации javax.validation для полей объекта DTO. Позже мы также внедрим наши собственные пользовательские аннотации проверки для проверки формата адреса электронной почты, а также для подтверждения пароля. (см. раздел 5)

4. Контролер регистрации

Ссылка для регистрации на странице входа приведет пользователя на страницу регистрации . Этот сервер для этой страницы находится в контроллере регистрации и отображается на «/user/registration» : ``

Пример 4.1. - Метод showRegistration

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

Когда контроллер получает запрос «/user/registration» , он создает новый объект UserDto , который будет поддерживать регистрационную форму, связывает ее и возвращает — довольно просто.

5. Проверка регистрационных данных

Далее — давайте посмотрим на проверки, которые будет выполнять контроллер при регистрации новой учетной записи:

  1. Все обязательные поля заполнены (нет пустых или пустых полей)
  2. Адрес электронной почты действителен (правильный формат)
  3. Поле подтверждения пароля совпадает с полем пароля
  4. Аккаунт еще не существует

5.1. Встроенная проверка

Для простых проверок мы будем использовать стандартные аннотации проверки бина для объекта DTO — такие аннотации, как @NotNull , @NotEmpty и т. д.

Чтобы запустить процесс проверки, мы просто аннотируем объект на уровне контроллера аннотацией @Valid :

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

5.2. Пользовательская проверка для проверки действительности электронной почты

Далее — давайте проверим адрес электронной почты и убедимся, что он правильно сформирован. Мы собираемся создать собственный валидатор для этого, а также пользовательскую аннотацию проверки — назовем это @ValidEmail .

Небольшое примечание: мы используем собственную пользовательскую аннотацию вместо @Email Hibernate, потому что Hibernate считает старый формат адресов интрасети: myaddress@myserver допустимым (см . статью Stackoverflow ), что не очень хорошо.

Вот аннотация проверки электронной почты и пользовательский валидатор:

Пример 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 {};
}

Обратите внимание, что мы определили аннотацию на уровне FIELD , поскольку именно там она применяется концептуально.

Пример 5.2.2. – Пользовательский EmailValidato r:

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

Давайте теперь используем новую аннотацию в нашей реализации UserDto :

@ValidEmail
@NotNull
@NotEmpty
private String email;

5.3. Использование пользовательской проверки для подтверждения пароля

Нам также нужна пользовательская аннотация и валидатор, чтобы убедиться, что поля пароля и соответствия пароля совпадают:

Пример 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 {};
}

Обратите внимание, что аннотация @Target указывает, что это аннотация уровня TYPE . Это потому, что нам нужен весь объект UserDto для выполнения проверки.

Пользовательский валидатор, который будет вызываться этой аннотацией, показан ниже:

Пример 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());
}
}

Теперь аннотацию @PasswordMatches следует применить к нашему объекту UserDto :

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

Все пользовательские проверки, конечно, оцениваются вместе со всеми стандартными аннотациями, когда выполняется весь процесс проверки.

5.4. Убедитесь, что учетная запись еще не существует

Четвертая проверка, которую мы реализуем, — это проверка того, что учетная запись электронной почты еще не существует в базе данных.

Это выполняется после проверки формы с помощью реализации UserService .

Пример 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
}

Пример 5.4.2. - Служба проверки пользователей на наличие дубликатов электронной почты

@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;

@Override
public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
if (emailExist(userDto.getEmail())) {
throw new UserAlreadyExistException("There is an account with that email address: "
+ userDto.getEmail());
}

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

UserService полагается на класс UserRepository , чтобы проверить, существует ли уже пользователь с данным адресом электронной почты в базе данных.

Теперь фактическая реализация UserRepository на уровне сохраняемости не имеет отношения к текущей статье. Одним из быстрых способов является, конечно же, использование Spring Data для генерации слоя репозитория . ``

6. Сохранение данных и завершающая обработка формы

Наконец — давайте реализуем логику регистрации на нашем уровне контроллера: ``

Пример 6.1.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);
}

На что следует обратить внимание в приведенном выше коде:

  1. Контроллер возвращает объект ModelAndView , который является удобным классом для отправки данных модели ( user ), привязанных к представлению.
  2. Контроллер перенаправит на регистрационную форму, если во время проверки будут установлены какие-либо ошибки.

7. UserService — операция регистрации

Закончим реализацию операции регистрации в UserService :

Пример 7.1. Интерфейс IUserService _

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

Пример 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. Загрузка данных пользователя для безопасного входа

В нашей предыдущей статье для входа в систему использовались жестко заданные учетные данные. Давайте изменим это и будем использовать только что зарегистрированную информацию о пользователе и учетные данные. Мы реализуем пользовательскую службу UserDetailsService для проверки учетных данных для входа в систему с уровня сохраняемости.

8.1. Пользовательская служба сведений о пользователях

Начнем с реализации пользовательского сервиса сведений о пользователе:

@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. Включить нового поставщика аутентификации

Чтобы включить новую пользовательскую службу в конфигурации Spring Security, нам просто нужно добавить ссылку на UserDetailsService внутри элемента authentication-manager и добавить bean- компонент UserDetailsService :

Пример 8.2.- Диспетчер аутентификации и служба UserDetailsService

<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>

<beans:bean id="userDetailsService" class="com.foreach.security.MyUserDetailsService" />

Или через конфигурацию Java:

@Autowired
private MyUserDetailsService userDetailsService;

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

9. Заключение

И мы закончили — полный и почти готовый к работе процесс регистрации, реализованный с помощью Spring Security и Spring MVC. Далее мы собираемся обсудить процесс активации вновь зарегистрированной учетной записи путем проверки электронной почты нового пользователя.

Реализацию этого учебного пособия по Spring Security REST можно найти на GitHub .