vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》Spring Security 入门

Spring Security 入门

在本章中,我们将应用一个最小的 Spring Security 配置来解决我们的第一个发现——由于缺乏 URL 保护而导致的无意权限提升,以及来自 第 1 章剖析不安全的应用程序。然后,我们将在基本配置的基础上为我们的用户提供定制的体验。本章旨在让您开始使用 Spring Security,并为您需要执行的任何其他与安全相关的任务提供基础。

在本章的课程中,我们将讨论以下主题:

  • Implementing a basic level of security on the JBCP calendar application, using the automatic configuration option in Spring Security
  • Learning how to customize both the login and logout experience
  • Configuring Spring Security to restrict access differently, depending 
on the URL
  • Leveraging the expression-based access controls of Spring Security
  • Conditionally displaying basic information about the logged-in user using the JSP library in Spring Security
  • Determining the user's default location after login, based on their role

你好春天安全

尽管 Spring Security 可能非常难以配置,但该产品的创建者已经深思熟虑,并为我们提供了一种非常简单的机制,以使该软件的大部分功能具有强大的基线。从这个基线开始,额外的配置将允许对应用程序的安全行为进行精细的详细控制。

我们将从 第 1 章中的不安全日历应用程序开始,剖析不安全的应用程序,并将其变成一个使用基本用户名和密码身份验证保护的站点。此身份验证仅用于说明为我们的 Web 应用程序启用 Spring Security 所涉及的步骤;您会看到这种方法存在一些明显的缺陷,这将导致我们进一步改进配置。

导入示例应用程序

我们鼓励您将 chapter02.00-calendar 项目导入您的 IDE,然后按照JBCP 日历示例代码入门中的说明从本章获取源代码< 附录中的 /em> 部分,其他参考资料< /em>。

对于每一章,您都会发现代表书中检查点的代码的多个修订版。这使您可以轻松地将您的工作与正确的答案进行比较。在每章的开头,我们将导入该章的第一个修订版作为起点。例如,在本章中,我们从 chapter02.00-calendar 开始,第一个检查点将是 chapter02.01-calendar。在第 3 章自定义身份验证中,我们将从 chapter03.00-calendar 开始,第一个检查点将是 chapter03.01-calendarJBCP 日历示例代码入门部分提供了更多详细信息"ch18lvl1sec19">附录附加参考资料,所以请务必参考它以了解详细信息。

更新你的依赖

第一步是更新项目的依赖项以包含必要的 Spring Security JAR 文件。更新 Gradle build.gradle 文件(来自您之前导入的示例应用程序)以包含我们将在以下几节中使用的 Spring Security JAR 文件。

在整本书中,我们将演示如何使用 Gradle 提供所需的依赖项。这 build.gradle 文件位于项目的根目录中,代表构建项目所需的所有内容(包括项目的依赖项)。请记住,Gradle 将为每个列出的依赖项下载传递依赖项。因此,如果您使用另一种机制来管理依赖项,请确保您还包括传递依赖项。手动管理依赖项时,了解 Spring Security 引用包含其传递依赖项的列表很有用。 Spring Security 参考的链接可以在 补充材料部分 补充材料部分 附录其他参考资料

让我们看一下下面的代码片段:

    build.gradle:
    dependencies {
        compile "org.springframework.security:spring-security-  
        config:${springSecurityVersion}"
        compile "org.springframework.security:spring-security- 
        core:${springSecurityVersion}"
        compile "org.springframework.security:spring-security- 
        web:${springSecurityVersion}"
        ...
    }

使用 Spring 4.3 和 Spring Security 4.2

Spring 4.2 一直使用。我们的示例应用程序提供了前一个选项的示例,这意味着您不需要额外的工作。

在下面的代码中,我们展示了添加到 Gradle build.gradle 文件以利用 Gradle 的依赖管理功能的示例片段;这可确保在整个应用程序中使用正确的 Spring 版本。我们将利用 Spring IO bill of materials (BOM) 依赖,这将确保 BOM 导入的所有依赖版本都能正确协同工作:

    build.gradle
    // Spring Security IO with ensures correct Springframework versions
    dependencyManagement {
         imports {
            mavenBom 'io.spring.platform:platform-bom:Brussels-${springIoVersion}'
        }
    }
    dependencies {
        ...
    }
如果您使用的是 Spring Tool Suite,则无论何时更新 build.gradle 文件,确保您右键单击项目并导航到 Gradle | 刷新 Gradle 项目... 并选择 OK 更新所有依赖项。

有关 Gradle 如何处理传递依赖项以及 BOM 的更多信息,请参阅附录补充材料部分中列出的 Gradle 文档>,其他参考资料

实现 Spring Security XML 配置文件

配置过程的下一步是创建一个 Java 配置文件,该文件代表涵盖标准 Web 请求所需的所有 Spring Security 组件。

