在本文中,我们将使用 Spring Security 实现一个基本的注册流程。该示例是建立在上一篇文章介绍的内容基础之上。
本文目标是添加一个完整的注册流程,可以注册用户、验证和持久化用户数据。
首先,让我们实现一个简单的注册页面,有以下字段:
以下示例展示了一个简单的 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>
我们需要一个数据传输对象(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 节)
登录页面上的注册链接跳转到 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()); } ... // 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.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 的项目,可以很容易导入运行。