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 SecuritySpEL的扩展

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监听登录请求,并期望用户名和密码字段命名为usernamepassword。但是,这是可配置的。例如,以下配置自定义路径和字段名:

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

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

默认情况下,当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 MVCJSP标签库或带有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支持的用户存储库对用户进行身份验证。现在让我们看看如何获取有关登录用户的信息。

上一页
下一页