[译] 学习 Spring Security(三):注册流程

www.baeldung.com/registratio…html

做者:Eugen Paraschivjava

转载自公众号:stackgcgit

一、概述

在本文中,咱们将使用 Spring Security 实现一个基本的注册流程。该示例是创建在上一篇文章的基础上。github

本文目标是添加一个完整的注册流程,能够注册用户、验证和持久化用户数据。spring

二、注册页面

首先,让咱们实现一个简单的注册页面,有如下字段:数据库

  • name
  • emal
  • password

下例展现了一个简单的 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>
复制代码

三、User DTO 对象

咱们须要一个数据传输对象(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 对象,绑定它并返回注册表单,很简单。

五、验证注册数据

让咱们看看控制器在注册新帐户时所执行的验证:

  1. 全部必填字段都已填写(无空白字段或 null 字段)
  2. 电子邮件地址有效(格式正确)
  3. 密码确认字段与密码字段匹配
  4. 账户不存在

5.一、内置验证

对于简单的检查,咱们在 DTO 对象上使用开箱即用的 bean 验证注解 — @NotNull@NotEmpty 等。

为了触发验证流程,咱们只需使用 @Valid 注解对控制器层中的对象进行标注:

public ModelAndView registerUserAccount( @ModelAttribute("user") @Valid UserDto accountDto, BindingResult result, WebRequest request, Errors errors) {
    ...
}
复制代码

5.二、使用自定义验证检查电子邮件有效性

让咱们来验证电子邮件地址并确保其格式正确。咱们要建立一个自定义的验证器,以及一个自定义验证注解,将它命名为 @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;
复制代码

5.三、密码确认使用自定义验证

咱们还须要一个自定义注解和验证器来确保 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 {
   ...
}
复制代码

5.四、检查账户是否存在

咱们要执行的第四项检查:验证电子邮件账户是否存在于数据库中。

这是在表单验证以后执行的,而且是在 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;
}
复制代码

上面的代码中须要注意如下事项:

  1. 控制器返回一个 ModelAndView 对象,它可将模型数据(user)传入到要绑定的视图中。
  2. 若是在验证时发生错误,控制器将重定向到注册表单。
  3. createUserAccount 方法调用 UserService 持久化数据 。咱们将在下一节讨论 UserService 实现

七、UserService - 注册操做

让咱们来完成 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;
    }
}
复制代码

八、加载 User Detail 用于安全登陆

在以前的文章中,登陆使用了硬编码的凭据。如今让咱们修改一下,使用新注册的用户信息和凭证。咱们将实现一个自定义的 UserDetailsService 来检查持久层的登陆凭据。

8.一、自定义 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;
    }
}
复制代码

8.二、启用新的验证提供器

为了在 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 项目。

原文示例代码

相关文章
相关标签/搜索