Рубрики
Без рубрики

Регистрация – Активируйте новую учетную запись по электронной почте

Проверьте вновь зарегистрированных пользователей, отправив им маркер проверки по электронной почте, прежде чем разрешить им войти в систему – с помощью Spring Security.

Автор оригинала: Eugen Paraschiv.

1. Обзор

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

Механизм подтверждения регистрации заставляет пользователя ответить на электронное письмо ” Подтвердить регистрацию “, отправленное после успешной регистрации, чтобы подтвердить свой адрес электронной почты и активировать свою учетную запись. Пользователь делает это, нажав на уникальную ссылку активации, отправленную ему по электронной почте.

Следуя этой логике, вновь зарегистрированный пользователь не сможет войти в систему до тех пор, пока этот процесс не будет завершен.

2. Токен Проверки

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

2.1. Сущность Токена Проверки

Объект Verification Token должен соответствовать следующим критериям:

  1. Он должен вернуться к Пользователю (через однонаправленное отношение)
  2. Он будет создан сразу после регистрации
  3. Он истечет в течение 24 часов после его создания
  4. Имеет уникальное, случайно сгенерированное значение

Требования 2 и 3 являются частью логики регистрации. Два других реализованы в простом Токене проверки сущности, как в примере 2.1.:

Пример 2.1.

@Entity
public class VerificationToken {
    private static final int EXPIRATION = 60 * 24;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    
    private String token;
  
    @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
    @JoinColumn(nullable = false, name = "user_id")
    private User user;
    
    private Date expiryDate;
   
    private Date calculateExpiryDate(int expiryTimeInMinutes) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new Timestamp(cal.getTime().getTime()));
        cal.add(Calendar.MINUTE, expiryTimeInMinutes);
        return new Date(cal.getTime().getTime());
    }
    
    // standard constructors, getters and setters
}

Обратите внимание на nullable для пользователя, чтобы обеспечить целостность и согласованность данных в маркере проверки < – > User ассоциации.

2.2. Добавьте поле включено в поле Пользователь

Первоначально, когда Пользователь зарегистрирован, это поле включено будет установлено в false . Во время процесса верификации учетной записи – в случае успеха – он станет true .

Давайте начнем с добавления поля в нашу User сущность:

public class User {
    ...
    @Column(name = "enabled")
    private boolean enabled;
    
    public User() {
        super();
        this.enabled=false;
    }
    ...
}

Обратите внимание, что мы также установили значение по умолчанию для этого поля в false .

3. Во Время Регистрации Учетной Записи

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

  1. Создайте Токен проверки для Пользователя и сохраните его
  2. Отправьте сообщение электронной почты для подтверждения учетной записи, которое включает ссылку подтверждения со значением VerificationToken

3.1. Использование события Spring для создания Токена и отправки Проверочного электронного письма

Эти две дополнительные части логики не должны выполняться контроллером напрямую, поскольку они являются “сопутствующими” внутренними задачами.

Контроллер опубликует событие Spring Application для запуска выполнения этих задач. Это так же просто, как ввести ApplicationEventPublisher , а затем использовать его для публикации завершения регистрации.

Пример 3.1. показывает эту простую логику:

Пример 3.1.

@Autowired
ApplicationEventPublisher eventPublisher

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

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

Еще одна вещь, на которую следует обратить внимание, – это блок try catch , окружающий публикацию события. Этот фрагмент кода будет отображать страницу с ошибкой всякий раз, когда в логике, выполняемой после публикации события, возникает исключение, которое в данном случае является отправкой сообщения электронной почты.

3.2. Событие и Слушатель

Теперь давайте посмотрим на фактическую реализацию этого нового события On Registration Complete , которое отправляет ваш контроллер, а также прослушиватель, который будет его обрабатывать:

Пример 3.2.1. –/| OnRegistrationCompleteEvent

public class OnRegistrationCompleteEvent extends ApplicationEvent {
    private String appUrl;
    private Locale locale;
    private User user;

    public OnRegistrationCompleteEvent(
      User user, Locale locale, String appUrl) {
        super(user);
        
        this.user = user;
        this.locale = locale;
        this.appUrl = appUrl;
    }
    
