[譯] 學習 Spring Security(三):註冊流程

Oopsguy發表於2019-03-04

www.baeldung.com/registratio…

作者:Eugen Paraschiv

譯者:oopsguy.com

1、概述

在本文中,我們將使用 Spring Security 實現一個基本的註冊流程。該示例是建立在上一篇文章介紹的內容基礎之上。

本文目標是新增一個完整的註冊流程,可以註冊使用者、驗證和持久化使用者資料。

2、註冊頁面

首先,讓我們實現一個簡單的註冊頁面,有以下欄位:

  • name
  • emal
  • password

以下示例展示了一個簡單的 registration.html 頁面:

示例 2.1

<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
    <div>
        <label th:text="#{label.user.firstName}">first</label>
        <input th:field="*{firstName}"/>
        <p th:each="error: ${#fields.errors(`firstName`)}"
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.lastName}">last</label>
        <input th:field="*{lastName}"/>
        <p th:each="error : ${#fields.errors(`lastName`)}"
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.email}">email</label>
        <input type="email" th:field="*{email}"/>
        <p th:each="error : ${#fields.errors(`email`)}"
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.password}">password</label>
        <input type="password" th:field="*{password}"/>
        <p th:each="error : ${#fields.errors(`password`)}"
          th:text="${error}">Validation error</p>
    </div>
    <div>
        <label th:text="#{label.user.confirmPass}">confirm</label>
        <input type="password" th:field="*{matchingPassword}"/>
    </div>
    <button type="submit" th:text="#{label.form.submit}">submit</button>
</form>
 
<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>
複製程式碼

3、User DTO 物件

我們需要一個資料傳輸物件(Data Transfer Object,DTO)來將所有的註冊資訊傳送到 Spring 後端。當我們建立和填充 User 物件時,DTO 物件應該要有後面需要用到的所有資訊:

public class UserDto {
    @NotNull
    @NotEmpty
    private String firstName;
     
    @NotNull
    @NotEmpty
    private String lastName;
     
    @NotNull
    @NotEmpty
    private String password;
    private String matchingPassword;
     
    @NotNull
    @NotEmpty
    private String email;
     
    // standard getters and setters
}
複製程式碼

注意我們在 DTO 物件的欄位上使用了標準的 javax.validation 註解。稍後,我們還將實現自定義驗證註解來驗證電子郵件地址格式以及密碼確認。(見第 5 節)

4、註冊控制器

登入頁面上的註冊連結跳轉到 registration 頁面。該頁面的後端位於註冊控制器中,其對映到 /user/registration

示例 4.1 — showRegistration 方法

@RequestMapping(value = "/user/registration", method = RequestMethod.GET)
public String showRegistrationForm(WebRequest request, Model model) {
    UserDto userDto = new UserDto();
    model.addAttribute("user", userDto);
    return "registration";
}
複製程式碼

當控制器收到 /user/registration 請求時,它會建立一個新的 UserDto 物件,繫結它並返回登錄檔單,很簡單。

5、驗證註冊資料

接下來,讓我們看看控制器在註冊新賬戶時所執行的驗證:

  1. 所有必填欄位都已填寫(無空白欄位或 null 欄位)
  2. 電子郵件地址有效(格式正確)
  3. 密碼確認欄位與密碼欄位匹配
  4. 帳戶不存在

5.1、內建驗證

對於簡單的檢查,我們在 DTO 物件上使用開箱即用的 bean 驗證註解 — @NotNull@NotEmpty 等。

為了觸發驗證流程,我們只需使用 @Valid 註解對控制器層中的物件進行標註:

public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto, 
  BindingResult result, WebRequest request, Errors errors) {
    ...
}
複製程式碼

5.2、使用自定義驗證檢查電子郵件有效性

接下來,讓我們驗證電子郵件地址並確保其格式正確。 我們將要建立一個自定義的驗證器,以及一個自定義驗證註解,並把它命名為 @ValidEmail

要注意的是, 我們使用的是自定義註解,而不是 Hibernate 的 @Email,因為 Hibernate 會將內網地址如 myaddress@myserver 認為是有效的電子郵箱地址格式(見 Stackoverflow 文章),這並不好。

