4.3 保护Web请求

4.3 保护Web请求

Taco Cloud的安全性需求是用户在设计taco和提交订单之前必须要经过认证。但是,主页、登录页和注册页应该对未认证的用户开放。

为了配置这些安全性规则,需要介绍一下WebSecurityConfigurerAdapter的其他configure()方法:

1
2
3
4
@Override
protected void configure(HttpSecurity http) throws Exception {
...
}

configure()方法接受一个HttpSecurity对象,能够用来配置在Web级别该如何处理安全性。我们可以使用HttpSecurity配置的功能包括:

  • 在为某个请求提供服务之前,需要预先满足特定的条件;
  • 配置自定义的登录页;
  • 支持用户退出应用;
  • 预防跨站请求伪造。

配置HttpSecurity常见的需求就是拦截请求以确保用户具备适当的权限。接下来,我们会确保Taco Cloud的顾客能够满足这些需求。

4.3.1 保护请求

我们需要确保只有认证过的用户才能发起对“/design”和“/orders”的请求,而其他请求对所有用户均可用。如下的configure()实现就能实现这一点:

1
2
3
4
5
6
7
8
9
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.hasRole("ROLE_USER")
.antMatchers(“/”, "/**").permitAll()
;
}

对authorizeRequests()的调用会返回一个对象(ExpressionInterceptUrlRegistry),基于它我们可以指定URL路径和这些路径的安全需求。在本例中,我们指定了两条安全规则:

  • 具备ROLE_USER权限的用户才能访问“/design”和“/orders”;
  • 其他的请求允许所有用户访问。

这些规则的顺序是很重要的。声明在前面的安全规则比后面声明的规则有更高的优先级。如果我们交换这两个安全规则的顺序,那么所有的请求都会有permitAll()的规则,对“/design”和“/orders”声明的规则就不会生效了。

在声明请求路径的安全需求时,hasRole()和permitAll()只是众多方法中的两个。表4.1列出了所有可用的方法。

表4.1 用来定义如何保护路径的配置方法

epub_29101559_32

表4.1中的大多数方法为请求处理提供了基本的安全的规则,但它们是自我限制的,也就是只能支持由这些方法所定义的安全规则。除此之外,我们还可以使用access()方法,通过为其提供SpEL表达式来声明更丰富的安全规则。SpringSecurity扩展了SpEL,包含了多个安全相关的值和函数,如表4.2所示。

表4.2 Spring Security对Spring表达式语言的扩展

epub_29101559_33

我们可以看到,表4.2中大多数的安全规则都对应表4.1中类似的方法。实际上,借助access()方法和hasRole()、permitAll表达式,我们可以将configure()重写为程序清单4.9所示的形式:

程序清单4.9 使用Spring表达式来定义认证规则

1
2
3
4
5
6
7
8
9
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers(“/”, "/**").access("permitAll")
;
}

看上去,这似乎也没什么大不了的。毕竟,这些表达式只是模拟了我们之前通过方法调用已经完成的事情。但是,表达式可以更加灵活。例如,假设(基于某些疯狂的原因)我们只允许具备ROLE_USER权限的用户在星期二创建新taco(我们可以将其称为Taco Tuesday),这样就可以重写表达式。如下的代码展现了已修改的configure():

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER') && " +
"T(java.util.Calendar).getInstance().get("+
"T(java.util.Calendar).DAY_OF_WEEK) == " +
"T(java.util.Calendar).TUESDAY")
.antMatchers(“/”, "/**").access("permitAll")
;
}

我们可以使用SpEL实现各种各样的安全性限制。我敢打赌,你已经在想象基于SpEL所能实现的那些有趣的安全性限制了。

Taco Cloud应用的权限可以通过简单使用access()和SpEL表达式来实现,如程序清单4.9所示。现在,我们看一下如何自定义登录页以适应Taco Cloud应用的外观。

4.3.2 创建自定义的登录页

默认的登录页已经比最初丑陋的HTTP basic认证对话框好了很多,但是它依然非常简单,并且与Taco Cloud应用其他部分的外观不搭配。

为了替换内置的登录页,我们首先需要告诉Spring Security自定义登录页的路径是什么。这可以通过调用传入到configure()中的HttpSecurity对象的formLogin()方法来实现:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/design", "/orders")
.access("hasRole('ROLE_USER')")
.antMatchers(“/”, "/**").access("permitAll")
.and()
.formLogin()
.loginPage("/login")
;
}

请注意,在调用formLogin()之前,我们通过and()方法将这一部分的配置与前面的配置连接在一起。and()方法表示我们已经完成了授权相关的配置,并且要添加一些其他的HTTP配置。在开始新的配置区域时,我们可以多次调用and()。

在这个连接之后,我们调用formLogin()开始配置自定义的登录表单。在此之后,对loginPage()的调用声明了我们提供的自定义登录页面的路径。当SpringSecurity断定用户没有认证并且需要登录的时候,它就会将用户重定向到该路径。

现在,我们需要有一个控制器来处理对该路径的请求。因为我们的登录页非常简单,只有一个视图,没有其他内容,所以我们可以很简单地在WebConfig中将其声明为一个视图控制器。在映射到“/”的主页控制器基础之上,如下的addViewControllers()方法声明了登录页面的视图控制器:

1
2
3
4
5
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
registry.addViewController("/login");
}

