学习 Spring Security(二)错误处理与本地化

http://www.baeldung.com/spring-security-login-error-handling-localization
作者:Eugen Paraschiv
译者:oopsguy.com

1、概述

在本文中,我们将介绍如何使用 Spring MVC 实现一个简单的登录页面,该应用程序在后端使用 Spring Security 进行身份验证。

2、登录页面

我们首先定义一个非常简单的登录页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<html>
<head></head>
<body>
<h1>Login</h1>
<form name='f' action="login" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td><input name="submit" type="submit" value="submit" /></td>
</tr>
</table>
</form>
</body>
</html>

现在,让我们添加客户端检查,以确保在提交表单之前用户名和密码不为空。针对这个例子,我们将使用简单的 Javascript,也许 JQuery 是一个更好的选择:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
function validate() {
if (document.f.username.value == "" && document.f.password.value == "") {
alert("Username and password are required");
document.f.username.focus();
return false;
}
if (document.f.username.value == "") {
alert("Username is required");
document.f.username.focus();
return false;
}
if (document.f.password.value == "") {
alert("Password is required");
document.f.password.focus();
return false;
}
}
</script>

您可以看到,我们只需检查用户名或密码字段是否为空,如果为空,将弹出一个 javascript 弹窗以显示相应的消息。

3、Message 本地化

接下来,让我们将正在使用的消息本地化应用到前端。有以下消息类型,每个都以不同的方式进行本地化:

  1. 在 Spring 的控制器或 handler 处理表单之前生成的消息。这些消息可以在 JSP 页面中引用,并使用 Jsp/Jstl 进行本地化(参见第 4.3 节)
  2. 一旦页面已提交给 Spring 进行处理(在提交登录表单之后)本地化的消息,这些消息将使用 Spring MVC 进行本地化(参见第 4.2 节)

3.1、message.properties 文件

在这两种情况下,我们需要为要支持的每种语言创建一个 message.properties 文件。文件的名称应遵循以下约定:messages_[localeCode].properties

例如,如果我们要支持英语和西班牙语错误消息,我们需要以下文件:messages_en.propertiesmessages_es_ES.properties。请注意,对于英文,命名为 messages.properties 也是可以。

我们将把这两个文件放在项目的 classpath中(src/main/resources)。这些文件只包含我们需要用到的不同语言的错误代码和消息,例如:

1
2
3
4
5
6
7
message.username=Username required
message.password=Password required
message.unauth=Unauthorized access!!
message.badCredentials=Invalid username or password
message.sessionExpired=Session timed out
message.logoutError=Sorry, error login out
message.logoutSucc=You logged out successfully

3.2、配置 Spring MVC 本地化

Spring MVC 提供了一个 LocaleResolver,它与 LocaleChangeInterceptor API 一起使用,可根据区域(locale)设置显示不同语言的消息。要配置本地化,我们需要在 MVC 配置中定义以下 bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
registry.addInterceptor(localeChangeInterceptor);
}
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
return cookieLocaleResolver;
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(0);
return messageSource;
}

这里有一个重要和更高级的注意事项是,我们需要确保 messageSource bean 定义在正确的 Spring 上下文中 — 它将在此上下文中使用。请记住,Web 应用程序具有根上下文,并且有一个(或多个)servlet 上下文。

默认情况下,locale 解析器(resolver)将从 HTTP 头获取区域代码。要强制使用默认语言环境,我们需要在 localeResolver() 上设置它:

1
2
3
4
5
6
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver cookieLocaleResolver = new CookieLocaleResolver();
cookieLocaleResolver.setDefaultLocale(Locale.ENGLISH);
return cookieLocaleResolver;
}

该 locale 解析器是一个 CookieLocaleResolver,这意味着它将客户端 cookie 中的 locale 信息存储起来。 因此,它将在每次登录时以及整个访问过程中记住用户的 locale 设置。

此外,还有一个 SessionLocaleResolver,可以记住整个会话中的 locale 设置。 要使用此 LocaleResolver,我们需要使用以下方法替换上述方法:

1
2
3
4
5
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
return sessionLocaleResolver;
}

最后,请注意,LocaleChangeInterceptor 将根据通过登录页面的简单链接发送的 lang 参数的值来更改语言环境:

1
2
<a href="?lang=en">English</a> |
<a href="?lang=es_ES">Spanish</a>

3.3、JSP/JSTL 本地化