以下是電子郵件驗證註解和自定義驗證器:

例 5.2.1 — 用於電子郵件驗證的自定義註解

@Target({TYPE, FIELD, ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {   
    String message() default "Invalid email";
    Class<?>[] groups() default {}; 
    Class<? extends Payload>[] payload() default {};
}
複製程式碼

請注意,我們在 FIELD 級別定義了註解。

例 5.2.2 — 自定義 EmailValidator:

public class EmailValidator 
  implements ConstraintValidator<ValidEmail, String> {
     
    private Pattern pattern;
    private Matcher matcher;
    private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
        (.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
        (.[A-Za-z]{2,})$"; 
    @Override
    public void initialize(ValidEmail constraintAnnotation) {       
    }
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context){   
        return (validateEmail(email));
    } 
    private boolean validateEmail(String email) {
        pattern = Pattern.compile(EMAIL_PATTERN);
        matcher = pattern.matcher(email);
        return matcher.matches();
    }
}
複製程式碼

現在讓我們在 UserDto 實現上使用新的註解:

@ValidEmail
@NotNull
@NotEmpty
private String email;
複製程式碼

5.3、密碼確認使用自定義驗證

我們還需要一個自定義註解和驗證器來確保 password 和 matchingPassword 欄位匹配:

例 5.3.1 — 驗證密碼確認的自定義註解

@Target({TYPE,ANNOTATION_TYPE}) 
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches { 
    String message() default "Passwords don`t match";
    Class<?>[] groups() default {}; 
    Class<? extends Payload>[] payload() default {};
}
複製程式碼

請注意,@Target 註解指定了這是一個 TYPE 級別註解。這是因為我們需要整個 UserDto 物件來執行驗證。

下面展示了由此註解呼叫的自定義驗證器:

例 5.3.2 — PasswordMatchesValidator 自定義驗證器

public class PasswordMatchesValidator 
  implements ConstraintValidator<PasswordMatches, Object> { 
     
    @Override
    public void initialize(PasswordMatches constraintAnnotation) {       
    }
    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context){   
        UserDto user = (UserDto) obj;
        return user.getPassword().equals(user.getMatchingPassword());    
    }     
}
複製程式碼

現在,應該將 @PasswordMatches 註解應用到 UserDto 物件上:

@PasswordMatches
public class UserDto {
   ...
}
複製程式碼

5.4、檢查帳戶是否存在

我們要執行的第四項檢查是驗證電子郵件帳戶是否存在於資料庫中。

這是在表單驗證之後執行的,並且是在 UserService 實現的幫助下完成的。

例 5.4.1 — 控制器的 createUserAccount 方法呼叫 UserService 物件

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount
      (@ModelAttribute("user") @Valid UserDto accountDto, 
      BindingResult result, WebRequest request, Errors errors) {    
    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    // rest of the implementation
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }    
    return registered;
}
複製程式碼

例 5.4.2 — UserService 檢查重複的電子郵件

@Service
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository; 
     
    @Transactional
    @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());
        }
        ...
        // the rest of the registration operation
    }
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}
複製程式碼

UserService 使用 UserRepository 類來檢查具有給定電子郵件地址的使用者是否已經存在於資料庫中。

持久層中 UserRepository 的實際實現與當前文章無關。 您可以使用 Spring Data 來快速生成資源庫層。

6、持久化資料和完成表單處理

最後,讓我們在控制器層實現註冊邏輯:

例 6.1.1 — 控制器中的 RegisterAccount 方法

@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
public ModelAndView registerUserAccount(
  @ModelAttribute("user") @Valid UserDto accountDto, 
  BindingResult result, 
  WebRequest request, 
  Errors errors) {
     
    User registered = new User();
    if (!result.hasErrors()) {
        registered = createUserAccount(accountDto, result);
    }
    if (registered == null) {
        result.rejectValue("email", "message.regError");
    }
    if (result.hasErrors()) {
        return new ModelAndView("registration", "user", accountDto);
    } 
    else {
        return new ModelAndView("successRegister", "user", accountDto);
    }
}
private User createUserAccount(UserDto accountDto, BindingResult result) {
    User registered = null;
    try {
        registered = service.registerNewUserAccount(accountDto);
    } catch (EmailExistsException e) {
        return null;
    }
    return registered;
}
複製程式碼