最后,我们需要自己定义登录页的视图。我们目前使用了Thymeleaf作为模板引擎,所以如下的Thymeleaf就能实现我们的要求:

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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<title>Taco Cloud</title>
</head>
<body>
<h1>Login</h1>
<img th:src="@{/images/TacoCloud.png}"/>
<div th:if="${error}">
Unable to login. Check your username and password.
</div>
<p>New here? Click
<a th:href="@{/register}">here</a> to register.</p>
<!-- tag::thAction[] -->
<form method="POST" th:action="@{/login}" id="loginForm">
<!-- end::thAction[] -->
<label for="username">Username: </label>
<input type="text" name="username" id="username" /><br/>
<label for="password">Password: </label>
<input type="password" name="password" id="password" /><br/>
<input type="submit" value="Login"/>
</form>
</body>
</html>

这个登录页需要关注的事情就是表单提交到了什么地方以及用户名和密码输入域的名称。默认情况下,Spring Security会在“/login”路径监听登录请求并且预期的用户名和密码输入域的名称为username和password。但这都是可配置的,举例来说,如下的配置自定义了路径和输入域的名称:

1
2
3
4
5
6
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/authenticate")
.usernameParameter("user")
.passwordParameter("pwd")

在这里,我们声明Spring Security要监听对“/authenticate”的请求来处理登录信息的提交。同时,用户名和密码的字段名应该是user和pwd。

默认情况下,登录成功之后,用户将会被导航到Spring Security决定让用户登录之前的页面。如果用户直接访问登录页,那么登录成功之后用户将会被导航至根路径(例如,主页)。但是,我们可以通过指定默认的成功页来更改这种行为:

1
2
3
4
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/design")

按照这个配置,用户直接导航至登录页并且成功登录之后将会被定向到“/design”页面。

另外,我们还可以强制要求用户在登录成功之后统一访问设计页面,即便用户在登录之前正在访问其他页面,在登录之后也会被定向到设计页面,这可以通过为defaultSuccessUrl方法传递第二个参数true来实现:

1
2
3
4
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/design", true)

现在,我们已经完成了自定义的登录页面,接下来我们关注认证功能的另一面,那就是如何让用户退出应用。

4.3.3 退出

退出和应用的登录是同等重要的。为了启用退出功能,我们只需在HttpSecurity对象上调用logout方法:

1
2
3
.and()
.logout()
.logoutSuccessUrl("/")

这样会搭建一个安全过滤器,该过滤器会拦截对“/logout”的请求。所以,为了提供退出功能,我们需要为应用的视图添加一个退出表单和按钮:

1
2
3
<form method="POST" th:action="@{/logout}">
<input type="submit" value="Logout"/>
</form>

当用户点击按钮的时候,他们的session将会被清理,这样他们就退出应用了。默认情况下,用户会被重定向到登录页面,这样他们可以重新登录。但是,如果你想要将他们导航至不同的页面,那么可以调用logoutSuccessUrl()指定退出后的不同页面:

1
2
3
.and()
.logout()
.logoutSuccessUrl("/")

在本例中,用户在退出之后将会回到主页。

4.3.4 防止跨站请求伪造

跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的安全攻击。它会让用户在一个恶意的Web页面上填写信息,然后自动(通常是秘密的)将表单以攻击受害者的身份提交到另外一个应用上。例如,用户看到一个来自攻击者的Web站点的表单,这个站点会自动将数据POST到用户银行Web站点的URL上(这个站点可能设计得很糟糕,无法防御这种类型的攻击),实现转账的操作。用户可能根本不知道发生了攻击,直到他们发现账号上的钱已经不翼而飞。

为了防止这种类型的攻击,应用可以在展现表单的时候生成一个CSRF token,并放到隐藏域中,然后将其临时存储起来,以便后续在服务器上使用。在提交表单的时候,token将和其他的表单数据一起发送至服务器端。请求会被服务器拦截,并与最初生成的token进行对比。如果token匹配,那么请求将会允许处理;否则,表单肯定是由恶意网站渲染的,因为它不知道服务器所生成的token。

比较幸运的是,Spring Security提供了内置的CSRF保护。更幸运的是,默认它就是启用的,我们不需要显式配置它。我们唯一需要做的就是确保应用中的每个表单都要有一个名为“_csrf”的字段,它会持有CSRF token。

Spring Security甚至进一步简化了将token放到请求的“_csrf”属性中这一任务。在Thymeleaf模板中,我们可以按照如下的方式在隐藏域中渲染CSRF token:

1
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>

如果你使用Spring MVC的JSP标签库或者Spring Security的Thymeleaf方言,那么甚至都不用明确包含这个隐藏域(这个隐藏域会自动生成)。

在Thymeleaf中,我们只需要确保<form>的某个属性带有Thymeleaf属性前缀即可。通常这并不是什么问题,因为我们一般会使用Thymeleaf渲染相对于上下文的路径。例如,为了让Thymeleaf渲染隐藏域,我们只需要使用th:action属性就可以了:

1
<form method="POST" th:action="@{/login}" id="loginForm">

我们还可以禁用Spring Security对CSRF的支持,但是我很犹豫是否要为你们展现这个功能。CSRF的防护非常重要,并且很容易在表单中实现,所以我们没有理由禁用它。但是,如果你坚持要禁用,那么可以通过调用disable()来实现:

1
2
3
.and()
.csrf()
.disable()

再次强调,不要禁用CSRF防护,对于生产环境的应用来说更是如此。

Taco Cloud应用所有Web层的安全性都已经配置好了。除此之外,我们还有了一个自定义的登录页并且能够通过基于JPA的用户repository来认证用户。接下来,我们看一下如何获取已登录用户的信息。