    // standard getters and setters
}

Пример 3.2.2.RegistrationListener Обрабатывает OnRegistrationCompleteEvent

@Component
public class RegistrationListener implements 
  ApplicationListener {
 
    @Autowired
    private IUserService service;
 
    @Autowired
    private MessageSource messages;
 
    @Autowired
    private JavaMailSender mailSender;

    @Override
    public void onApplicationEvent(OnRegistrationCompleteEvent event) {
        this.confirmRegistration(event);
    }

    private void confirmRegistration(OnRegistrationCompleteEvent event) {
        User user = event.getUser();
        String token = UUID.randomUUID().toString();
        service.createVerificationToken(user, token);
        
        String recipientAddress = user.getEmail();
        String subject = "Registration Confirmation";
        String confirmationUrl 
          = event.getAppUrl() + "/regitrationConfirm.html?token=" + token;
        String message = messages.getMessage("message.regSucc", null, event.getLocale());
        
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(recipientAddress);
        email.setSubject(subject);
        email.setText(message + "\r\n" + "http://localhost:8080" + confirmationUrl);
        mailSender.send(email);
    }
}

Здесь метод confirm Registration получит событие On Registration Complete , извлечет из него всю необходимую Пользовательскую информацию, создаст токен проверки, сохранит его, а затем отправит его в качестве параметра по ссылке ” Confirm Registration “.

Как уже упоминалось выше, любой javax.mail.Исключение AuthenticationFailedException , вызванное JavaMailSender , будет обработано контроллером.

3.3. Обработка параметра Токена проверки

Когда пользователь получает ссылку ” Подтвердить регистрацию “, он должен нажать на нее.

Как только они это сделают – контроллер извлекет значение параметра токена в результирующем запросе GET и будет использовать его для включения Пользователя .

Давайте рассмотрим этот процесс в примере 3.3.1.:

Пример 3.3.1. – Регистрационный контроллер Обработка подтверждения регистрации

@Autowired
private IUserService service;

@GetMapping("/regitrationConfirm")
public String confirmRegistration
  (WebRequest request, Model model, @RequestParam("token") String token) {
 
    Locale locale = request.getLocale();
    
    VerificationToken verificationToken = service.getVerificationToken(token);
    if (verificationToken == null) {
        String message = messages.getMessage("auth.message.invalidToken", null, locale);
        model.addAttribute("message", message);
        return "redirect:/badUser.html?lang=" + locale.getLanguage();
    }
    
    User user = verificationToken.getUser();
    Calendar cal = Calendar.getInstance();
    if ((verificationToken.getExpiryDate().getTime() - cal.getTime().getTime()) <= 0) {
        String messageValue = messages.getMessage("auth.message.expired", null, locale)
        model.addAttribute("message", messageValue);
        return "redirect:/badUser.html?lang=" + locale.getLanguage();
    } 
    
    user.setEnabled(true); 
    service.saveRegisteredUser(user); 
    return "redirect:/login.html?lang=" + request.getLocale().getLanguage(); 
}

Пользователь будет перенаправлен на страницу ошибки с соответствующим сообщением, если:

  1. Токен Проверки не существует по какой-либо причине или
  2. Срок действия токена Проверки истек

См. Пример 3.3.2. , чтобы увидеть страницу ошибки.

Пример 3.3.2. – Пример 3.3.2. –



    

signup

Если ошибок не обнаружено, пользователь включен.

Есть две возможности для улучшения обработки Токена проверки сценариев проверки и истечения срока действия:

  1. Мы можем использовать задание Cron для проверки истечения срока действия токена в фоновом режиме
  2. Мы можем дать пользователю возможность получить новый токен по истечении срока его действия

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

4. Добавление проверки активации учетной записи в процесс входа в систему

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

Давайте рассмотрим это в примере 4.1. который показывает loadUserByUsername метод MyUserDetailsService .

Пример 4.1.

@Autowired
UserRepository userRepository;

