Spring?Boot用戶注冊(cè)驗(yàn)證的實(shí)現(xiàn)全過(guò)程記錄
1. 概述
在這篇文章中,我們將使用Spring Boot實(shí)現(xiàn)一個(gè)基本的郵箱注冊(cè)賬戶以及驗(yàn)證的過(guò)程。
我們的目標(biāo)是添加一個(gè)完整的注冊(cè)過(guò)程,允許用戶注冊(cè),驗(yàn)證,并持久化用戶數(shù)據(jù)。
2. 創(chuàng)建User DTO Object
首先,我們需要一個(gè)DTO來(lái)囊括用戶的注冊(cè)信息。這個(gè)對(duì)象應(yīng)該包含我們?cè)谧?cè)和驗(yàn)證過(guò)程中所需要的基本信息。
例2.1 UserDto的定義
package com.savagegarden.web.dto;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
public class UserDto {
@NotBlank
private String username;
@NotBlank
private String password;
@NotBlank
private String repeatedPassword;
@NotBlank
private String email;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRepeatedPassword() {
return repeatedPassword;
}
public void setRepeatedPassword(String repeatedPassword) {
this.repeatedPassword = repeatedPassword;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}請(qǐng)注意我們?cè)贒TO對(duì)象的字段上使用了標(biāo)準(zhǔn)的javax.validation注解——@NotBlank。
@NotBlank、@NotEmpty、@NotNull的區(qū)別
@NotNull: 適用于CharSequence, Collection, Map 和 Array 對(duì)象,不能是null,但可以是空集(size = 0)。
@NotEmpty: 適用于CharSequence, Collection, Map 和 Array 對(duì)象,不能是null并且相關(guān)對(duì)象的size大于0。
@NotBlank: 該注解只能作用于String類型。String非null且去除兩端空白字符后的長(zhǎng)度(trimmed length)大于0。
在下面的章節(jié)里,我們還將自定義注解來(lái)驗(yàn)證電子郵件地址的格式以及確認(rèn)二次密碼。
3. 實(shí)現(xiàn)一個(gè)注冊(cè)Controller
登錄頁(yè)面上的注冊(cè)鏈接將用戶帶到注冊(cè)頁(yè)面:
例3.1 RegistrationController的定義
package com.savagegarden.web.controller;
import com.savagegarden.web.dto.UserDto;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class RegistrationController {
@GetMapping("/user/registration")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new UserDto());
return "registration";
}
}當(dāng)RegistrationController收到請(qǐng)求/user/registration時(shí),它創(chuàng)建了新的UserDto對(duì)象,將其綁定在Model上,并返回了注冊(cè)頁(yè)面registration.html。
Model 對(duì)象負(fù)責(zé)在控制器Controller和展現(xiàn)數(shù)據(jù)的視圖View之間傳遞數(shù)據(jù)。
實(shí)際上,放到 Model 屬性中的數(shù)據(jù)將會(huì)復(fù)制到 Servlet Response 的屬性中,這樣視圖就能在這里找到它們了。
從廣義上來(lái)說(shuō),Model 指的是 MVC框架 中的 M,即 Model(模型)。從狹義上講,Model 就是個(gè) key-value 集合。
4. 驗(yàn)證注冊(cè)數(shù)據(jù)
接下來(lái),讓我們看看控制器在注冊(cè)新賬戶時(shí)將執(zhí)行的驗(yàn)證:
- 所有必須填寫(xiě)的字段都已填寫(xiě)且沒(méi)有空字段
- 該電子郵件地址是有效的
- 密碼確認(rèn)字段與密碼字段相符
- 該賬戶不存在
4.1 內(nèi)置的驗(yàn)證
對(duì)于簡(jiǎn)單的檢查,我們將使用@NotBlank來(lái)驗(yàn)證DTO對(duì)象。
為了觸發(fā)驗(yàn)證過(guò)程,我們將在Controller中用@Valid注解來(lái)驗(yàn)證對(duì)象。
例4.1 registerUserAccount
public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
//...
}4.2 自定義驗(yàn)證以檢查電子郵件的有效性
下一步,讓我們驗(yàn)證電子郵件地址,以保證它的格式是正確的。我們將為此建立一個(gè)自定義驗(yàn)證器,以及一個(gè)自定義驗(yàn)證注解--IsEmailValid。
下面是電子郵件驗(yàn)證注解IsEmailValid和自定義驗(yàn)證器EmailValidator:
為什么不使用Hibernate內(nèi)置的@Email?
因?yàn)镠ibernate中的@Email會(huì)驗(yàn)證通過(guò)XXX@XXX之類的郵箱,其實(shí)這是不符合規(guī)定的。
感興趣的讀者朋友可以移步此處Hibernate validator: @Email accepts ask@stackoverflow as valid?。
例4.2.1 IsEmailVaild注解的定義
package com.savagegarden.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ TYPE, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface IsEmailVaild {
String message() default "Invalid Email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}@Target的作用是說(shuō)明了該注解所修飾的對(duì)象范圍
@Retention的作用是說(shuō)明了被它所注解的注解保留多久
@Constraint的作用是說(shuō)明自定義注解的方法
@Documented的作用是說(shuō)明了被這個(gè)注解修飾的注解可以被例如javadoc此類的工具文檔化
關(guān)于如何自定義一個(gè)Java Annotation,感興趣的朋友可以看看我的另一篇文章。
例4.2.2 EmailValidator的定義
package com.savagegarden.validation;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EmailValidator implements ConstraintValidator<IsEmailVaild, String> {
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,})$";
private static final Pattern PATTERN = Pattern.compile(EMAIL_PATTERN);
@Override
public void initialize(IsEmailVaild constraintAnnotation) {
}
@Override
public boolean isValid(final String username, final ConstraintValidatorContext context) {
return (validateEmail(username));
}
private boolean validateEmail(final String email) {
Matcher matcher = PATTERN.matcher(email);
return matcher.matches();
}
}現(xiàn)在讓我們?cè)谖覀兊腢serDto實(shí)現(xiàn)上使用新注解。
@NotBlank @IsEmailVaild private String email;
4.3 使用自定義驗(yàn)證來(lái)確認(rèn)密碼
我們還需要一個(gè)自定義注解和驗(yàn)證器,以確保UserDto中的password和repeatedPassword字段相匹配。
例4.3.1 IsPasswordMatching注解的定義
package com.savagegarden.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchingValidator.class)
@Documented
public @interface IsPasswordMatching {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}請(qǐng)注意,@Target注解表明這是一個(gè)Type級(jí)別的注解。這是因?yàn)槲覀冃枰麄€(gè)UserDto對(duì)象來(lái)執(zhí)行驗(yàn)證。
例4.3.2 PasswordMatchingValidator的定義
package com.savagegarden.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.savagegarden.web.dto.UserDto;
public class PasswordMatchingValidator implements ConstraintValidator<IsPasswordMatching, Object> {
@Override
public void initialize(final IsPasswordMatching constraintAnnotation) {
//
}
@Override
public boolean isValid(final Object obj, final ConstraintValidatorContext context) {
final UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getRepeatedPassword());
}
}現(xiàn)在,將@IsPasswordMatching注解應(yīng)用到我們的UserDto對(duì)象。
@IsPasswordMatching
public class UserDto {
//...
}4.4 檢查該賬戶是否已經(jīng)存在
我們要實(shí)現(xiàn)的第四個(gè)檢查是驗(yàn)證該電子郵件帳戶在數(shù)據(jù)庫(kù)中是否已經(jīng)存在。
這是在表單被驗(yàn)證后進(jìn)行的,我們把這項(xiàng)驗(yàn)證放在了UserService。
例4.4.1 UserService
package com.savagegarden.service.impl;
import com.savagegarden.error.user.UserExistException;
import com.savagegarden.persistence.dao.UserRepository;
import com.savagegarden.persistence.model.User;
import com.savagegarden.service.IUserService;
import com.savagegarden.web.dto.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.transaction.Transactional;
@Service
@Transactional
public class UserService implements IUserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User registerNewUserAccount(UserDto userDto) throws UserExistException {
if (hasEmailExisted(userDto.getEmail())) {
throw new UserExistException("The email has already existed: "
+ userDto.getEmail());
}
User user = new User();
user.setUsername(userDto.getUsername());
user.setPassword(passwordEncoder.encode(userDto.getPassword()));
user.setEmail(userDto.getEmail());
return userRepository.save(user);
}
private boolean hasEmailExisted(String email) {
return userRepository.findByEmail(email) != null;
}
}使用@Transactional開(kāi)啟事務(wù)注解,至于為什么@Transactional加在Service層而不是DAO層?
如果我們的事務(wù)注解@Transactional加在DAO層,那么只要做增刪改,就要提交一次事務(wù),那么事務(wù)的特性就發(fā)揮不出來(lái),尤其是事務(wù)的一致性。當(dāng)出現(xiàn)并發(fā)問(wèn)題的時(shí)候,用戶從數(shù)據(jù)庫(kù)查到的數(shù)據(jù)都會(huì)有所偏差。
一般的時(shí)候,我們的Service層可以調(diào)用多個(gè)DAO層,我們只需要在Service層加一個(gè)事務(wù)注解@Transactional,這樣我們就可以一個(gè)事務(wù)處理多個(gè)請(qǐng)求,事務(wù)的特性也會(huì)充分地發(fā)揮出來(lái)。
UserService依靠UserRepository類來(lái)檢查數(shù)據(jù)庫(kù)中是否已存在擁有相同郵箱的用戶賬戶。當(dāng)然在本文中我們不會(huì)涉及到UserRepository的實(shí)現(xiàn)。
5. 持久化處理
然后我們繼續(xù)實(shí)現(xiàn)RegistrationController中的持久化邏輯。
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request,
Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserExistException uaeEx) {
ModelAndView mav = new ModelAndView();
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
return new ModelAndView("successRegister", "user", userDto);
}在上面的代碼中我們可以發(fā)現(xiàn):
- 我們創(chuàng)建了ModelAndView對(duì)象,該對(duì)象既可以保存數(shù)據(jù)也可以返回一個(gè)View。
常見(jiàn)的ModelAndView的三種用法
(1) new ModelAndView(String viewName, String attributeName, Object attributeValue);
(2) mav.setViewName(String viewName);
mav.addObejct(String attributeName, Object attributeValue);
(3) new ModelAndView(String viewName);
- 在注冊(cè)的過(guò)程中如果產(chǎn)生任何報(bào)錯(cuò),將會(huì)返回到注冊(cè)頁(yè)面。
6. 安全登錄
在本節(jié)內(nèi)容中,我們將實(shí)現(xiàn)一個(gè)自定義的UserDetailsService,從持久層檢查登錄的憑證。
6.1 自定義UserDetailsService
讓我們從自定義UserDetailsService開(kāi)始。
例6.1.1 MyUserDetailsService
@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
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;
}
}6.2 開(kāi)啟New Authentication Provider
然后,為了真正地能夠開(kāi)啟自定義的MyUserDetailsService,我們還需要在SecurityConfig配置文件中加入以下代碼:
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authProvider());
}
復(fù)制代碼限于篇幅,我們就不在這里詳細(xì)展開(kāi)SecurityConfig配置文件。
7. 結(jié)語(yǔ)
至此我們完成了一個(gè)由Spring Boot實(shí)現(xiàn)的基本的用戶注冊(cè)過(guò)程。
到此這篇關(guān)于Spring Boot用戶注冊(cè)驗(yàn)證實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Spring Boot用戶注冊(cè)驗(yàn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解基于Spring Boot與Spring Data JPA的多數(shù)據(jù)源配置
本篇文章主要介紹了詳解基于Spring Boot與Spring Data JPA的多數(shù)據(jù)源配置,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05
Java創(chuàng)建線程三種方式的優(yōu)缺點(diǎn)
今天小編就為大家分享一篇關(guān)于Java創(chuàng)建線程三種方式的優(yōu)缺點(diǎn),小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12
詳解Java合并數(shù)組的兩種實(shí)現(xiàn)方式
這篇文章主要介紹了Java合并數(shù)組的兩種實(shí)現(xiàn)方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
為什么Java開(kāi)發(fā)需要配置環(huán)境變量
這篇文章主要介紹了為什么Java開(kāi)發(fā)需要配置環(huán)境變量,幫助大家更好的理解和學(xué)習(xí)Java,感興趣的朋友可以了解下2020-08-08
Mybatis的parameterType造成線程阻塞問(wèn)題分析
這篇文章主要詳細(xì)分析了Mybatis的parameterType造成線程阻塞問(wèn)題,文中有詳細(xì)的解決方法,及相關(guān)的代碼示例,具有一定的參考價(jià)值,感興趣的朋友可以借鑒閱讀2023-06-06
Spring Boot詳解創(chuàng)建和運(yùn)行基礎(chǔ)流程
這篇文章主要介紹了SpringBoot創(chuàng)建和運(yùn)行的基礎(chǔ)流程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06
SpringBoot配置log4j2的實(shí)現(xiàn)示例
SpringBoot中默認(rèn)使用Logback作為日志框架,本文主要介紹了SpringBoot配置log4j2的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2023-12-12