src/main/java/com/packtpub/springsecurity/configuration/ 目录下新建一个名为 SecurityConfig.java 的 Java 文件,内容如下。除其他外,以下文件演示了我们应用程序中每个页面的用户登录要求,提供登录页面,对用户进行身份验证,并要求登录用户与名为 USER 的角色相关联每个 URL 元素:

    //src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        public void configure(final AuthenticationManagerBuilder auth) throws Exception     
        {
            auth.inMemoryAuthentication().withUser("[email protected]")
            .password("user1").roles("USER");
        }
        @Override
        protected void configure(final HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/**").access("hasRole('USER')")
                    // equivalent to <http auto-config="true">
                    .and().formLogin()
                    .and().httpBasic()
                    .and().logout()
                    // CSRF is enabled by default (will discuss later)
                    .and().csrf().disable();
        }
    }
如果您使用的是 Spring Tool Suite,您可以轻松查看 WebSecurityConfigurerAdapter 使用 F3。请记住,下一个检查点 ( chapter02.01-calendar) 有一个可行的解决方案,因此也可以从那里复制文件。

这是使用最少的标准配置来保护我们的 Web 应用程序所需的唯一 Spring Security 配置。这种配置风格,使用 Spring Security 特定的 Java 配置,称为 Java Config。

让我们花一点时间来分解这个配置,这样我们就可以对正在发生的事情有一个高级的了解。在 configure(HttpSecurity) 方法中,HttpSecurity 对象创建了一个 Servlet 过滤器,它确保当前登录的用户与适当的角色相关联。在这种情况下,过滤器将确保用户与 ROLE_USER 相关联。重要的是要了解角色的名称是任意的。稍后,我们将创建一个具有 ROLE_ADMIN 的用户,并允许该用户访问我们当前用户无权访问的其他 URL。

configure(AuthenticationManagerBuilder) 方法中,AuthenticationManagerBuilder 对象是 Spring Security 对用户进行身份验证的方式。 在这种情况下,我们利用内存数据存储来比较用户名和密码。

我们的例子和对正在发生的事情的解释有点做作。内存中的身份验证存储在生产环境中不起作用。但是,它使我们能够快速启动并运行。随着我们更新应用程序以在本书中使用生产质量安全性,我们将逐步提高对 Spring Security 的理解。

一般支持 Java 配置 在 Spring 3.1 中被添加到 Spring Framework。从 Spring Security 3.2 版本开始,就有了 Spring Security Java Configuration 支持,这使得用户可以在不使用任何 XML 的情况下轻松配置 Spring Security。如果你熟悉 第 6 章LDAP 目录服务,以及 Spring Security 文档,那么您应该会发现它与 安全 Java 配置 支持。

更新您的 web.xml 文件

接下来的步骤涉及对 web.xml 文件的一系列更新。一些步骤已经执行,因为应用程序已经在使用 Spring MVC。但是,如果您在未启用 Spring 的应用程序中使用 Spring Security,我们将讨论这些要求以确保了解更多基本的 Spring 要求。

ContextLoaderListener 类

更新 web.xml 文件的第一步是将其删除并替换为 javax.servlet.ServletContainerInitializer,这是 Servlet 3.0+ 初始化的首选方法。 Spring MVC 提供了 o.s.w.WebApplicationInitializer 接口,它利用了这种机制。在 Spring MVC 中,首选的方法是扩展 o.s.w.servlet.support.AbstractAnnotationConfigDispatcherServletInitializerWebApplicationInitializer 类是多态的 o.s.w.context.AbstractContextLoaderInitializer 并使用抽象的 createRootApplicationContext() 方法来创建根 ApplicationContext ,然后将其委托给 ContextLoaderListener,它在 ServletContext 实例中注册,如下代码片段所示:

    //src/main/java/c/p/s/web/configuration/WebAppInitializer

    public class WebAppInitializer extends   
    AbstractAnnotationConfigDispatcherServletInitializer {
        @Override
        protected Class<?>[] getRootConfigClasses() {
            return new Class[] { JavaConfig.class, SecurityConfig.class,    
            DataSourceConfig.class };
        }
        ...
    }

更新后的配置现在将从 WAR 文件的类路径加载 SecurityConfig.class

ContextLoaderListener 与 DispatcherServlet

o.s.web.servlet.DispatcherServlet 接口指定要使用 getServletConfigClasses() 方法自行加载的配置类:

    //src/main/java/c/p/s/web/configuration/WebAppInitializer

    public class WebAppInitializer extends     
    AbstractAnnotationConfigDispatcherServletInitializer {
        ...
        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[] { WebMvcConfig.class };
        }
        ...
        @Override
        public void onStartup(final ServletContext servletContext) throws  
        ServletException {
            // Registers DispatcherServlet
            super.onStartup(servletContext);
        }
    }

DispatcherServlet 类创建 o.s.context.ApplicationContext,它是根 ApplicationContext 接口的子接口。通常,Spring MVC 特定的组件在 DispatcherServletApplicationContext 接口中初始化,而其余的则由 ContextLoaderListener 加载。重要的是要知道子 ApplicationContext 中的 bean(例如由 DispatcherServlet 创建的那些)可以引用父 ApplicationContext 的 bean(例如ContextLoaderListener 创建的那些)。但是,父 ApplicationContext 接口不能引用子 ApplicationContext 的 bean。

如下图所示,其中 Child Beans 可以引用 Root Beans,但 Root Beans 不能引用 Child Beans :

读书笔记《spring-security-third-edition》Spring Security 入门

与 Spring Security 的大多数用例一样,我们不需要 Spring Security 来引用任何 MVC 声明的 bean。因此,我们决定让 ContextLoaderListener 初始化 Spring Security 的所有配置。

springSecurityFilterChain 过滤器

下一步是配置 springSecurityFilterChain 通过创建 AbstractSecurityWebApplicationInitializer 的实现来拦截所有请求。首先声明 springSecurityFilterChain 至关重要,以确保在调用任何其他逻辑之前请求是安全的。为了确保 springSecurityFilterChain 被首先加载,我们可以使用 @Order(1) ,如以下配置所示:

    //src/main/java/c/p/s/web/configuration/SecurityWebAppInitializer

    @Order(1)
    public class SecurityWebAppInitializer extends     
    AbstractSecurityWebApplicationInitializer {
        public SecurityWebAppInitializer() {
            super();
        }
    }

SecurityWebAppInitializer 类将自动为应用程序中的每个 URL 注册 springSecurityFilterChain 过滤器,并将添加 ContextLoaderListener,它会加载 SecurityConfig

DelegatingFilterProxy 类

o.s.web.filter.DelegatingFilterProxy 类是 Spring Web 提供的一个 Servlet 过滤器,它将所有工作委托给来自 ApplicationContext 根的 Spring bean,它必须实现 javax.servlet.Filter。因为默认情况下,bean 是按名称查找的,使用 ; 值,我们必须确保我们使用 springSecurityFilterChain 作为 的值。 o.s.web.filter.DelegatingFilterProxy 如何为我们的 web.xml 文件工作的伪代码可以在以下代码片段中找到:

    public class DelegatingFilterProxy implements Filter {
      void doFilter(request, response, filterChain) {
        Filter delegate = applicationContet.getBean("springSecurityFilterChain")
        delegate.doFilter(request,response,filterChain);
      }
    }

FilterChainProxy 类

当与 Spring Security 结合使用时,o.s.web.filter.DelegatingFilterProxy 将委托给 Spring Security 的 o.s.web.FilterChainProxy 接口,该接口是在我们最小的 中创建的security.xml 文件。 FilterChainProxy 类允许 Spring Security 有条件地将任意数量的 Servlet 过滤器应用于 servlet 请求。在本书的其余部分,我们将详细了解每个 Spring Security 过滤器,以及它们在确保我们的应用程序得到适当保护方面的作用。 FilterChainProxy 工作原理的伪代码如下:

    public class FilterChainProxy implements Filter {
  void doFilter(request, response, filterChain) {
    // lookup all the Filters for this request
    List<Filter> delegates = 
      lookupDelegates(request,response)
    // invoke each filter unless the delegate decided to stop
    for delegate in delegates {
      if continue processing
        delegate.doFilter(request,response,filterChain)
    }
    // if all the filters decide it is ok allow the 
    // rest of the application to run
    if continue processing
      filterChain.doFilter(request,response)
  }
    }

由于事实上两者 DelegatingFilterProxyFilterChainProxy 是 Spring Security 的前门,当在 Web 应用程序中使用时,您将在尝试找出正在发生的事情时添加一个调试点。

运行安全的应用程序

如果您还没有这样做,请重新启动应用程序并访问 http://localhost:8080/。您将看到以下屏幕:


读书笔记《spring-security-third-edition》Spring Security 入门

很好!我们已经使用 Spring Security 在我们的应用程序中实现了一个基本的安全层。此时,您应该可以使用 [email protected] 作为 Useruser1 作为 密码。您将看到日历欢迎页面,该页面从高层次上描述了应用程序在安全方面的期望。

您的代码现在应该看起来像 chapter02.01-日历

常见问题

许多用户在他们的应用程序中初始实现 Spring Security 时遇到问题。下面列出了一些常见问题和建议。我们希望确保您可以运行示例应用程序并跟随!

  • Make sure you can build and deploy the application before putting Spring Security in place.
  • Review some introductory samples and documentation on your servlet container if needed.
  • It's usually easiest to use an IDE, such as Eclipse, to run your servlet container. Not only is deployment typically seamless, but the console log is also readily available to review for errors. You can also set breakpoints at strategic locations, to be triggered by exceptions to better diagnose errors.
  • Make sure the versions of Spring and Spring Security that you're using match and that there aren't any unexpected Spring JARs remaining as part of your application. As previously mentioned, when using Gradle, it can be a good idea to declare the Spring dependencies in the dependency management section.

一点点润色

在这一点上停下来想想我们刚刚建立了什么。您可能已经注意到一些明显的问题,这些问题需要在我们的应用程序投入生产之前进行一些额外的工作和对 Spring Security 产品的了解。在此安全实施准备好在面向公众的网站上推出之前,尝试列出您认为需要进行的更改。

应用 Hello World Spring Security 实现的速度非常快,它为我们提供了登录页面、用户名和基于密码的身份验证,以及在我们的日历应用程序中自动拦截 URL。但是,自动配置设置提供的内容与我们的最终目标之间存在差距,如下所示:

  • While the login page is helpful, it's completely generic and doesn't look like the rest of our JBCP calendar application. We should add a login form that's integrated with our application's look and feel.
  • There is no obvious way for a user to log out. We've locked down all pages in the application, including the Welcome page, which a potential user may want to browse anonymously. We'll need to redefine the roles required to accommodate anonymous, authenticated, and administrative users.
  • We do not display any contextual information to indicate to the user that they are authenticated. It would be nice to display a greeting similar to welcome [email protected].
  • We've had to hardcode the username, password, and role information of the user in the SecurityConfig configuration file. Recall this section of the configure(AuthenticationManagerBuilder) method we added:
        auth.inMemoryAuthentication().withUser("[email protected]")
        .password("user1").roles("USER");
  • You can see that the username and password are right there in the file. It's unlikely that we'd want to add a new declaration to the file for every user of the system! To address this, we'll need to update the configuration with another type of authentication.

我们将在本书的前半部分探索不同的身份验证选项。

自定义登录

我们已经看到 Spring Security 如何让入门变得非常容易。现在让我们看看如何自定义登录体验。在下面的代码片段中,我们演示了一些更常见的自定义登录方式的用法,但我们建议您参考 Spring Security 的参考文档,其中包括 附录其他参考资料,包含所有受支持的属性。

让我们看一下自定义登录的以下步骤:

  1. First, update your SecurityConfig.java file as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

            http.authorizeRequests()
                ...
                .formLogin()
                   .loginPage("/login/form")
                   .loginProcessingUrl("/login")
                   .failureUrl("/login/form?error")
                   .usernameParameter("username")
                   .passwordParameter("password")
                ....

让我们看一下前面代码片段中描述的以下方法:

  • The loginPage() method specifies where Spring Security will redirect the browser if a protected page is accessed and the user is not authenticated. If a login page is not specified, Spring Security will redirect the user to /spring_security_login. Then o.s.s.web.filter.FilterChainProxy will choose o.s.s.web.authentication.ui.DefaultLoginPageGeneratingFilter, which renders the default login page as one of the delegates, since DefaultLoginPageGeneratingFilter is configured to process /spring_security_login by default. Since we have chosen to override the default URL, we are in charge of rendering the login page when the /login/form URL is requested.
  • The loginProcessingUrl() method defaults to /j_spring_security_check and specifies the URL that the login form (which should include the username and password) should be submitted to, using an HTTP post. When Spring Security processes this request, it will attempt to authenticate the user.
  • The failureUrl() method specifies the page that Spring Security will redirect to if the username and password submitted to loginProcessingUrl() are invalid.
  • The usernameParameter() and passwordParameter() methods default to j_username and j_password respectively, and specify the HTTP parameters that Spring Security will use to authenticate the user when processing the loginProcessingUrl() method.
这可能很明显,但如果我们只想添加自定义登录页面,我们只需要指定 loginPage() 方法。然后,我们将使用其余属性的默认值创建我们的登录表单。但是,覆盖对用户可见的任何内容的值通常是一种好习惯,以防止暴露我们正在使用 Spring Security。揭示我们正在使用的框架是一种信息泄漏,使攻击者更容易确定我们安全中的潜在漏洞。
  1. The next step is to create a login page. We can use any technology we want to render the login page, as long as the login form produces the HTTP request that we specified with our Spring Security configuration when submitted. By ensuring the HTTP request conforms to our configuration, Spring Security can authenticate the request for us. Create the login.html file, as shown in the following code snippet:
        //src/main/webapp/WEB-INF/tempates/login.html

            <div class="container">
        <!--/*/ <th:block th:include="fragments/header :: header"> </th:block> /*/-->
        <form th:action="@{/login}" method="POST" cssClass="form-horizontal">
            <div th:if="${param.error != null}" class="alert alert-danger">
                <strong>Failed to login.</strong>
                <span th:if="${session[SPRING_SECURITY_LAST_EXCEPTION] != null}">
                    <span th:text="${session [SPRING_SECURITY_LAST_EXCEPTION].message}">
                    Invalid credentials</span>
                </span>
            </div>
            <div th:if="${param.logout != null}" class="alert alert-success">You have been logged out.
            </div>
            <label for="username">Username</label>
            <input type="text" id="username" name="username" autofocus="autofocus"/>
            <label for="password">Password</label>
            <input type="password" id="password" name="password"/>
            <div class="form-actions">
                <input id="submit" class="btn" name="submit" type="submit" value="Login"/>
            </div>
        </form>
    </div>
请记住,如果您在书中输入任何内容时遇到问题,可以在下一个检查点(chapter02.02-calendar)参考解决方案。

在前面的 login.html 文件中,以下几项值得强调:

  • The form action should be /login, to match the value provided for the loginProcessingUrl() method we specified. For security reasons, Spring Security only attempts to authenticate when using POST by default.
  • We can use param.error to see whether there was a problem logging in, since the value of our failureUrl() method, /login/form?error, contains the HTTP parameter error.
  • The session attribute, SPRING_SECURITY_LAST_EXCEPTION, contains the last o.s.s.core.AuthenticationException exception, which can be used to display the reason for a failed login. The error messages can be customized by leveraging Spring's internationalization support.
  • The input names for the username and password inputs are chosen to correspond to the values we specified for the usernameParameter() and passwordParameter() methods in our SecurityConfig.java configuration.
  1. The last step is to make Spring MVC aware of our new URL. This can be done by adding the following method to WebMvcConfig:
        //src/main/java/com/packtpub/springsecurity/web/configuration/
        WebMvcConfig.java

        import org.springframework.web.servlet.config.annotation.
        ViewControllerRegistry;
            ...
            public class WebMvcConfig extends WebMvcConfigurationSupport {
              public void addViewControllers(ViewControllerRegistry 
              registry){
                registry.addViewController("/login/form")
                    .setViewName("login");
              }
             ...
            }

配置注销

Spring Security 的 HttpSecurity 配置自动添加了对用户注销的支持。所需要的只是创建一个指向 /j_spring_security_logout 的链接。但是,我们将通过执行以下步骤来演示如何自定义用于注销用户的 URL:

  1. Update the Spring Security configuration as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        http.authorizeRequests()
        ...
       .logout()
       .logoutUrl("/logout")
       .logoutSuccessUrl("/login?logout");
  1. You have to provide a link for the user to click on that will log them out. We will update the header.html file so that the Logout link appears on every page:
        //src/main/webapp/WEB-INF/templates/fragments/header.html

        <div id="navbar" ...>
         ...
           <ul class="nav navbar-nav pull-right">
             <li><a id="navLogoutLink" th:href="@{/logout}">
               Logout</a></li>
           </ul>
            ...
        </div>
  1. The last step is to update the login.html file to display a message indicating logout was successful when the logout parameter is present:
        //src/main/webapp/WEB-INF/templates/login.html

        <div th:if="${param.logout != null}" class="alert alert-success"> You have been logged out.</div>
          <label for="username">Username</label>
          ...
您的代码现在应该看起来像 chapter02.02-日历

页面未正确重定向

如果您还没有,请重新启动应用程序并在 Firefox 中访问 http://localhost:8080;您将看到一个错误,如以下屏幕截图所示:

读书笔记《spring-security-third-edition》Spring Security 入门

什么地方出了错?问题是,由于 Spring Security 不再渲染登录页面,我们必须允许所有人(不仅仅是 USER 角色)访问 Login 页面.如果不授予对 Login 页面的访问权限,则会发生以下情况:

  1. We request the Welcome page in the browser.
  2. Spring Security sees that the Welcome page requires the USER role and that we are not authenticated, so it redirects the browser to the Login page.

  1. The browser requests the Login page.
  2. Spring Security sees that the Login page requires the USER role and that we are still not authenticated, so it redirects the browser to the Login page again.
  3. The browser requests the Login page again.
  4. Spring Security sees that the Login page requires the USER role, as shown in the following diagram:
读书笔记《spring-security-third-edition》Spring Security 入门

这个过程可能会无限期地重复。对我们来说幸运的是,Firefox 意识到发生了太多重定向,停止执行重定向,并显示一个非常有用的错误消息。在下一节中,我们将学习如何通过不同的 URL 配置来修复此错误,具体取决于它们所需的访问权限。

基于角色的基本授权

我们可以从 Hello Spring Security 扩展 Spring Security 配置,以通过 URL 改变访问控制。在本节中,您将找到一种配置,该配置允许更精细地控制资源的访问方式。在配置中,Spring Security 执行以下任务:

  • It completely ignores any request that starts with /resources/. This is beneficial since our images, CSS, and JavaScript do not need to use Spring Security.
  • It allows anonymous users to access the Welcome, Login, and Logout pages.
  • It only allows administrators access to the All Events page.
  • It adds an administrator that can access the All Events page.

看看下面的代码片段:

  //src/main/java/com/packtpub/springsecurity/configuration/
  SecurityConfig.java

    http.authorizeRequests()
        .antMatchers("/resources/**").permitAll()
        .antMatchers("/").hasAnyRole("ANONYMOUS", "USER")
        .antMatchers("/login/*").hasAnyRole("ANONYMOUS", "USER")
        .antMatchers("/logout/*").hasAnyRole("ANONYMOUS", "USER")
        .antMatchers("/admin/*").hasRole("ADMIN")
        .antMatchers("/events/").hasRole("ADMIN")
        .antMatchers("/**").hasRole("USER")
        ...
    @Override
    public void configure(final AuthenticationManagerBuilder auth)
     throws Exception{
        auth.inMemoryAuthentication()
            .withUser("[email protected]").password("user1").roles("USER")
      .and().withUser("[email protected]").password("admin1").
      roles("USER", "ADMIN");
    }
请注意,我们不包括 /calendar,应用程序的上下文根,在 Spring Security 配置中,因为 Spring Security 透明地为我们处理上下文根。这样,如果我们决定将其部署到不同的上下文根,我们就不需要更新我们的配置。

在 Spring Security 4,2 中,您可以使用构建器模式指定多个 RequestMatcher 条目,该构建器模式允许您更好地控制如何将安全性应用于应用程序的不同部分。第一个 antMatchers() 方法声明 Spring Security 应该忽略任何以 /resources/ 开头的 URL,第二个 antMatchers() 方法声明任何其他请求都将由它处理。关于使用多个 antMatchers 方法,有几点需要注意,如下所示:

  • If no path attribute is specified, it is the equivalent of using a path of /**, which matches all requests.
  • Each antMatchers() method is considered in order, and only the first match is applied. So, the order in which they appear in your configuration file is important. The implication is that only the last antMatchers() method can use a path that matches every request. If you do not follow this rule, Spring Security will produce an error. The following is invalid because the first matcher matches every request and will never get to the second mapping:
       http.authorizeRequests()
            .antMatchers("/**").hasRole("USER")
            .antMatchers("/admin/**").hasRole("ADMIN")
  • The default pattern is backed by o.s.s.web.util.AntPathRequestMatcher, which will compare the specified pattern to an ant pattern to determine whether it matches the servletPath and pathInfo methods of HttpServletRequest. Note that query strings are ignored when determining whether a request is a match. Internally, Spring Security uses o.s.u.AntPathMatcher to do all the work. A summary of the rules is listed as follows:
        ? matches a single character.
        * matches zero or more characters, excluding /.
        ** matches zero or more directories in a path.

        The pattern "/events/**" matches "/events", "/events/", 
        "/events/1", and "/events/1/form?test=1"; it does not 
        match "/events123".
        The pattern "/events*" matches "/events", and "/events123"; 
        it does not match "/events/" or "/events/1".
        The pattern "/events*/**" matches "/events", "/events/",  
        "/events/1","/events123", "/events123/456", and 
        "/events/1/form?test=1".
  • The path attribute on the antMatchers() method further refines the filtering of the request and allows access control to be applied. You can see that the updated configuration allows different types of access, depending on the URL pattern. The role ANONYMOUS is of particular interest since we have not defined it anywhere in SecurityConfig.java. This is the default authority assigned to a user that is not logged in. The following line, from the updates to our SecurityConfig.java file, is what allows anonymous (unauthenticated) users and users with the role USER authority to access the Login page. We will cover access control options in more detail in the second half of the book:
        .antMatchers("/login/*").hasAnyRole("ANONYMOUS", "USER")

在定义 antMatchers() 方法时,需要牢记许多事项,包括以下内容:

    • Just as each http method is considered from top to bottom, so are the antMatchers() methods. This means it is important to specify the most specific elements first. The following example illustrates a configuration that does not specify the more specific pattern first, which will result in warnings from Spring Security at startup:
             http.authorizeRequests()
               …
                 // matches every request, so it will not continue
                 .antMatchers("/**").hasRole("USER")
                // below will never match      
                .antMatchers("/login/form").hasAnyRole("ANONYMOUS", "USER")
  • It is important to note that if http.authorizeRequests() is marked anyRequest(), there can be no child antMatchers() method defined. This is because anyRequest() will match all requests that match this http.authorizeRequests() tag. Defining an antMatchers() child method with anyRequest() contradicts the antMatchers() declaration. An example is as follows:
        http.authorizeRequests().anyRequest().permitAll()
            // This matcher will never be executed
            // and not produce an error.
            .antMatchers("/admin/*").hasRole("ADMIN")
  • The path attribute of the antMatchers() element is independent and is not aware of the anyRequest() attribute of the http method.

如果您还没有这样做,请重新启动应用程序并访问 http://localhost:8080。试用该应用程序以查看您所做的所有更新,如下所示:

  1. Select a link that requires authentication and observes the new login page.
  2. Try typing an invalid username/password and view the error message.
  3. Try logging in as an admin ([email protected]/admin1), and view all of the events. Note that we are able to view all the events.

  1. Try logging out and view the logout success message.
  2. Try logging in as a regular user ([email protected]/user1), and view all of the events. Note that we get an Access Denied page.
您的代码现在应该类似于 chapter02.03-calendar

基于表达式的授权

您可能已经注意到,向所有人授予访问权限并不像我们希望的那样简洁。幸运的是,Spring Security 可以利用 Spring Expression Language (SpEL) 来确定用户是否具有授权。在以下代码片段中,您可以看到将 SpEL 与 Spring Security 结合使用时的更新:

    //src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

    http.authorizeRequests()
        .antMatchers("/").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/login/*").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/logout/*").access("hasAnyRole('ANONYMOUS', 'USER')")
        .antMatchers("/admin/*").access("hasRole('ADMIN')")
        .antMatchers("/events/").access("hasRole('ADMIN')")
        .antMatchers("/**").access("hasRole('USER')")
您可能会注意到 /events/ 安全约束是脆弱的。例如, /events URL 不受 Spring Security 保护以限制 ADMIN 角色。这表明需要确保我们提供多层安全性。我们将利用这种弱点 第 11 章细粒度的访问控制

access 属性从 hasAnyRole('ANONYMOUS', 'USER') 更改为 permitAll() 可能看起来不多,但这只是划伤Spring Security 表达能力的表面。我们将在本书的后半部分详细介绍访问控制和 Spring 表达式。继续并通过运行应用程序来验证更新是否有效。

您的代码现在应该看起来像 chapter02.04-日历

有条件地显示认证信息

目前,我们的应用程序没有指示我们是否已登录。事实上,似乎我们总是在登录,因为总是显示 Logout 链接。在本节中,我们将演示如何使用 Thymeleaf 的 Spring Security 标签库显示经过身份验证的用户的用户名并有条件地显示页面的某些部分。我们通过执行以下步骤来做到这一点:

  1. Update your dependencies to include the thymeleaf-extras-springsecurity4 JAR file. Since we are using Gradle, we will add a new dependency declaration in our build.gradle file, as follows:
        //build.gradle

           dependency{
              ...
              compile 'org.thymeleaf.extras:thymeleaf-
              extras-springsecurity4'
         }
  1. Next, we need to add SpringSecurityDialect to the Thymeleaf engine as follows:
        //src/com/packtpub/springsecurity/web/configuration/
        ThymeleafConfig.java

            @Bean
            public SpringTemplateEngine templateEngine(
             final ServletContextTemplateResolver resolver)   
            {
                SpringTemplateEngine engine = new SpringTemplateEngine();
               engine.setTemplateResolver(resolver);
                engine.setAdditionalDialects(new HashSet<IDialect>() {{
                    add(new LayoutDialect());
                    add(new SpringSecurityDialect());
                }});
                return engine;
            }
  1. Update the header.html file to leverage the Spring Security tag library. You can find the updates as follows:
        //src/main/webapp/WEB-INF/templates/fragments/header.html

            <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf- extras-springsecurity4">
            ...
        <div id="navbar" class="collapse navbar-collapse">
            ...
            <ul class="nav navbar-nav pull-right" sec:authorize="isAuthenticated()">
                <li>
                    <p class="navbar-text">Welcome <div class="navbar-text" th:text="${#authentication.name}">User</div></p>
                </li>
                <li>
                    <a id="navLogoutLink" class="btn btn-default" role="button" th:href="@{/logout}">Logout</a>
                </li>
                <li>&nbsp;|&nbsp;</li>
            </ul>
            <ul class="nav navbar-nav pull-right" sec:authorize=" ! isAuthenticated()">
                <li><a id="navLoginLink" class="btn btn-default" role="button" th:href="@{/login/form}">Login</a></li>
                <li>&nbsp;|&nbsp;</li>
            </ul>
            ...

sec:authorize属性决定用户是否通过isAuthenticated()值认证,如果用户通过认证则显示HTML节点,事件中隐藏节点用户未通过身份验证。 access 属性应该与 antMatcher().access() 元素相当熟悉。事实上,这两个组件都利用了相同的 SpEL 支持。 Thymeleaf 标签库中有一些属性不使用表达式。但是,使用 SpEL 通常是首选方法,因为它更强大。

sec:authentication 属性将查找当前的 o.s.s.core.Authentication 对象。 property 属性将找到 o.s.s.core.Authentication 对象的主体属性,在本例中为 o.s.s.core.userdetails.UserDetails。然后它获取 UserDetails 用户名属性并将其呈现到页面。如果这方面的细节令人困惑,请不要担心。我们将在 第 3 章 中更详细地讨论这一点,< em>自定义身份验证。

如果您还没有这样做,请重新启动应用程序以查看我们所做的更新。此时,您可能会意识到我们仍在显示我们无权访问的链接。例如,[email protected] 不应看到指向 所有事件 页面的链接。请放心,当我们在 第 11 章中更详细地介绍标签时,我们会解决这个问题细粒度访问控制

您的代码现在应该如下所示: chapter02.05-日历

登录后自定义行为

我们已经讨论过如何自定义用户登录时的体验,但有时需要自定义登录后的行为。在本节中,我们将讨论 Spring Security 在登录后的行为方式,并将提供一种简单的机制来自定义这种行为。

在默认配置中,Spring Security 在认证成功后有两个不同的流程。如果用户从不访问需要身份验证的资源,则会出现第一种情况。在这种情况下,在成功登录尝试后,用户将被发送到链接到 formLogin() 方法的 defaultSuccessUrl() 方法。如果未定义,defaultSuccessUrl() 将是应用程序的上下文根。

如果用户在通过身份验证之前请求受保护的页面,Spring Security 将使用 o.s.s.web.savedrequest.RequestCache 记住在身份验证之前访问的最后一个受保护页面。身份验证成功后,Spring Security 会将用户发送到身份验证之前访问的最后一个受保护页面。例如,如果未经身份验证的用户请求 My Events 页面,他们将被发送到 Login 页面。

验证成功后,它们将被发送到之前请求的 我的活动 页面。

一个常见的要求是自定义 Spring Security 以根据用户的角色将用户发送到不同的 defaultSuccessUrl() 方法。让我们看一下如何通过执行以下步骤来完成此操作:

  1. The first step is to configure the defaultSuccessUrl() method chained after the formLogin() method. Go ahead and update the security.xml file to use /default instead of the context root:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

          .formLogin()
                      .loginPage("/login/form")
                      .loginProcessingUrl("/login")
                      .failureUrl("/login/form?error")
                      .usernameParameter("username")
                      .passwordParameter("password")
                      .defaultSuccessUrl("/default")
                      .permitAll()
  1. The next step is to create a controller that processes /default. In the following code, you will find a sample Spring MVC controller, DefaultController, which demonstrates how to redirect administrators to the All Events page and other users to the Welcome page. Create a new file in the following location:
        //src/main/java/com/packtpub/springsecurity/web/controllers/
        DefaultController.java

            // imports omitted
            @Controller 
            public class DefaultController {
           @RequestMapping("/default") 
             public String defaultAfterLogin(HttpServletRequest request) { 
                 if (request.isUserInRole("ADMIN")) { 
                     return "redirect:/events/"; 
                 } 
                 return "redirect:/"; 
             }
        }
在 Spring Tool Suite 中,您可以使用 Shift + Ctrl + O 自动添加缺少的导入。

关于 DefaultController 及其工作原理,有几点需要指出。首先是 Spring Security 使 HttpServletRequest 参数知道当前登录的用户。在这种情况下,我们能够检查用户属于哪个角色,而无需依赖任何 Spring Security 的 API。这很好,因为如果 Spring Security 的 API 发生变化,或者我们决定要切换安全实现,我们需要更新的代码就会减少。还应该注意的是,虽然我们使用 Spring MVC 控制器来实现这个控制器,但我们的 defaultSuccessUrl() 方法可以由任何控制器实现来处理(例如,Struts、标准 servlet 等)如果我们愿意。

  1. If you wish to always go to the defaultSuccessUrl() method, you can leverage the second parameter to the defaultSuccessUrl() method, which is a Boolean for always use. We will not do this in our configuration, but you can see an example of it as follows:
        .defaultSuccessUrl("/default", true)
  1. You are now ready to give it a try. Restart the application and go directly to the My Events page, then log in; you will see that you are on the My Events page.
  2. Next, log out and try logging in as [email protected].
  3. You should be on the Welcome page. Log out and log in as [email protected], and you will be
    sent to the All Events page.
您的代码现在应该类似于 chapter02.06-calendar

概括

在本章中,我们应用了一个非常基本的 Spring Security 配置,解释了如何自定义用户的登录和注销体验,并演示了如何在我们的 Web 应用程序中显示基本信息,例如用户名。

在下一章中,我们将讨论 Spring Security 中的身份验证是如何工作的,以及我们如何根据需要对其进行自定义。