Pendaftaran - Aktifkan Akaun Baru melalui E-mel

Artikel ini adalah sebahagian daripada siri: • Tutorial Pendaftaran Keselamatan Musim Semi

• Proses Pendaftaran Dengan Keselamatan Musim Semi

• Pendaftaran - Aktifkan Akaun Baru melalui E-mel (artikel semasa) • Pendaftaran Keselamatan Musim Semi - Kirim semula E-mel Pengesahan

• Pendaftaran dengan Spring Security - Pengekodan Kata Laluan

• API Pendaftaran menjadi RESTful

• Keselamatan Musim Semi - Tetapkan Semula Kata Laluan Anda

• Pendaftaran - Kekuatan dan Peraturan Kata Laluan

• Mengemas kini Kata Laluan anda

1. Gambaran keseluruhan

Artikel ini terus berterusan Pendaftaran dengan Spring Keselamatan siri dengan salah satu bahagian yang hilang daripada proses pendaftaran - mengesahkan e-mel pengguna untuk mengesahkan akaun mereka .

Mekanisme pengesahan pendaftaran memaksa pengguna untuk menjawab e-mel " Konfirmasi Pendaftaran " yang dihantar setelah berjaya mendaftar untuk mengesahkan alamat e-mel dan mengaktifkan akaun mereka. Pengguna melakukan ini dengan mengklik pautan pengaktifan unik yang dihantar kepada mereka melalui e-mel.

Mengikut logik ini, pengguna yang baru mendaftar tidak akan dapat masuk ke sistem sehingga proses ini selesai.

2. Token Pengesahan

Kami akan menggunakan token pengesahan mudah sebagai artifak utama yang digunakan oleh pengguna untuk mengesahkan.

2.1. The VerificationToken Entity

The VerificationToken entiti mesti memenuhi kriteria berikut:

  1. Ia mesti menghubungkan kembali ke Pengguna (melalui hubungan unidirectional)
  2. Ia akan dibuat tepat selepas pendaftaran
  3. Ia akan tamat dalam masa 24 jam selepas penciptaannya
  4. Mempunyai unik, secara rawak nilai

Keperluan 2 dan 3 adalah sebahagian daripada logik pendaftaran. Dua yang lain dilaksanakan dalam entiti VerifikasiToken yang sederhana seperti yang terdapat dalam Contoh 2.1:

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

Perhatikan nullable = false pada Pengguna untuk memastikan integriti data dan konsistensi dalam VerifikasiToken < -> kaitan Pengguna .

2.2. Tambahkan Medan yang diaktifkan ke Pengguna

Pada mulanya, apabila Pengguna didaftarkan, medan yang diaktifkan ini akan ditetapkan ke false . Semasa proses pengesahan akaun - jika berjaya - itu akan menjadi kenyataan .

Mari kita mulakan dengan menambahkan bidang ke entiti Pengguna kita :

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

Perhatikan bagaimana kami juga menetapkan nilai lalai bidang ini menjadi salah .

3. Semasa Pendaftaran Akaun

Mari tambahkan dua logik perniagaan tambahan ke kes penggunaan pendaftaran pengguna:

  1. Hasilkan Tanda Verifikasi untuk Pengguna dan teruskan
  2. Menghantar mesej e-mel pengesahan akaun - yang termasuk link pengesahan dengan VerificationToken ini nilai

3.1. Menggunakan Acara Musim Semi untuk Membuat Token dan Menghantar E-mel Pengesahan

Kedua-dua logik tambahan ini tidak boleh dilakukan secara langsung oleh pengawal kerana ia adalah "jaminan" tugas akhir.

Pengawal akan menerbitkan Spring ApplicationEvent untuk mencetuskan pelaksanaan tugas-tugas ini. Ini semudah menyuntikkan ApplicationEventPublisher dan kemudian menggunakannya untuk menerbitkan pendaftaran selesai.

Contoh 3.1. menunjukkan logik mudah ini:

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

Satu perkara tambahan yang perlu diperhatikan adalah cubaan menangkap sekitar penerbitan acara tersebut. Bahagian kod ini akan memaparkan halaman ralat setiap kali terdapat pengecualian dalam logik yang dilaksanakan setelah penerbitan acara, yang dalam hal ini adalah pengiriman e-mel.

3.2. Acara dan Pendengar

Sekarang mari kita lihat pelaksanaan sebenar OnRegistrationCompleteEvent baru yang dihantar oleh pengawal kami, dan juga pendengar yang akan mengatasinya:

Contoh 3.2.1. - The 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 }

Contoh 3.2.2. - PendaftaranListener Mengendalikan 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" + "//localhost:8080" + confirmationUrl); mailSender.send(email); } }

Di sini, confirmRegistration kaedah akan menerima OnRegistrationCompleteEvent , mengeluarkan semua yang diperlukan pengguna maklumat daripada itu, buat pengesahan token, berterusan, dan kemudian menghantarnya sebagai parameter dalam " Pendaftaran Sahkan link".

Seperti yang telah disebutkan di atas, setiap javax.mail.AuthenticationFailedException yang dilemparkan oleh JavaMailSender akan dikendalikan oleh pengawal.

3.3. Memproses Parameter Token Pengesahan

Apabila pengguna menerima pautan " Sahkan Pendaftaran " mereka harus mengkliknya.

Setelah mereka melakukannya - pengawal akan mengekstrak nilai parameter token dalam permintaan GET yang dihasilkan dan akan menggunakannya untuk membolehkan Pengguna .

Mari lihat proses ini dalam Contoh 3.3.1:

Contoh 3.3.1. - RegistrationController Memproses Pengesahan Pendaftaran

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

Pengguna akan diarahkan ke halaman ralat dengan mesej yang sesuai jika:

  1. Tanda Pengesahan tidak wujud, atas sebab tertentu atau
  2. Tanda Pengesahan telah tamat

Lihat Contoh 3.3.2. untuk melihat halaman ralat.

Contoh 3.3.2. - The BadUser.html

As we can see, now MyUserDetailsService not uses the enabled flag of the user – and so it will only allow enabled the user to authenticate.

Now, we will add an AuthenticationFailureHandler to customize the exception messages coming from MyUserDetailsService. Our CustomAuthenticationFailureHandler is shown in Example 4.2.:

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

We will need to modify login.html to show the error messages.

Example 4.3. – Display error messages at login.html:

 error 

5. Adapting the Persistence Layer

Let's now provide the actual implementation of some of these operations involving the verification token as well as the users.

We'll cover:

  1. A new VerificationTokenRepository
  2. New methods in the IUserInterface and its implementation for new CRUD operations needed

Examples 5.1 – 5.3. show the new interfaces and implementation:

Example 5.1. – The VerificationTokenRepository

public interface VerificationTokenRepository extends JpaRepository { VerificationToken findByToken(String token); VerificationToken findByUser(User user); }

Example 5.2. – The IUserService Interface

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

Example 5.3. The 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. Conclusion

In this article, we've expanded the registration process to include an email based account activation procedure.

The account activation logic requires sending a verification token to the user via email so that they can send it back to the controller to verify their identity.

The implementation of this Registration with Spring Security tutorial can be found in the GitHub project – this is an Eclipse based project, so it should be easy to import and run as it is.

Next » Spring Security Registration – Resend Verification Email « Previous The Registration Process With Spring Security