1. 概要

この記事では、継続的なSpring Securityシリーズへの登録を継続し、登録プロセスの欠落している部分の1つであるユーザーの電子メールを確認してアカウントを確認します

登録確認メカニズムにより、ユーザーは、登録が成功した後に送信される「登録の確認」電子メールに応答して、電子メールアドレスを確認し、アカウントをアクティブ化する必要があります。 ユーザーは、電子メールで送信された一意のアクティベーションリンクをクリックしてこれを行います。

このロジックに従うと、新しく登録されたユーザーは、このプロセスが完了するまでシステムにログインできなくなります。

2. 検証トークン

ユーザーを検証するためのキーアーティファクトとして、単純な検証トークンを使用します。

2.1. VerificationTokenエンティティ

VerificationToken エンティティは、次の基準を満たしている必要があります。

  1. ユーザーにリンクバックする必要があります(一方向の関係を介して)
  2. 登録後すぐに作成されます
  3. 作成後24時間以内に有効期限が切れます
  4. 一意のランダムに生成された値があります

要件2と3は、登録ロジックの一部です。 他の2つは、例2.1のような単純なVerificationTokenエンティティに実装されています。

例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 < -> ユーザー協会。

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. アカウント登録中

ユーザー登録のユースケースに2つのビジネスロジックを追加してみましょう。

  1. ユーザーのVerificationTokenを生成し、それを永続化します
  2. アカウント確認用の電子メールメッセージを送信します。これには、VerificationTokenの値を含む確認リンクが含まれています

3.1. Springイベントを使用してトークンを作成し、確認メールを送信する

これらの2つの追加ロジックは、「付随的な」バックエンドタスクであるため、コントローラーが直接実行しないでください。

コントローラは、Spring ApplicationEvent を公開して、これらのタスクの実行をトリガーします。 これは、 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);
}

もう1つ注意すべき点は、イベントの公開を取り巻く trycatchブロックです。 このコードは、イベントの公開後に実行されるロジック(この場合は電子メールの送信)に例外がある場合は常にエラーページを表示します。

3.2. イベントとリスナー

ここで、コントローラーが送信するこの新しい OnRegistrationCompleteEvent の実際の実装と、それを処理するリスナーを見てみましょう。

例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<OnRegistrationCompleteEvent> {
 
    @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?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);
    }
}

ここで、 confirmRegistration メソッドは、 OnRegistrationCompleteEvent を受け取り、そこから必要なすべての User 情報を抽出し、検証トークンを作成して永続化し、次のように送信します。 「登録の確認」リンクのパラメーター。

前述のように、JavaMailSenderによってスローされたjavax.mail.AuthenticationFailedExceptionはすべてコントローラーによって処理されます。

3.3. 検証トークンパラメータの処理

ユーザーが「登録の確認」リンクを受け取ったら、それをクリックする必要があります。

実行すると、コントローラーは結果のGETリクエストでトークンパラメーターの値を抽出し、それを使用してUserを有効にします。

例3.3.1でこのプロセスを見てみましょう。

例3.3.1。 – RegistrationController登録確認の処理

@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. VerificationToken が何らかの理由で存在しない、または
  2. VerificationTokenの有効期限が切れています

例3.3.2を参照してください。 エラーページを表示します。

例3.3.2。 – badUser.html

<html>
<body>
    <h1 th:text="${param.message[0]}>Error Message</h1>
    <a th:href="@{/registration.html}" 
      th:text="#{label.form.loginSignUp}">signup</a>
</body>
</html>

エラーが見つからない場合、ユーザーは有効になっています。

VerificationToken のチェックと有効期限のシナリオの処理を改善するには、次の2つの機会があります。

  1. Cron Job を使用して、バックグラウンドでトークンの有効期限を確認できます
  2. 有効期限が切れたら、ユーザーに新しいトークンを取得する機会を与えることができます

今後の記事のために新しいトークンの生成を延期し、ユーザーがここでトークンを実際に正常に検証したと想定します。

4. ログインプロセスへのアカウントアクティベーションチェックの追加

ユーザーが有効になっているかどうかを確認するコードを追加する必要があります。

これを例4.1で見てみましょう。 これは、MyUserDetailsServiceloadUserByUsernameメソッドを示しています。

例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にエラーメッセージを表示します。

<div th:if="${param.error != null}" 
  th:text="${session[SPRING_SECURITY_LAST_EXCEPTION]}">error</div>

5. 永続性レイヤーの適応

ここで、検証トークンとユーザーを含むこれらの操作のいくつかの実際の実装を提供しましょう。

カバーします:

  1. 新しいVerificationTokenRepository
  2. IUserInterfaceの新しいメソッドと必要な新しいCRUD操作の実装

例5.1–5.3。 新しいインターフェースと実装を示します。

例5.1。 VerificationTokenRepository

public interface VerificationTokenRepository 
  extends JpaRepository<VerificationToken, Long> {

    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。UserService

@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. 結論

この記事では、登録プロセスを拡張して、電子メールベースのアカウントアクティベーション手順を含めました。

アカウントアクティベーションロジックでは、ユーザーが本人確認を行うために確認トークンをコントローラーに返送できるように、確認トークンを電子メールでユーザーに送信する必要があります。

このRegistrationwithSpring Securityチュートリアルの実装は、 GitHubプロジェクトにあります。これはEclipseベースのプロジェクトであるため、そのままインポートして実行するのは簡単です。