http://www.baeldung.com/registration-with-spring-mvc-and-spring-security 作者:Eugen Paraschiv 译者:oopsguy.com
1、概述 在本文中,我们将使用 Spring Security 实现一个基本的注册流程。该示例是建立在上一篇文章的基础上。
本文目标是添加一个完整的注册流程,可以注册用户、验证和持久化用户数据。
2、注册页面 首先,让我们实现一个简单的注册页面,有以下字段:
下例展示了一个简单的 registration.html 页面:
示例 2.1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <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 对象应该要有之后需要用到的所有信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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; }
注意,我们在 DTO 对象的字段上使用了标准的 javax.validation 注解。稍后,我们还将实现自定义验证注解来验证电子邮件地址格式和确认密码。(见第 5 节)
4、注册控制器 登录页面上的注册链接跳转到 registration 页面。该页面的后端位于注册控制器中,其映射到 /user/registration :
示例 4.1 — showRegistration 方法
1 2 3 4 5 6 @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、验证注册数据 让我们看看控制器在注册新账户时所执行的验证:
所有必填字段都已填写(无空白字段或 null 字段)
电子邮件地址有效(格式正确)
密码确认字段与密码字段匹配
帐户不存在
5.1、内置验证 对于简单的检查,我们在 DTO 对象上使用开箱即用的 bean 验证注解 — @NotNull 、@NotEmpty 等。
为了触发验证流程,我们只需使用 @Valid 注解对控制器层中的对象进行标注:
1 2 3 4 5 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 — 用于电子邮件验证的自定义注解
1 2 3 4 5 6 7 8 9 @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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 实现上使用新的注解:
1 2 3 4 @ValidEmail @NotNull @NotEmpty private String email;
5.3、密码确认使用自定义验证 我们还需要一个自定义注解和验证器来确保 password 和 matchingPassword 字段匹配:
例 5.3.1 — 验证密码确认的自定义注解
1 2 3 4 5 6 7 8 9 @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 自定义验证器
1 2 3 4 5 6 7 8 9 10 11 12 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 对象上:
1 2 3 4 @PasswordMatches public class UserDto { ... }
5.4、检查帐户是否存在 我们要执行的第四项检查:验证电子邮件帐户是否存在于数据库中。
这是在表单验证之后执行的,并且是在 UserService 实现的帮助下完成的。
例 5.4.1 — 控制器的 createUserAccount 方法调用 UserService 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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" ); } } 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 检查重复的电子邮件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @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、持久化数据和完成表单处理 最后,在控制器层实现注册逻辑:
例 6.1.1 — 控制器中的 RegisterAccount 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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; }
上面的代码中需要注意以下事项:
控制器返回一个 ModelAndView 对象,它可将模型数据(user)传入到要绑定的视图中。
如果在验证时发生错误,控制器将重定向到注册表单。
createUserAccount 方法调用 UserService 持久化数据 。我们将在下一节讨论 UserService 实现
7、UserService - 注册操作 让我们来完成 UserService 中注册操作实现:
例 7.1 — IUserService 接口
1 2 3 4 public interface IUserService { User registerNewUserAccount (UserDto accountDto) throws EmailExistsException ;}
例 7.2 — UserService 类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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 服务实现开始:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @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
1 2 3 4 5 6 <authentication-manager > <authentication-provider user-service-ref ="userDetailsService" /> </authentication-manager > <beans:bean id ="userDetailsService" class ="org.baeldung.security.MyUserDetailsService" />
或者,通过 Java 配置:
1 2 3 4 5 6 7 8 @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 项目。
原文示例代码