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. Проверка регистрационных данных
Далее — давайте посмотрим на проверки, которые будет выполнять контроллер при регистрации новой учетной записи:
- Все обязательные поля заполнены (нет пустых или пустых полей)
- Адрес электронной почты действителен (правильный формат)
- Поле подтверждения пароля совпадает с полем пароля
- Аккаунт еще не существует
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);
}
На что следует обратить внимание в приведенном выше коде:
- Контроллер возвращает объект
ModelAndView
, который является удобным классом для отправки данных модели (user
), привязанных к представлению. - Контроллер перенаправит на регистрационную форму, если во время проверки будут установлены какие-либо ошибки.
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 .