[譯] 學習 Spring Security(四):使用郵箱激活新帳戶

www.baeldung.com/registratio…html

做者:Eugen Paraschivjava

轉載自公衆號:stackgcgit

一、概述

本文續以前的 Spring Security 系列之註冊流程中缺失的部分 — 驗證用戶的電子郵件以確認賬戶github

註冊確認機制強制用戶在成功註冊後執行確認註冊電子郵件中的操做,以驗證其電子郵件地址並激活賬戶。用戶經過單擊電子郵件中的惟一激活連接來完成激活操做。spring

根據此邏輯,新註冊的用戶沒法登陸到系統,除非完成了該流程。session

二、驗證 Token

咱們將使用一個簡單的驗證令牌做爲驗證用戶的憑據。併發

2.一、VerificationToken 實體

VerificationToken 實體必須符合如下標準:app

  1. 它必須指向 User(經過一個單向關係)
  2. 它將在註冊後當即建立
  3. 它將在建立後 24 小時內過時
  4. 有一個惟1、隨機生成的值

第 2 和 3 點是註冊邏輯的一部分。其他的兩個實現位於簡單的 VerificationToken 實體中,如示例 2.1。dom

示例 2.1ide

@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());
    }
     
    // 省略構造方法、getter 和 setter
}
複製代碼

注意 User 上的 nullable = false,其確保了 VerificationToken <-> User 關聯中數據的完整性和一致性。

2.二、將 enabled 字段添加到 User 中

當用戶註冊時,此 enabled 字段將被設置爲 false。在賬戶驗證過程當中,若是經過,則置爲 true。

添加字段到 User 實體中:

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

請注意,咱們也將該字段的默認值設置爲 false。

三、賬戶註冊期間

添加兩個額外的業務邏輯到用戶註冊用例中:

  1. User 生成 VerificationToken 並保存
  2. 發送電子郵件進行賬戶確認 —— 其中包含帶有 VerificationToken 值的確認連接

3.一、使用 Spring Event 建立令牌併發送驗證郵件

這兩個額外的邏輯不該該由控制器直接執行,由於它們是並行的後臺任務。

控制器將發佈一個 Spring ApplicationEvent 來觸發這些任務的執行。這和注入 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.二、Event 與 Listener

如今讓咱們看看 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.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 信息,建立驗證令牌,將其保存,而後在確認註冊連接中將其做爲參數發送。

如上所述,JavaMailSender 引起的任何 javax.mail.AuthenticationFailedException 都將由控制器處理。

3.三、處理驗證令牌參數

當用戶收到確認註冊連接時點擊它。

一旦點擊,控制器將提取 GET 請求中的令牌參數的值,並將使用它來啓用 User。

例 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 已過時

見示例 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 檢查和過時流程中有兩個地方能夠改進:

  1. 咱們可使用 Cron 做業在後臺檢查令牌是否過時
  2. 一旦過時,咱們能夠讓用戶有機會得到新的令牌

咱們將生成新令牌流程推遲到後面的文章再講,如今假設用戶確實在這裏成功驗證了令牌。

四、將賬戶激活檢查添加到登陸流程

咱們須要添加檢查用戶是否啓用的代碼:

示例 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 不使用 User 的 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>
複製代碼

五、適配持久層

讓咱們來看看一些涉及到驗證令牌和用戶操做的實際實現。

涵蓋如下內容:

  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 accountDto) throws EmailExistsException;
 
    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 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);
    }
}
複製代碼

六、總結

本文介紹了註冊流程 —— 基於電子郵件的賬戶激活流程。

賬戶激活邏輯爲經過電子郵件向用戶發送驗證令牌,以便他們能夠將信息發送回控制器以驗證身份。

該註冊示例和 Spring Security 教程的實現能夠在 GitHub 項目中找到。

原文項目源碼

相關文章
相關標籤/搜索