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

http://www.baeldung.com/registration-with-spring-mvc-and-spring-security
作者:Eugen Paraschiv
译者:oopsguy.com
最近工作繁忙,更文频次较少,见谅

1、概述

在本文中,我们将使用 Spring Security 实现一个基本的注册流程。该示例是建立在上一篇文章介绍的内容基础之上。

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

2、注册页面

首先,让我们实现一个简单的注册页面,有以下字段:

  • name
  • emal
  • password

以下示例展示了一个简单的 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;

// standard getters and setters
}

注意我们在 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、验证注册数据

接下来,让我们看看控制器在注册新账户时所执行的验证:

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

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");
}
// 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 检查重复的电子邮件

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());
}
...
// 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、持久化数据和完成表单处理

最后,让我们在控制器层实现注册逻辑:

例 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;
}

上面的代码中需要注意以下事项:

  1. 控制器返回一个 ModelAndView 对象,它是发送绑定到视图的模型数据(user)的便捷类。
  2. 如果在验证时发生错误,控制器将重定向到注册表单。
  3. 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 的项目,可以很容易导入运行。

原文示例代码