public UserDetails loadUserByUsername(String email) 
  throws UsernameNotFoundException {
 
    boolean enabled = true;
    boolean accountNonExpired = true;
    boolean credentialsNonExpired = true;
    boolean accountNonLocked = true;
    try {
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException(
              "No user found with username: " + email);
        }
        
        return new org.springframework.security.core.userdetails.User(
          user.getEmail(), 
          user.getPassword().toLowerCase(), 
          user.isEnabled(), 
          accountNonExpired, 
          credentialsNonExpired, 
          accountNonLocked, 
          getAuthorities(user.getRole()));
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

Как мы видим, теперь MyUserDetailsService не использует флаг enabled пользователя – и поэтому он будет разрешать аутентификацию только включенному пользователю.

Теперь мы добавим AuthenticationFailureHandler для настройки сообщений об исключениях, поступающих из MyUserDetailsService . Наш CustomAuthenticationFailureHandler показан в примере 4.2. :

Пример 4.2. – CustomAuthenticationFailureHandler :

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Autowired
    private LocaleResolver localeResolver;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
      HttpServletResponse response, AuthenticationException exception)
      throws IOException, ServletException {
        setDefaultFailureUrl("/login.html?error=true");

        super.onAuthenticationFailure(request, response, exception);

        Locale locale = localeResolver.resolveLocale(request);

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);

        if (exception.getMessage().equalsIgnoreCase("User is disabled")) {
            errorMessage = messages.getMessage("auth.message.disabled", null, locale);
        } else if (exception.getMessage().equalsIgnoreCase("User account has expired")) {
            errorMessage = messages.getMessage("auth.message.expired", null, locale);
        }

        request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, errorMessage);
    }
}

Нам нужно будет изменить login.html для отображения сообщений об ошибках.

Пример 4.3. – Отображение сообщений об ошибках на Пример 4.3. – Отображение сообщений об ошибках на :

error

5. Адаптация уровня персистентности

Теперь давайте представим фактическую реализацию некоторых из этих операций, связанных с токеном проверки, а также с пользователями.

Мы прикроем:

  1. Новый репозиторий токенов проверки
  2. Новые методы в пользовательском интерфейсе и его реализация для новых операций CRUD необходимы

Примеры 5.1 – 5.3. показать новые интерфейсы и реализацию:

Пример 5.1.VerificationTokenRepository

public interface VerificationTokenRepository 
  extends JpaRepository {

    VerificationToken findByToken(String token);

    VerificationToken findByUser(User user);
}

Пример 5.2. – Интерфейс IUserService

public interface IUserService {
    
    User registerNewUserAccount(UserDto userDto) 
      throws UserAlreadyExistException;

    User getUser(String verificationToken);

    void saveRegisteredUser(User user);

    void createVerificationToken(User user, String token);

    VerificationToken getVerificationToken(String VerificationToken);
}

Пример 5.3. Пользовательская служба

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

    @Autowired
    private VerificationTokenRepository tokenRepository;

    @Override
    public User registerNewUserAccount(UserDto userDto) 
      throws UserAlreadyExistException {
        
        if (emailExist(userDto.getEmail())) {
            throw new UserAlreadyExistException(
              "There is an account with that email adress: " 
              + userDto.getEmail());
        }
        
        User user = new User();
        user.setFirstName(userDto.getFirstName());
        user.setLastName(userDto.getLastName());
        user.setPassword(userDto.getPassword());
        user.setEmail(userDto.getEmail());
        user.setRole(new Role(Integer.valueOf(1), user));
        return repository.save(user);
    }

    private boolean emailExist(String email) {
        return userRepository.findByEmail(email) != null;
    }
    
    @Override
    public User getUser(String verificationToken) {
        User user = tokenRepository.findByToken(verificationToken).getUser();
        return user;
    }
    
    @Override
    public VerificationToken getVerificationToken(String VerificationToken) {
        return tokenRepository.findByToken(VerificationToken);
    }
    
    @Override
    public void saveRegisteredUser(User user) {
        repository.save(user);
    }
    
    @Override
    public void createVerificationToken(User user, String token) {
        VerificationToken myToken = new VerificationToken(token, user);
        tokenRepository.save(myToken);
    }
}

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

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

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

Реализацию этой регистрации с помощью руководства по безопасности Spring можно найти в проекте GitHub – это проект на основе Eclipse, поэтому его должно быть легко импортировать и запускать как есть.