• Spring Security 註冊 – 發送驗證郵件
• Spring Security 註冊 – 密碼編碼
• 註冊 API 變為 RESTful
• Spring Security – 重置您的密碼
• 註冊 – 密碼強度和規則
• 更新您的密碼
• 通知用户從新設備或位置登錄
1. 概述
在本教程中,我們將實現一個基本的註冊流程,並結合 Spring Security。我們將在此基礎上擴展我們之前文章中探討的登錄概念。我們之前文章中,我們探討了登錄過程。
目標是添加一個完整的註冊流程,允許用户註冊,並驗證和持久化用户數據。
2. 註冊頁面
首先,我們將實現一個簡單的註冊頁面,顯示以下字段:
- 姓名 (名字和姓氏)
- 郵箱
- 密碼 (以及密碼確認字段)
以下示例顯示了一個簡單的 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. 用户 DTO 對象
我們需要一個 數據傳輸對象 (DTO) 將所有註冊信息發送到我們的 Spring 後端。該 DTO 對象應包含我們在創建和填充我們的 User 對象時所需的所有信息:
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
}請注意,我們使用了標準 javax.validation 聲明在 DTO 對象字段上的註解。稍後,我們還將 實現自定義驗證註解,以驗證電子郵件格式,以及密碼確認(參見 第 5 章)。
4. 註冊控制器
一個登錄頁面的註冊鏈接會將用户引導到註冊頁面。該頁面的後端邏輯位於註冊控制器中,並映射到 “/user/registration”:
示例 4.1. showRegistration 方法
@GetMapping("/user/registration")
public String showRegistrationForm(WebRequest request, Model model) {
UserDto userDto = new UserDto();
model.addAttribute("user", userDto);
return "registration";
}當控制器接收到請求“/user/registration”,它會創建新的 UserDto 對象,該對象支持 registration 表單,將其綁定並返回。
5. 驗證註冊數據
接下來,我們將探討控制器在註冊新帳户時將執行的驗證:
- 所有必填字段已填寫(無空或空值字段)。
- 電子郵件地址有效(格式正確)。
- 密碼確認字段與密碼字段匹配。
- 帳户不存在。
5.1. 內置驗證
對於簡單的檢查,我們將使用 DTO 對象上的內置 Bean 驗證註解,例如 @NotNull、@NotEmpty 等。
然後,為了觸發驗證過程,我們只需在控制器層使用 @Valid 註解來標記對象:
public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest 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. 自定義 EmailValidato 器:
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. 使用自定義驗證以進行密碼確認
我們需要自定義標註和驗證器,以確保 密碼 和 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. 控制器方法 registerUserAccount 調用 UserService 對象
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request,
Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
// rest of the implementation
}
示例 5.4.2. 用户服務 檢查重複的電子郵件
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Override
public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
if (emailExists(userDto.getEmail())) {
throw new UserAlreadyExistException("There is an account with that email address: "
+ userDto.getEmail());
}
// the rest of the registration operation
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}UserService 依賴 UserRepository 類來檢查給定電子郵件地址是否已存在於數據庫中。
持久層中 UserRepository 類的實際實現與本文檔無關;但是,一種快速的方法是使用 Spring Data 生成 Repository 層。
6. persisting data and finishing-up form processing
接下來,我們將實現控制層中的註冊邏輯:
示例 6.1. 控制器中的 RegisterAccount 方法
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request,
Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
return new ModelAndView("successRegister", "user", userDto);
}
請注意以上代碼中的事項:
- 控制器返回一個 ModelAndView 對象,這是用於將模型數據(user)綁定到視圖的便捷類。
- 如果驗證時設置了任何錯誤,控制器將重定向到註冊表單。
7. UserService – 註冊操作
最後,我們將完成 UserService 中註冊操作的實現:
示例 7.1. IUserService 接口
public interface IUserService {
User registerNewUserAccount(UserDto userDto);
}示例 7.2:UserService 類
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Override
public User registerNewUserAccount(UserDto userDto) throws UserAlreadyExistException {
if (emailExists(userDto.getEmail())) {
throw new UserAlreadyExistException("There is an account with that email address: "
+ userDto.getEmail());
}
User user = new User();
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setPassword(userDto.getPassword());
user.setEmail(userDto.getEmail());
user.setRoles(Arrays.asList("ROLE_USER"));
return repository.save(user);
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}8. 為安全登錄加載用户詳情
在之前的文章中,登錄使用了硬編碼的憑據。我們將通過使用新註冊的用户信息和憑據來改變這一點。此外,我們還將實現一個自定義的UserDetailsService,用於從持久化層檢查登錄憑據。
8.1. 自定義 UserDetailsService
我們將從自定義 UserDetailsService 的實現開始:
@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(), 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 配置中的新用户服務,只需在 <em authentication-manager</em> 元素中添加對 <em UserDetailsService</em> 的引用,並添加 <em UserDetailsService</em> Bean 即可。
示例 8.2:身份管理器和 <em UserDetailsService
<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
<beans:bean id="userDetailsService" class="com.baeldung.security.MyUserDetailsService" />通過 Java 配置也是一種選擇:
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}9. 結論
在本文中,我們演示了使用 Spring Security 和 Spring MVC 實現的完整且接近生產環境的註冊流程。接下來,我們將討論如何通過驗證新用户的電子郵件來激活新註冊的賬户。