上面的程式碼中需要注意以下事項:

  1. 控制器返回一個 ModelAndView 物件,它是傳送繫結到檢視的模型資料(user)的便捷類。
  2. 如果在驗證時發生錯誤,控制器將重定向到登錄檔單。
  3. createUserAccount 方法呼叫 UserService 持久化資料 。我們將在下一節討論 UserService 實現

7、UserService – 註冊操作

讓我們來完成 UserService 中註冊操作實現:

例 7.1 — IUserService 介面

public interface IUserService {
    User registerNewUserAccount(UserDto accountDto)     
      throws EmailExistsException;
}
複製程式碼

例 7.2 — UserService 類

@Service
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
     
    @Transactional
    @Override
    public User registerNewUserAccount(UserDto accountDto) 
      throws EmailExistsException {
         
        if (emailExist(accountDto.getEmail())) {   
            throw new EmailExistsException(
              "There is an account with that email address:  + accountDto.getEmail());
        }
        User user = new User();    
        user.setFirstName(accountDto.getFirstName());
        user.setLastName(accountDto.getLastName());
        user.setPassword(accountDto.getPassword());
        user.setEmail(accountDto.getEmail());
        user.setRoles(Arrays.asList("ROLE_USER"));
        return repository.save(user);       
    }
    private boolean emailExist(String email) {
        User user = repository.findByEmail(email);
        if (user != null) {
            return true;
        }
        return false;
    }
}
複製程式碼

8、載入 User Detail 用於安全登入

在之前的文章中,登入是使用硬編碼的憑據。現在讓我們改變一下,使用新註冊的使用者資訊和憑證。我們將實現一個自定義的 UserDetailsService 來檢查持久層的登入憑據。

8.1、自定義 UserDetailsService

我們從自定義的 user detail 服務實現開始:

@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
  
    @Autowired
    private UserRepository userRepository;
    // 
    public UserDetails loadUserByUsername(String email)
      throws UsernameNotFoundException {
  
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UsernameNotFoundException(
              "No user found with username: "+ email);
        }
        boolean enabled = true;
        boolean accountNonExpired = true;
        boolean credentialsNonExpired = true;
        boolean accountNonLocked = true;
        return  new org.springframework.security.core.userdetails.User
          (user.getEmail(), 
          user.getPassword().toLowerCase(), enabled, accountNonExpired, 
          credentialsNonExpired, accountNonLocked, 
          getAuthorities(user.getRoles()));
    }
     
    private static List<GrantedAuthority> getAuthorities (List<String> roles) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return authorities;
    }
}
複製程式碼

8.2、啟用新的驗證提供器

為了在 Spring Security 配置中啟用新的使用者服務,我們只需要在 authentication-manager 元素內新增對 UserDetailsService 的引用,並新增 UserDetailsService bean:

例子 8.2 — 驗證管理器和 UserDetailsService

<authentication-manager>
    <authentication-provider user-service-ref="userDetailsService" /> 
</authentication-manager>
  
<beans:bean id="userDetailsService"
  class="org.baeldung.security.MyUserDetailsService"/>
複製程式碼

或者,通過 Java 配置:

@Autowired
private MyUserDetailsService userDetailsService;
 
@Override
protected void configure(AuthenticationManagerBuilder auth) 
  throws Exception {
    auth.userDetailsService(userDetailsService);
}
複製程式碼

9、結論

我們終於完成了 — 一個通過 Spring Security 和 Spring MVC 實現的幾乎可用於準生產的註冊流程。後續文章中,我們將通過驗證新使用者的電子郵件來探討新註冊帳戶的啟用流程。

該 Spring Security REST 教程的實現原始碼可在 GitHub 專案上獲取 — 這是一個基於 Eclipse 的專案,可以很容易匯入執行。

原文示例程式碼

相關文章