9.4 认证用户
在重写configure(HttpSecurity)之前,我们都能使用一个简单却功能完备的登录页。但是,一旦重写了configure(HttpSecurity)方法,就失去了这个简单的登录页面。
不过,把这个功能找回来也很容易。我们所需要做的就是在configure(HttpSecurity)方法中,调用formLogin()。
9.4.1 添加自定义的登录页
创建自定义登录页的第一步就是了解登录表单中都需要些什么。只需 看一下默认登录页面的HTML源码,我们就能了解需要些什么:
<html>
<head><title>Login Page</title></head>
<body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3>
<form name='f' action='/Spittr/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 colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
<input name="_csrf" type="hidden" value="b9147218-5a1b-4e78-92fb-9b3a242f6312" />
</table>
</form>
</body>
</html>
需要注意的一个关键点是<form>提交到了什么地方。同时还需要注意username和password输入域,在你的登录页中,需要同样的输入域。最后,假设没有禁用CSRF的话,还需要保证包含了值为CSRF token的_csrf输入域。
如下程序清单所展现的Thymeleaf模板提供了一个与Spittr应用风格一致的登录页。
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Spitter</title>
<link rel="stylesheet"
type="text/css"
th:href="http://blog.163.com/yetong1985@126/blog/@{/resources/style.css}"></link>
</head>
<body onload='document.f.username.focus();'>
<div id="header" th:include="page :: header"></div>
<div id="content">
<a th:href="http://blog.163.com/yetong1985@126/blog/@{/spitter/register}">Register</a>
<form name='f' th: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 colspan='2'>
<input name="submit" type="submit" value="Login"/></td></tr>
</table>
</form>
</div>
<div id="footer" th:include="page :: copy"></div>
</body>
</html>
需要注意的是,在Thymeleaf模板中,包含了username和password输入域,就像默认的登录页一样,它也提交到了相对于上下文的/login页面上。因为这是一个Thymeleaf模板,因此隐藏的csrf域将会自动添加到表单中。
然后在Spring MVC Controller 中映射“/login” url对应自己写的登录view ,返回登录界面。
package spittr.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller //声明一个控制器
@RequestMapping({"/","/homepage"})
public class HomeController {
@RequestMapping(method=RequestMethod.GET) //处理对"/"的GET请求
public String home() {
System.out.println("处理get请求");
return "home";
}
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String loginPage() {
return "login";
}
}
9.4.2 启用HTTP Basic认证
HTTP Basic认证(HTTP Basic Authentication)会直接通过HTTP请求本身,对要访问应用程序的用户进行认证。你可能在以前见过HTTP Basic认证。当在Web浏览器中使用时,它将向用户弹出一个简单的模态对话框。
但这只是Web浏览器的显示方式。本质上,这是一个HTTP 401响应,表明必须要在请求中包含一个用户名和密码。在REST客户端向它使用的服务进行认证的场景中,这种方式比较适合。
如果要启用HTTP Basic认证的话,只需在configure()方法所传入的HttpSecurity对象上调用httpBasic()即可。另外,还可以通过调用realmName()方法指定域。如下是在Spring Security中启用HTTP Basic认证的典型配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.formLogin() //基于表单的登 录认证
.loginPage("/WEB-INF/views/login.html")
.and().httpBasic() //HTTP Basic方式的认证
.and()
.authorizeRequests()
.antMatchers("/spitter/me").authenticated()
.antMatchers(HttpMethod.POST, "/spittles").authenticated()
.anyRequest().permitAll();
}
9.4.3 启用Remember-me功能
对于应用程序来讲,能够对用户进行认证是非常重要的。但是站在用 户的角度来讲,如果应用程序不用每次都提示他们登录是更好的。这就是为什么许多站点提供了Remember-me功能,你只要登录过一次, 应用就会记住你,当再次回到应用的时候你就不需要登录了。
Spring Security使得为应用添加Remember-me功能变得非常容易。为了 启用这项功能,只需在configure()方法所传入的HttpSecurity 对象上调用rememberMe()即可。
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.formLogin() //基于表单的登 录认证
.loginPage("/login")
.and().httpBasic() //HTTP Basic方式的认证
.and().rememberMe().tokenValiditySeconds(2419200).key("spittrkey")
.and()
.authorizeRequests()
.antMatchers("/spitter/me").authenticated()
.antMatchers(HttpMethod.POST, "/spittles").authenticated()
.anyRequest().permitAll();
}
在这里,我们通过一点特殊的配置就可以启用Remember-me功能。默 认情况下,这个功能是通过在cookie中存储一个token完成的,这个 token最多两周内有效。但是,我们指定这个token最多四周内有效(2,419,200秒)。 存储在cookie中的token包含用户名、密码、过期时间和一个私钥—— 在写入cookie前都进行了MD5哈希。默认情况下,私钥的名为SpringSecured,但在这里我们将其设置为spitterKey,使它专门用于Spittr应用。 如此简单。
既然Remember-me功能已经启用,我们需要有一种方式来让用户表明他们希望应用程序能够记住他们。为了实现这一点,登录请求必须包含一个名为remember-me的参数。在登录表单中,增加一个简单复选框就可以完成这件事情:/spittr/WebRoot/WEB-INF/views/login.html:
<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label></td></tr>
9.4.4 退出
退出功能是通过Servlet容器中的Filter实现的(默认情况下),这个 Filter会拦截针对“/logout”的请求。因此,为应用添加退出功能只需添 加如下的链接即可(如下以Thymeleaf代码片段的形式进行了展现):
<a th:href="http://blog.163.com/yetong1985@126/blog/@{/logout}">Logout</a>
当用户点击这个链接的时候,会发起对/logout的请求,这个请求会被Spring Security的LogoutFilter所处理。用户会退出应用,所有的Remember-me token都会被清除掉。在退出完成后,用户浏览器将会重定向到/login?logout,从而允许用户进行再次登录。
如果你希望用户被重定向到其他的页面,如应用的首页,那么可以在configure()中进行如下的配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.formLogin() //基于表单的登 录认证
.loginPage("/login")
.and().logout().logoutSuccessUrl("/")
.and().httpBasic() //HTTP Basic方式的认证
.and().rememberMe().tokenValiditySeconds(2419200).key("spittrkey")
.and()
.authorizeRequests()
.antMatchers("/spitter/me").authenticated()
.antMatchers(HttpMethod.POST, "/spittles").authenticated()
.anyRequest().permitAll();
}
在本例中,调 用logoutSuccessUrl()表明在退出成功之后,浏览器需要重定向 到“/”。
9.5 保护视图
当为浏览器渲染HTML内容时,你可能希望视图中能够反映安全限制和相关的信息。一个简单的样例就是渲染用户的基本信息(比如显示“您已经以……身份登录”)。或者你想根据用户被授予了什么权限,有条件地渲染特定的视图元素。
9.5.1 使用Spring Security的JSP标签库
Spring Security的JSP标签库很小,只包含三个标签,如表9.6所示。
表9.6 Spring Security通过JSP标签库在视图层上支持安全性
<% @taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
访问认证信息的细节
借助Spring Security JSP标签库,所能做到的最简单的一件事情就是便利地访问用户的认证信息。例如,对于Web站点来讲,在页面顶部以用户名标示显示“欢迎”或“您好”信息是很常见的。这恰恰 是<security:authentication>能为我们所做的事情。例如:
Hello <security:authentication property="principal.username" />!
其中,property用来标示用户认证对象的一个属性。可用的属性取决于用户认证的方式。但是,我们可以依赖几个通用的属性,在不同的认证方式下,它们都是可用的,如表9.7所示。
表9.7 <security:authentiation>认证详情
在我们的示例中,实际上渲染的是principal属性中嵌套的 username属性。
当像前面示例那样使用时,<security:authentication>将在视图中渲染属性的值。但是如果你愿意将其赋值给一个变量,那只需要在var属性中指明变量的名字即可。例如,如下展现了如何将其设置给名为loginId的属性:
<security:authentication property="principal.username" var="loginId" />
这个变量默认是定义在页面作用域内的。但是如果你愿意在其他作用域内创建它,例如请求或会话作用域(或者是能够在javax.servlet.jsp.PageContext中获取的其他作用域),那么可以通过scope属性来声明。例如,要在请求作用域内创建这个变量,那可以使用<security:authentication>按照如下的方式来设置:
<security:authentication property="principal.username" var="loginId" scope="request" />
条件性的渲染内容
有时候视图上的一部分内容需要根据用户被授予了什么权限来确定是否渲染。对于已经登录的用户显示登录表单,或者对还未登录的用户显示个性化的问候信息都是毫无意义的。
Spring Security的<security:authorize>JSP标签能够根据用户被授予的权限有条件地渲染页面的部分内容。例如,在Spittr应用中,对于没有ROLE_SPITTER角色的用户,我们不会为其显示添加新Spitter记录的表单。下面展现了如何使用<security:authorize>标签来为具有ROLE_SPITTER角色的用户显示Spitter表单。
<sec:authorize access="hasRole('ROLE_SPITTER)">
<s:url value="/spittles" var="spittle_url"></s:url>
<sf:form modelAttribute="spittle" action="${spittle_url}">
<sf:label path="text">
<s:message code="label.spittle" text="Enter spittle:"></s:message>
</sf:label>
<sf:textarea path="text" rows="2" cols="40" />
<sf:errors path="text"></sf:errors>
<br />
<div class="spitItSubmit">
<input type="submit" value="Spit it!"
class="status-btn round-btn disabled" />
</div>
</sf:form>
</sec:authorize>
access属性被赋值为一个SpEL表达式,这个表达式的值将确定<security:authorize>标签主体内的内容是否渲染。这里我们使用了hasRole('ROLE_SPITTER')表达式来确保用户具有ROLE_SPITTER角色。但是,当你设置access属性时,可以任意发挥SpEL的强大威力。
9.5.2 使用Thymeleaf的Spring Security方言
与Spring Security的JSP标签库类似,Thymeleaf的安全方言提供了条件化渲染和显示认证细节的能力。表9.8列出了安全方言所提供的属性。
为了使用安全方言,我们需要确保Thymeleaf Extras Spring Security已经位于应用的类路径下。然后,还需要在配置中使用SpringTemplateEngine来注册SpringSecurity Dialect。程序清单9.10所展现的@Bean方法声明了SpringTemplateEngine bean,其中就包含了SpringSecurityDialect。
@Bean
public TemplateEngine templateEngine(TemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.addDialect(new SpringSecurityDialect());
return templateEngine;
}
安全方言注册完成之后,我们就可以在Thymeleaf模板中使用它的属性了。首先,需要在使用这些属性的模板中声明安全命名空间:
<html xmlns="http://www.w3.org/1999/xthml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
</html>
标准的Thymeleaf方法依旧与之前一样,使用th
前缀,安全方言则设置为使用sec
前缀。
这样我们就能在任意合适的地方使用Thymeleaf属性了。比如,假设我们想要为认证用户渲染Hello
文本。如下的Thymeleaf模板代码片段就能完成这项任务:
<div sec:authorize="isAuthenticated()">
Hello <span sec:authentication="name"></span>
</div>
sec:authorize
属性会接受一个SpEL表达式。如果表达式的计算结果为true,那么元素的主体内容就会渲染。在本例中,表达式为isAuthenticated()
,所以只有用户已经进行了认证,才会渲染<div>
标签的主体内容。就这个标签的主体内容部分而言,它的功能是使用认证对象的name
属性提示Hello
文本。