学习 Spring Security(七):RESTful 化注册 API

http://www.baeldung.com/registration-restful-api

作者:Eugen Paraschiv
译者:oopsguy.com

1、概述

在注册系列的最后几篇博文中,我们将以 MVC 方式来构建所需要到的大部分功能。

我们将把这些 API 中的一部分转换成更具 REST 风格。

2、注册操作

从注册操作开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequestMapping(value = "/user/registration", method = RequestMethod.POST)
@ResponseBody
public GenericResponse registerUserAccount(
@Valid UserDto accountDto, HttpServletRequest request) {
logger.debug("Registering user account with information: {}", accountDto);
User registered = createUserAccount(accountDto);
if (registered == null) {
throw new UserAlreadyExistException();
}
String appUrl = "http://" + request.getServerName() + ":" +
request.getServerPort() + request.getContextPath();

eventPublisher.publishEvent(
new OnRegistrationCompleteEvent(registered, request.getLocale(), appUrl));

return new GenericResponse("success");
}

这与原来的 MVC 实现有何不同呢?

  • 请求映射到 HTTP POST
  • 通过 @ResponseBody 注解直接返回一个合适的 DTO 并进行转换序列化,之后写入响应体
  • 不再处理方法中的错误处理

我们还删除了旧的 showRegistrationPage() —— 因为不需要显示注册页面。

3、registration.html

为了适应上述的变化,我们现在需要修改 registration.html

  • 使用 Ajax 提交注册表单
  • 以 JSON 的方式接收操作后响应的结果
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<html>
<head>
<title th:text="#{label.form.title}">form</title>
</head>
<body>
<form action="/" method="POST" enctype="utf8">
<input name="firstName" value="" />
<span id="firstNameError" style="display:none"></span>

<input name="lastName" value="" />
<span id="lastNameError" style="display:none"></span>

<input name="email" value="" />
<span id="emailError" style="display:none"></span>

<input name="password" value="" type="password" />
<span id="passwordError" style="display:none"></span>

<input name="matchingPassword" value="" type="password" />
<span id="globalError" style="display:none"></span>

<a href="#" onclick="register()" th:text="#{label.form.submit}">submit</a>
</form>


<script src="jquery.min.js"></script>
<script type="text/javascript">
var serverContext = [[@{/}]];

function register(){
$(".alert").html("").hide();
var formData= $('form').serialize();
$.post(serverContext + "/user/registration",formData ,function(data){
if(data.message == "success"){
window.location.href = serverContext +"/successRegister.html";
}
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("MailError") > -1)
{
window.location.href = serverContext + "/emailError.html";
}
else if(data.responseJSON.error.indexOf("InternalError") > -1){
window.location.href = serverContext +
"/login.html?message=" + data.responseJSON.message;
}
else if(data.responseJSON.error == "UserAlreadyExist"){
$("#emailError").show().html(data.responseJSON.message);
}
else{
var errors = $.parseJSON(data.responseJSON.message);
$.each( errors, function( index,item ){
$("#"+item.field+"Error").show().html(item.defaultMessage);
});
errors = $.parseJSON(data.responseJSON.error);
$.each( errors, function( index,item ){
$("#globalError").show().append(item.defaultMessage+"<br>");
});
}
});
}
</script>
</body>
</html>

4、异常处理

随着 RESTful API 数量的增长,异常处理逻辑当然也会变得更加成熟。

我们使用 @ControllerAdvice 机制来干净利落地处理程序抛出的异常,不过 我们需要一种新的异常类型。

BindException —— 在 UserDto 验证(如果无效)时抛出。我们将重写默认的 ResponseEntityExceptionHandler 中的 handleBindException() 方法,往响应体中添加错误信息:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected ResponseEntity<Object> handleBindException
(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
logger.error("400 Status Code", ex);
BindingResult result = ex.getBindingResult();
GenericResponse bodyOfResponse =
new GenericResponse(result.getFieldErrors(), result.getGlobalErrors());

return handleExceptionInternal(
ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request);
}

我们还需要处理自定义异常 UserAlreadyExistException —— 当用户注册一个已经存在的电子邮件时就会抛出该异常:

1
2
3
4
5
6
7
8
9
@ExceptionHandler({ UserAlreadyExistException.class })
public ResponseEntity<Object> handleUserAlreadyExist(RuntimeException ex, WebRequest request) {
logger.error("409 Status Code", ex);
GenericResponse bodyOfResponse = new GenericResponse(
messages.getMessage("message.regError", null, request.getLocale()), "UserAlreadyExist");

return handleExceptionInternal(
ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request);
}

6、通用响应

我们还需要改进 GenericResponse 实现来保存这些验证错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenericResponse {

public GenericResponse(List<FieldError> fieldErrors, List<ObjectError> globalErrors) {
super();
ObjectMapper mapper = new ObjectMapper();
try {
this.message = mapper.writeValueAsString(fieldErrors);
this.error = mapper.writeValueAsString(globalErrors);
} catch (JsonProcessingException e) {
this.message = "";
this.error = "";
}
}
}

6、UI —— 字段和全局错误

最后,让我们看看如何使用 jQuery 处理字段和全局错误:

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
var serverContext = [[@{/}]];

function register(){
$(".alert").html("").hide();
var formData= $('form').serialize();

$.post(serverContext + "/user/registration",formData ,function(data){
if(data.message == "success"){
window.location.href = serverContext +"/successRegister.html";
}
})
.fail(function(data) {
if(data.responseJSON.error.indexOf("MailError") > -1)
{
window.location.href = serverContext + "/emailError.html";
}
else if(data.responseJSON.error.indexOf("InternalError") > -1){
window.location.href = serverContext +
"/login.html?message=" + data.responseJSON.message;
}
else if(data.responseJSON.error == "UserAlreadyExist"){
$("#emailError").show().html(data.responseJSON.message);
}
else{
var errors = $.parseJSON(data.responseJSON.message);
$.each( errors, function( index,item ){
$("#"+item.field+"Error").show().html(item.defaultMessage);
});
errors = $.parseJSON(data.responseJSON.error);
$.each( errors, function( index,item ){
$("#globalError").show().append(item.defaultMessage+"<br>");
});
}
});
}

注意:

  • 如果存在验证错误,则 message 对象包含 error 字段,error 对象包含全局错误
  • 我们在每个字段旁边显示该字段的错误信息
  • 我们在表单的最后一个地方显示所有的全局错误

7、结论

这篇博文的重点是将 API 转换 RESTful 风格,并简单介绍了前端处理 API 的方法。

jQuery 前端在本次内容中并不是重点,它只是一个基础的客户端类库,您可以在任何 JS 框架中实现这些逻辑,而后端的 API 仍然完全相同。

本教程的完整实现可以在 github 项目中找到。

原文项目地址

https://github.com/eugenp/spring-security-registration