등록 – 이메일로 새 계정 활성화

이 기사는 시리즈의 일부입니다. • Spring Security Registration Tutorial

• Spring Security를 ​​사용한 등록 프로세스

• 등록 – 이메일로 새 계정 활성화 (현재 기사) • 봄 보안 등록 – 확인 이메일 재전송

• Spring Security 등록 – 비밀번호 인코딩

• 등록 API가 RESTful이 됨

• 봄 보안 – 비밀번호 재설정

• 등록 – 암호 강도 및 규칙

• 비밀번호 업데이트

1. 개요

이 기사는 등록 프로세스 중 누락 된 부분 중 하나 인 Spring Security로 등록 시리즈 를 계속 진행 합니다. 사용자의 이메일을 확인하여 계정을 확인합니다 .

등록 확인 메커니즘은 사용자가 성공적으로 등록한 후 보낸 " 등록 확인 "이메일 에 응답하여 이메일 주소를 확인하고 계정을 활성화하도록합니다. 사용자는 이메일로 전송 된 고유 활성화 링크를 클릭하여이를 수행합니다.

이 논리에 따라 새로 등록 된 사용자는이 프로세스가 완료 될 때까지 시스템에 로그인 할 수 없습니다.

2. 확인 토큰

사용자가 확인되는 주요 아티팩트로 간단한 확인 토큰을 사용합니다.

2.1. VerificationToken 엔티티

VerificationToken의 기업은 다음과 같은 기준을 충족해야합니다 :

  1. 사용자 에게 다시 연결되어야합니다 (단방향 관계를 통해).
  2. 등록 직후 생성됩니다.
  3. 그것은 것입니다 24 시간 이내에 만료 그것의 창조 다음
  4. 가지고 임의로 생성 된 고유 한 값을

요구 사항 2와 3은 등록 로직의 일부입니다. 나머지 두 개는 예제 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 }

VerificationToken < -> 사용자 연결 에서 데이터 무결성과 일관성을 보장하려면 사용자에 대해 nullable = false 를 확인합니다 .

2.2. 사용자 에게 활성화 된 필드 추가

처음에 사용자 가 등록되면이 활성화 된 필드는 false 로 설정됩니다 . 계정 확인 과정에서 성공하면 사실이 됩니다.

User 엔터티에 필드를 추가하여 시작하겠습니다 .

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

이 필드의 기본값을 false로 설정하는 방법에 유의하십시오 .

3. 계정 등록 중

사용자 등록 사용 사례에 두 가지 비즈니스 논리를 추가해 보겠습니다.

  1. 사용자에 대한 VerificationToken 을 생성 하고 유지합니다.
  2. VerificationToken 값 이 포함 된 확인 링크가 포함 된 계정 확인을위한 이메일 메시지를 보냅니다.

3.1. Spring 이벤트를 사용하여 토큰 생성 및 확인 이메일 보내기

이 두 가지 추가 로직은 "부수적 인"백엔드 작업이므로 컨트롤러에서 직접 수행해서는 안됩니다.

컨트롤러는 이러한 작업의 실행을 트리거하기 위해 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); }

주목해야 할 추가 사항 중 하나 는 이벤트 게시를 둘러싼 try catch 블록입니다. 이 코드는 이벤트 게시 후 실행 된 논리에 예외가있을 때마다 오류 페이지를 표시합니다.이 경우에는 이메일 전송입니다.

3.2. The Event and the Listener

Let's now see the actual implementation of this new OnRegistrationCompleteEvent that our controller is sending out, as well as the listener that is going to handle it:

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

Example 3.2.2. The RegistrationListener Handles the 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); } }

Here, the confirmRegistration method will receive the OnRegistrationCompleteEvent, extract all the necessary User information from it, create the verification token, persist it, and then send it as a parameter in the “Confirm Registration” link.

As was mentioned above, any javax.mail.AuthenticationFailedException thrown by JavaMailSender will be handled by the controller.

3.3. Processing the Verification Token Parameter

When the user receives the “Confirm Registration” link they should click on it.

Once they do – the controller will extract the value of the token parameter in the resulting GET request and will use it to enable the User.

Let's see this process in Example 3.3.1.:

Example 3.3.1. – RegistrationController Processing the Registration Confirmation

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

The user will be redirected to an error page with the corresponding message if:

  1. The VerificationToken does not exist, for some reason or
  2. The VerificationToken has expired

See Example 3.3.2. to see the error page.

Example 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