4.3 保护 web 请求

4.3 保护 web 请求

Taco Cloud 的安全需求应该要求用户在设计 tacos 或下订单之前进行身份验证。但是主页、登录页面和注册页面应该对未经身份验证的用户可用。

要配置这些安全规则,需要介绍一下 WebSecurityConfigurerAdapter 的另一个 configure() 方法:

@Override
protected void configure(HttpSecurity http) throws Exception {
    ...
}

这个 configure() 方法接受 HttpSecurity 对象,可以使用该对象来配置如何在 web 级别处理安全性。可以配置 HttpSecurity 的属性包括:

  • 在允许服务请求之前,需要满足特定的安全条件
  • 配置自定义登录页面
  • 使用户能够退出应用程序
  • 配置跨站请求伪造保护

拦截请求以确保用户拥有适当的权限是配置 HttpSecurity 要做的最常见的事情之一。让我们确保 Taco Cloud 的客户满足这些要求。

4.3.1 保护请求

需要确保 /design 和 /orders 的请求仅对经过身份验证的用户可用;应该允许所有用户发出所有其他请求。下面的 configure() 实现就是这样做的:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        	.antMatchers("/design", "/orders")
        		.hasRole("ROLE_USER")
        	.antMatchers(/, "/**").permitAll();
}

对 authorizeRequests() 的调用返回一个对象(ExpressionInterceptUrlRegistry),可以在该对象上指定 URL 路径和模式以及这些路径的安全需求。在这种情况下,指定两个安全规则:

  • 对于 /design 和 /orders 的请求应该是授予 ROLE_USER 权限的用户的请求。
  • 所有的请求都应该被允许给所有的用户。

这些规则的顺序很重要。首先声明的安全规则优先于较低级别声明的安全规则。如果交换这两个安全规则的顺序,所有请求都将应用 permitAll(),那么关于 /design 和 /orders 请求的规则将不起作用。

hasRole() 和 permitAll() 方法只是声明请求路径安全需求的两个方法。表 4.1 描述了所有可用的方法。

表 4.1 定义被保护路径的配置方法

方法 做了什么
access(String) 如果 SpEL 表达式的值为 true,则允许访问
anonymous() 默认用户允许访问
authenticated() 认证用户允许访问
denyAll() 无条件拒绝所有访问
fullyAuthenticated() 如果用户是完全授权的(不是记住用户),则允许访问
hasAnyAuthority(String…) 如果用户有任意给定的权限,则允许访问
hasAnyRole(String…) 如果用户有任意给定的角色,则允许访问
hasAuthority(String) 如果用户有给定的权限,则允许访问
hasIpAddress(String) 来自给定 IP 地址的请求允许访问
hasRole(String) 如果用户有给定的角色,则允许访问
not() 拒绝任何其他访问方法
permitAll() 无条件允许访问
rememberMe() 允许认证了的同时标记了记住我的用户访问

表 4.1 中的大多数方法为请求处理提供了基本的安全规则,但是它们是自我限制的,只支持那些方法定义的安全规则。或者,可以使用 access() 方法提供 SpEL 表达式来声明更丰富的安全规则。Spring Security 扩展了 SpEL,包括几个特定于安全性的值和函数,如表 4.2 所示。

表 4.2 Spring Security 对 SpEL 的扩展

Security 表达式 意指什么
authentication 用户认证对象
denyAll 通常值为 false
hasAnyRole(list of roles) 如果用户有任何给定的角色,则为 true
hasRole(role) 如果用户有给定的角色,则为 true
hasIpAddress(IP Address) 如果请求来自给定 IP 地址,则为 true
isAnonymous() 如果用户是默认用户,则为 true
isAuthenticated() 如果用户是认证了的,则为 true
isFullyAuthenticated() 如果用户被完全认证了的(不是使用记住我进行认证),则为 true
isRememberMe() 如果用户被标记为记住我后认证了,则为 true
permitAll() 通常值为 true
principal 用户 pricipal 对象

表 4.2 中的大多数安全表达式扩展对应于表 4.1 中的类似方法。实际上,使用 access() 方法以及 hasRole() 和 permitAll 表达式,可以按如下方式重写 configure()。程序清单 4.9 使用 Spring 表达式定义认证规则

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/design", "/orders")
        	.access("hasRole('ROLE_USER')")
        .antMatchers(/, "/**").access("permitAll");
}

乍一看,这似乎没什么大不了的。毕竟,这些表达式只反映了已经对方法调用所做的工作。但是表达式可以灵活得多。例如,假设(出于某种疯狂的原因)只想允许具有 ROLE_USER 权限的用户在周二(例如,在周二)创建新的 Taco;你可以重写表达式如下:

@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 的有趣的安全约束了。

只需使用 access() 和程序清单 4.9 中的 SpEL 表达式,就可以满足 Taco Cloud 应用程序的授权需求。现在,让我们来看看如何定制登录页面来适应 Taco Cloud 应用程序的外观。

4.3.2 创建用户登录页面

默认的登录页面比您开始时使用的笨拙的 HTTP 基本对话框要好得多,但它仍然相当简单,不太适合 Taco 云应用程序的其余部分。

要替换内置的登录页面,首先需要告诉 Spring Security 自定义登录页面的路径。这可以通过调用传递给 configure() 的 HttpSecurity 对象上的 formLogin() 来实现:

@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() 的调用指定了将提供自定义登录页面的路径。当 Spring Security 确定用户未经身份验证并且需要登录时,它将把用户重定向到此路径。

现在需要提供一个控制器来处理该路径上的请求。因为你的登录页面非常简单 —— 除了一个视图什么都没有 —— 在 WebConfig 中声明它为一个视图控制器是很容易的。下面的 addViewControllers() 方法在将 “/” 映射到主控制器的视图控制器旁边设置登录页面视图控制器:

@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("home");
    registry.addViewController("/login");
}

最后,需要定义 login 页面视图本身,因为使用 Thymeleaf 作为模板引擎,下面的 Thymeleaf 模板应该做得很好:

<!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。但是,这是可配置的。例如,以下配置自定义路径和字段名:

.and()
    .formLogin()
    	.loginPage("/login")
    	.loginProcessingUrl("/authenticate")
    	.usernameParameter("user")
    	.passwordParameter("pwd")

这里,指定 Spring Security 应该监听请求 /authenticate 请求以处理登录提交。此外,用户名和密码字段现在应该命名为 user 和 pwd。

默认情况下,当 Spring Security 确定用户需要登录时,成功的登录将直接将用户带到他们所导航到的页面。如果用户要直接导航到登录页面,成功的登录将把他们带到根路径(例如,主页)。但你可以通过指定一个默认的成功页面来改变:

.and()
    .formLogin()
    	.loginPage("/login")
    	.defaultSuccessUrl("/design")

按照这里的配置,如果用户在直接进入登录页面后成功登录,那么他们将被引导到 /design 页面。

另外,可以强制用户在登录后进入设计页面,即使他们在登录之前已经在其他地方导航,方法是将 true 作为第二个参数传递给 defaultSuccessUrl:

.and()
    .formLogin()
    	.loginPage("/login")
    	.defaultSuccessUrl("/design", true)

现在已经处理了自定义登录页面,让我们来看看身份验证的另一面 —— 如何让用户登出。

4.3.3 登出

与登录应用程序同样重要的是登出。要启用登出功能,只需调用 HttpSecurity 对象上的 logout:

.and()
    .logout()
    	.logoutSuccessUrl("/")

这将设置一个安全筛选器来拦截发送到 /logout 的请求。因此,要提供登出功能,只需在应用程序的视图中添加登出表单和按钮:

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

当用户单击按钮时,他们的 session 将被清除,他们将退出应用程序。默认情况下,它们将被重定向到登录页面,在那里它们可以再次登录。但是,如果希望它们被发送到另一个页面,可以调用 logoutSucessFilter() 来指定一个不同的登出后的登录页面:

.and()
    .logout()
    	.logoutSuccessUrl("/")

在这个例子中,用户在登出后将被跳转到主页。

4.3.4 阻止跨站请求伪造攻击

跨站请求伪造(CSRF)是一种常见的安全攻击。它涉及到让用户在一个恶意设计的 web 页面上编写代码,这个页面会自动(通常是秘密地)代表经常遭受攻击的用户向另一个应用程序提交一个表单。例如,在攻击者的网站上,可能会向用户显示一个表单,该表单会自动向用户银行网站上的一个 URL 发送消息(该网站的设计可能很糟糕,很容易受到这种攻击),以转移资金。用户甚至可能不知道攻击发生了,直到他们注意到他们的帐户中少了钱。

为了防止此类攻击,应用程序可以在显示表单时生成 CSRF token,将该 token 放在隐藏字段中,然后将其存储在服务器上供以后使用。提交表单时,token 将与其他表单数据一起发送回服务器。然后服务器拦截请求,并与最初生成的 token 进行比较。如果 token 匹配,则允许继续执行请求。否则,表单一定是由一个不知道服务器生成的 token 的恶意网站呈现的。

幸运的是,Spring Security 有内置的 CSRF 保护。更幸运的是,它是默认启用的,不需要显式地配置它。只需确保应用程序提交的任何表单都包含一个名为 _csrf 的字段,该字段包含 CSRF token。

Spring Security 甚至可以通过将 CSRF token 放在名为 _csrf 的请求属性中来简化这一过程。因此,可以使用以下代码,在 Thymeleaf 模板的一个隐藏字段中呈现 CSRF token:

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

如果使用 Spring MVC 的 JSP 标签库或带有 Spring 安全方言的 Thymeleaf,那么甚至不需要显式地包含一个隐藏字段,隐藏字段将自动呈现。

在 Thymeleaf 中,只需确保

元素的一个属性被前缀为 Thymeleaf 属性。因为让 Thymeleaf 将路径呈现为上下文相关是很常见的,所以这通常不是问题。例如,Thymeleaf 渲染隐藏字段所需要的仅仅是 th:action 属性:

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

当然也可以禁用 CSRF 支持,但我不太愿意展示如何禁用。CSRF 保护很重要,而且在表单中很容易处理,所以没有理由禁用它,但如果你坚持禁用它,你可以这样调用 disable():

.and()
    .csrf()
    	.disable()

我再次提醒你不要禁用 CSRF 保护,特别是对于生产环境中的应用程序。

所有 web 层安全性现在都配置到 Taco Cloud 了。除此之外,现在有了一个自定义登录页面,并且能够根据 JPA 支持的用户存储库对用户进行身份验证。现在让我们看看如何获取有关登录用户的信息。

上一页
下一页