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

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

1. обзор

Эта статья продолжаетongoing Registration with Spring Security series с одной из недостающих частей процесса регистрации -verifying the user’s email to confirm their account.

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

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

2. Токен подтверждения

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

2.1. СущностьVerificationToken

СущностьVerificationToken должна соответствовать следующим критериям:

  1. Он должен связываться сUser (через однонаправленное отношение)

  2. Будет создан сразу после регистрации

  3. После создания будетexpire within 24 hours

  4. Имеет значениеunique, randomly generated

Требования 2 и 3 являются частью логики регистрации. Два других реализованы в простой сущностиVerificationToken, такой как в примере 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 = false у пользователя, чтобы гарантировать целостность и согласованность данных в ассоциацииVerificationToken< → _User_.

2.2. Добавьте полеenabled вUser

Первоначально, когдаUser зарегистрирован, это полеenabled будет установлено вfalse. В процессе проверки учетной записи - в случае успеха - она ​​станетtrue.

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

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

    public User() {
        super();
        this.enabled=false;
    }
    ...
}

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

3. Во время регистрации аккаунта

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

  1. СоздайтеVerificationToken для пользователя и сохраните его

  2. Отправьте электронное сообщение для подтверждения учетной записи, которое включает ссылку для подтверждения со значениемVerificationToken’s

3.1. Использование весеннего события для создания токена и отправки письма с подтверждением

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

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

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

Пример 3.1.

@Autowired
ApplicationEventPublisher eventPublisher

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto,
  BindingResult result,
  WebRequest request,
  Errors errors) {

    if (result.hasErrors()) {
        return new ModelAndView("registration", "user", accountDto);
    }

    User registered = createUserAccount(accountDto);
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    try {
        String appUrl = request.getContextPath();
        eventPublisher.publishEvent(new OnRegistrationCompleteEvent
          (registered, request.getLocale(), appUrl));
    } catch (Exception me) {
        return new ModelAndView("emailError", "user", accountDto);
    }
    return new ModelAndView("successRegister", "user", accountDto);
}

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

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

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

Example 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
}

Example 3.2.2. -The 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 + " rn" + "http://localhost:8080" + confirmationUrl);
        mailSender.send(email);
    }
}

Здесь методconfirmRegistration получитOnRegistrationCompleteEvent, извлечет из него всю необходимую информациюUser, создаст токен проверки, сохранит его, а затем отправит его как параметр в «Confirm Registration ”ссылка.

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

3.3. Обработка параметра токена подтверждения

Когда пользователь получает ссылку «Confirm Registration», он должен щелкнуть по ней.

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

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

Пример 3.3.1. -RegistrationController Обработка подтверждения регистрации

@Autowired
private IUserService service;

@RequestMapping(value = "/regitrationConfirm", method = RequestMethod.GET)
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. VerificationToken не существует по какой-либо причине или

  2. VerificationToken истек

See Example 3.3.2., чтобы увидеть страницу с ошибкой.

Пример 3.3.2. -badUser.html



    

signup

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

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

  1. We can use a Cron Job для проверки истечения срока действия токена в фоновом режиме

  2. Мы можемgive the user the opportunity to get a new token по истечении срока его действия

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

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. - Отображать сообщения об ошибках вlogin.html:

error

5. Адаптация слоя постоянства

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

Мы покроем:

  1. НовыйVerificationTokenRepository

  2. Необходимы новые методы вIUserInterface и их реализация для новых операций CRUD

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

Example 5.1. -VerificationTokenRepository

public interface VerificationTokenRepository
  extends JpaRepository {

    VerificationToken findByToken(String token);

    VerificationToken findByUser(User user);
}

Example 5.2. - ИнтерфейсIUserService

public interface IUserService {

    User registerNewUserAccount(UserDto accountDto)
      throws EmailExistsException;

    User getUser(String verificationToken);

    void saveRegisteredUser(User user);

    void createVerificationToken(User user, String token);

    VerificationToken getVerificationToken(String VerificationToken);
}

Example 5.3.UserService

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

    @Autowired
    private VerificationTokenRepository tokenRepository;

    @Override
    public User registerNewUserAccount(UserDto accountDto)
      throws EmailExistsException {

        if (emailExist(accountDto.getEmail())) {
            throw new EmailExistsException(
              "There is an account with that email adress: "
              + accountDto.getEmail());
        }

        User user = new User();
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRole(new Role(Integer.valueOf(1), user));
        return repository.save(user);
    }

    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }

    @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. Заключение

В этой статье мы расширили процесс регистрации, включив в негоan email based account activation procedure.

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

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