做者:Eugen Paraschivjava
轉載自公衆號:stackgcgit
在本文中,咱們將使用 Spring Security 實現一個基本的註冊流程。該示例是創建在上一篇文章的基礎上。github
本文目標是添加一個完整的註冊流程,能夠註冊用戶、驗證和持久化用戶數據。spring
首先,讓咱們實現一個簡單的註冊頁面,有如下字段:數據庫
下例展現了一個簡單的 registration.html 頁面:後端
示例 2.1spring-mvc
<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>
複製代碼
咱們須要一個數據傳輸對象(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 節)mvc
登陸頁面上的註冊連接跳轉到 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 對象,綁定它並返回註冊表單,很簡單。
讓咱們看看控制器在註冊新帳戶時所執行的驗證:
對於簡單的檢查,咱們在 DTO 對象上使用開箱即用的 bean 驗證註解 — @NotNull、@NotEmpty 等。
爲了觸發驗證流程,咱們只需使用 @Valid 註解對控制器層中的對象進行標註:
public ModelAndView registerUserAccount( @ModelAttribute("user") @Valid UserDto accountDto, BindingResult result, WebRequest request, Errors errors) {
...
}
複製代碼
讓咱們來驗證電子郵件地址並確保其格式正確。咱們要建立一個自定義的驗證器,以及一個自定義驗證註解,將它命名爲 @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;
複製代碼
咱們還須要一個自定義註解和驗證器來確保 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 {
...
}
複製代碼
咱們要執行的第四項檢查:驗證電子郵件賬戶是否存在於數據庫中。
這是在表單驗證以後執行的,而且是在 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());
}
...
// 其他的註冊操做邏輯
}
private boolean emailExist(String email) {
User user = repository.findByEmail(email);
if (user != null) {
return true;
}
return false;
}
}
複製代碼
UserService 使用 UserRepository 類來檢查指定的電子郵件地址的用戶是否已經存在於數據庫中。
持久層中 UserRepository 的實際實現與當前文章無關。你可使用 Spring Data 來快速生成資源庫(repository)層。
最後,在控制器層實現註冊邏輯:
例 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;
}
複製代碼
上面的代碼中須要注意如下事項:
讓咱們來完成 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;
}
}
複製代碼
在以前的文章中,登陸使用了硬編碼的憑據。如今讓咱們修改一下,使用新註冊的用戶信息和憑證。咱們將實現一個自定義的 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;
}
}
複製代碼
爲了在 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);
}
複製代碼
終於完成了 —— 一個由 Spring Security 和 Spring MVC 實現的幾乎可用於準生產環境的註冊流程。在後續文章中,咱們將經過驗證新用戶的電子郵件來探討新註冊賬戶的激活流程。
該 Spring Security REST 教程的實現源碼可在 GitHub 項目上獲取 —— 這是一個 Eclipse 項目。