JSP/JSTL API 将用于显示在 jsp 页面获取的本地化消息。要使用 jsp 本地化库,我们应该将以下依赖项添加到 pom.xml 中:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.2-b01</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>

4、显示错误信息

4.1、登录验证错误

为了使用 JSP/JSTL 支持并在 login.jsp 中显示本地化消息,您可以在页面中实现以下更改:

  1. 将以下 taglib 标签添加到 login.jsp 中:
1
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
  1. 添加指向 messages.properties 文件的 jsp/jslt 标签:
1
<fmt:setBundle basename="messages" />
  1. 添加以下 fmt:... 标签以将消息存储在 jsp 变量中:
1
2
<fmt:message key="message.password" var="noPass" />
<fmt:message key="message.username" var="noUser" />
  1. 修改我们在第 2 节中的登录验证脚本,以便本地化错误消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">
function validate() {
if (document.f.username.value == "" && document.f.password.value == "") {
alert("${noUser} and ${noPass}");
document.f.username.focus();
return false;
}
if (document.f.username.value == "") {
alert("${noUser}");
document.f.username.focus();
return false;
}
if (document.f.password.value == "") {
alert("${noPass}");
document.f.password.focus();
return false;
}
}
</script>

4.2、登录前错误

如果之前的操作发生失败,将被传递一个错误参数到登录页面。例如,注册表单提交按钮将加载登录页面。 如果注册成功,则在登录表单中显示成功消息,如果失败,则会出现错误消息。

在下面的登录表单示例中,我们通过拦截与 regSuccregError 参数来实现,并根据它们的值显示本地化消息。

1
2
3
4
5
6
7
8
9
10
11
12
<c:if test="${param.regSucc == true}">
<div id="status">
<spring:message code="message.regSucc">
</spring:message>
</div>
</c:if>
<c:if test="${param.regError == true}">
<div id="error">
<spring:message code="message.regError">
</spring:message>
</div>
</c:if>

4.3、登录安全性错误

如果由于某种原因登录过程发生失败,Spring Security 将会重定向到登录错误 URL,我们将其指定为 /login.html?error=true

因此,类似于如何在页面中显示注册状态,我们需要在登录发生问题的情况下做同样的处理:

1
2
3
4
5
6
<c:if test="${param.error != null}">
<div id="error">
<spring:message code="message.badCredentials">
</spring:message>
</div>
</c:if>

请注意,我们使用了 spring:message... 标签。这意味着在 Spring MVC 处理期间将生成错误消息。

完整的登录页面包括了 js 验证和附加的状态消息,您可以在 github 项目中找到。

4.4、注销错误

在下面的示例中,logout.html 页面中的 jsp 代码 <c:if test=”${not empty SPRING_SECURITY_LAST_EXCEPTION}”> 将检查注销过程中是否有存在错误。

例如,如果自定义注销 handler 在重定向到注销页面之前尝试存储用户数据时发生了持久化异常。 虽然这些错误很罕见,但我们也应该尽可能地处理。

让我们来看看完整的 logout.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec"
uri="http://www.springframework.org/security/tags"%>
<%@taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<c:if test="${not empty SPRING_SECURITY_LAST_EXCEPTION}">
<div id="error">
<spring:message code="message.logoutError">
</spring:message>
</div>
</c:if>
<c:if test="${param.logSucc == true}">
<div id="success">
<spring:message code="message.logoutSucc">
</spring:message>
</div>
</c:if>
<html>
<head>
<title>Logged Out</title>
</head>
<body>
<a href="login.html">Login</a>
</body>
</html>

请注意,注销页面还读取查询字符串参数 logSucc,如果其值等于 true,则会显示本地化成功消息。

5、Spring Security 配置

本文的重点是前端的登录过程,而不是后端。

5.1、重定向到登录错误 URL

<form-login…/> 指令将应用程序的引导到处理登录错误 URL:

1
authentication-failure-url="/login.html?error=true"

5.2、注销成功重定向

1
2
3
4
<logout
invalidate-session="false"
logout-success-url="/logout.html?logSucc=true"
delete-cookies="JSESSIONID" />

logout-success-url 属性简单地重定向到注销页面,该参数确定注销成功。

6、结论

在本文中,我们介绍了如何为 Spring Security 支持的应用程序实现 Login 页面以及处理登录验证、显示身份验证错误和消息本地化。

相关代码和链接