vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》记住我服务

记住我服务

在本章中,我们将添加应用程序记住用户的功能,即使他们的会话已过期并且浏览器已关闭。本章将涵盖以下主题:

  • Discussing what remember-me is
  • Learning how to use the token-based remember-me feature
  • Discussing how secure remember-me is, and various ways of making it more secure
  • Enabling the persistent-based remember-me feature, and how to handle additional considerations for using it
  • Presenting the overall remember-me architecture
  • Learning how to create a custom remember-me implementation that is restricted to the user's IP address

什么是记住我?

为网站的频繁用户提供方便的功能是记住我功能。此功能允许用户选择即使在他们的浏览器关闭后也被记住。在 Spring Security 中,这是通过使用存储在用户浏览器中的 remember-me cookie 来实现的。如果 Spring Security 识别出用户正在提供一个记住我的 cookie,那么用户将自动登录到应用程序,并且不需要输入用户名或密码。

什么是cookie?
cookie 是客户端(即网络浏览器)保持状态的一种方式。有关 cookie 的更多信息,请参阅其他在线资源,例如 Wikipedia ( http://en.wikipedia.org/wiki/HTTP_cookie)。

Spring Security 提供了以下两种不同的策略,我们将在本章中讨论:

  • The first is the token-based remember-me feature, which relies on a cryptographic signature
  • The second method, the persistent-based remember-me feature, requires a datastore (a database)

正如我们之前提到的,我们将在本章中更详细地讨论这些策略。必须明确配置记住我功能才能启用它。让我们从尝试基于令牌的记住我功能开始,看看它如何影响登录体验的流程。

依赖项

除了来自 第 2 章Spring Security 入门。但是,如果您正在利用基于持久性的记住我功能,则需要确保在 pom.xml 文件中包含以下附加依赖项。我们已经在本章的示例中包含了这些依赖项,因此无需更新示例应用程序:

    //build.gradle

    dependencies {
    // JPA / ORM / Hibernate:
      compile('org.springframework.boot:spring-boot-starter-data-jpa')
    // H2 RDBMS
      runtime('com.h2database:h2')
       ...
    }

基于令牌的记住我功能

Spring Security 提供了两种不同的 remember-me 功能实现。我们将从探索如何设置基于令牌的记住我服务开始。

配置基于令牌的记住我功能

完成此练习将使我们能够提供一种简单且安全的方法来让用户长时间保持登录状态。首先,请执行以下步骤:

  1. Modify the SecurityConfig.java configuration file and add the rememberMe method.

看看下面的代码片段:

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

        @Override
        protected void configure(HttpSecurity http) throws Exception {
           ...
           http.rememberMe().key("jbcpCalendar")
           ...
        }
你应该从开始 chapter07.00-日历 .
  1. If we try running the application now, we'll see nothing different in the flow. This is because we also need to add a field to the login form that allows the user to opt for this functionality. Edit the login.html file and add a checkbox, as shown in the following code snippet:
        //src/main/resources/templates/login.html

        <input type="password" id="password" name="password"/>
        <label for="remember-me">Remember Me?</label>
        <input type="checkbox" id="remember-me" name="remember_me" value="true"/>
        <div class="form-actions">
           <input id="submit" class="btn" name="submit" type="submit" value="Login"/>
        </div>
您的代码应如下所示 chapter07.01-日历
  1. When we next log in, if the remember-me box is selected, a remember-me cookie is set in the user's browser.

Spring Security 知道它应该通过检查 HTTP 参数 remember_me 来记住用户。

在 Spring Security 3.1 及更早版本中,remember-me 表单字段的默认参数为 spring_security_remember_me。现在,在 Spring Security 4.x 中,默认的 remember-me 表单字段是 记住我。这可以用 rememberMeParameter 方法。
  1. If the user then closes his/her browser and reopens it to an authenticated page on the JBCP calendar website, he/she won't be presented with the login page a second time. Try it yourself now—log in with the remember-me option selected, bookmark the home page, then restart the browser and access the home page. You'll see that you're immediately logged in successfully without needing to supply your login credentials again. If this appears to be happening to you, it means that your browser or a browser plugin is restoring the session.
Try closing the tab first and then close the browser.

一种更有效的解决方案是使用浏览器插件,例如 Firebug (https://addons.mozilla.org/en-US/firefox/addon/firebug/),删除 JSESSIONID cookie。这通常可以在您网站上开发和验证此类功能的过程中节省时间和烦恼。

登录并选择 remember-me 后,您应该会看到已设置两个 cookie,JSESSIONIDremember-me,如以下屏幕截图所示:

读书笔记《spring-security-third-edition》记住我服务

基于令牌的记住我功能如何工作

remember-me 功能在用户浏览器中设置一个 cookie,其中包含 Base64 编码的字符串,其中包含以下部分:

  • The username
  • An expiration date/time
  • An MD5 hash value of the expiration date/time, username, password, and the key attribute of the rememberMe method

这些组合成一个 cookie 值,存储在浏览器中供以后使用。

MD5

MD5 是几种著名的加密哈希算法之一。加密哈希算法计算具有任意长度的输入数据的紧凑且唯一的文本表示,称为摘要。该摘要可用于通过将不受信任输入的摘要与预期输入的已知有效摘要进行比较来确定是否应信任不受信任的输入。

下图说明了这是如何工作的:

读书笔记《spring-security-third-edition》记住我服务

例如,许多开源软件站点允许镜像分发他们的软件以帮助提高下载速度。但是,作为该软件的用户,我们希望确保该软件是真实的并且不包含任何病毒。软件分销商将使用其已知的良好软件版本在其网站上计算并发布预期的 MD5 校验和。然后,我们可以从任何位置下载文件。在我们安装软件之前,我们会计算我们下载的文件上的不受信任的 MD5 校验和。然后我们将不受信任的 MD5 校验和与预期的 MD5 校验和进行比较。如果这两个值匹配,我们知道我们可以安全地安装我们下载的文件。如果这两个值不匹配,我们不应该信任下载的文件并将其删除。

虽然不可能从哈希值中获取原始数据,但 MD5 容易受到多种类型的攻击,包括利用算法本身的弱点和彩虹表攻击。彩虹表通常包含数百万个输入值的预先计算的哈希值。这允许攻击者在彩虹表中查找散列值并确定实际(未散列的)值。 Spring Security 通过在散列值中包含过期日期、用户密码和记住我的键来解决这个问题。

记住我的签名

我们可以看到 MD5 如何确保我们下载了正确的文件,但是这如何应用于 Spring Security 的 remember-me 服务呢?就像我们下载的文件一样,cookie 是不受信任的,但如果我们可以验证源自我们应用程序的签名,我们就可以信任它。当一个请求带有 remember-me cookie 时,它​​的内容被提取出来,并且预期的签名与在 cookie 中找到的签名进行比较。计算预期签名的步骤如下图所示:

读书笔记《spring-security-third-edition》记住我服务

记住我的 cookie 包含 用户名、有效期和签名。 Spring Security 将从 cookie 中提取 username 和 expiration。然后它将利用 cookie 中的 username 来使用 UserDetailsS​​ervice 查找 password。 key 是已知的,因为它是使用 rememberMe 方法提供的。现在所有参数都已知,Spring Security 可以使用 username、expiration、password 和 key 计算预期签名。然后它将预期签名与cookie的签名进行比较。

如果两个签名匹配,我们可以相信 username 和 expiration 日期是有效的。如果不知道记住我的密钥(只有应用程序知道)和用户的密码(只有这个用户知道),伪造 签名 几乎是不可能的。这意味着如果签名匹配并且令牌未过期,则用户可以登录。

您已经预料到,如果用户更改了他们的用户名或密码,任何记住我的令牌集都将不再有效。如果您允许他们更改其帐户的这些位,请确保您向用户提供适当的消息。在本章的后面,我们将看到一个只依赖于用户名而不依赖于密码的另一种记住我的实现。

请注意,仍然可以区分已使用记住我 cookie 进行身份验证的用户和已提供用户名和密码(或等效)凭据的用户。当我们调查记住我功能的安全性时,我们将很快对此进行试验。

基于令牌的记住我的配置指令

通常会进行以下两个配置更改来更改记住我功能的默认行为:

属性

说明

定义生成记住我 cookie 签名时使用的唯一密钥。

tokenValiditySeconds

定义时间长度(以秒为单位)。记住我的 cookie 将被视为对身份验证有效。它还用于设置 cookie 过期时间戳。

正如您从关于如何对 cookie 内容进行散列的讨论中推断的那样,key 属性对于记住我功能的安全性至关重要。确保您选择的密钥对于您的应用程序可能是唯一的,并且足够长,以便不容易被猜到。

牢记本书的目的,我们保持键值相对简单,但是如果您在自己的应用程序中使用记住我,建议您的键包含您的应用程序的唯一名称,并且至少为 36随机字符长。密码生成器工具(在 Google 上搜索“在线密码生成器”)是获得字母数字和特殊字符的伪随机组合来组成您的记住我密钥的好方法。对于存在于多个环境(例如开发、测试和生产)中的应用程序,remember-me cookie 值也应该包含这一事实。这将防止在测试期间在错误的环境中无意中使用记住我的 cookie!

生产应用程序中的示例键值可能类似于以下内容:

    prodJbcpCalendar-rmkey-paLLwApsifs24THosE62scabWow78PEaCh99Jus

tokenValiditySeconds 方法用于设置自动登录功能不接受记住我令牌的秒数,即使它是有效令牌。相同的属性还用于设置记住我 cookie 在用户浏览器上的最长生存期。

remember-me session cookie 的配置
如果 tokenValiditySeconds 设置为 -1,登录cookie会被设置为会话cookie,在用户关闭浏览器后不会持续存在。令牌将在两周的不可配置长度内有效(假设用户没有关闭浏览器)。不要将其与存储用户会话 ID 的 cookie 混淆——它们是具有相似名称的两个不同的东西!

您可能已经注意到我们列出的属性很少。别担心,我们将在本章中花时间介绍其他一些配置属性。

记住我安全吗?

为方便用户而添加的任何与安全相关的功能都有可能使我们精心保护的网站面临安全风险。 “记住我”功能在其默认形式中存在用户 cookie 被恶意用户截获和重用的风险。下图说明了这种情况是如何发生的:

读书笔记《spring-security-third-edition》记住我服务

使用 SSL(在附录其他参考资料中介绍)和其他网络安全技术可以减轻此类攻击,但请注意,有其他技术,例如 跨站点脚本 (XSS),可以窃取或破坏记住的用户会话。虽然对用户来说很方便,但我们不想冒财务或其他个人信息被无意更改或如果记住的会话被滥用而可能被盗的风险。

尽管本书并未详细介绍恶意用户行为,但在实施任何安全系统时,了解可能试图破解您的客户或员工的用户所使用的技术非常重要。 XSS 就是这样一种技术,但还有许多其他技术。强烈建议您查看 OWASP 十大文章 ( http://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project ) 以获得一个很好的列表,还可以选择一本 Web 应用程序安全参考书,其中演示了许多技术,以适用于任何技术。

在便利性和安全性之间保持平衡的一种常见方法是识别站点上可能存在个人或敏感信息的功能位置。然后,您可以使用 fullyAuthenticated 表达式来确保这些位置受到保护,使用的授权不仅检查用户的角色,而且还使用完整的用户名和密码对其进行了身份验证。我们将在下一节中更详细地探讨此功能。

记住我的授权规则

我们稍后将在第11章中全面探讨高级授权技术,< em>细粒度的访问控制,然而,重要的是要认识到可以根据是否记住经过身份验证的会话来区分访问规则。

假设我们希望将尝试访问 H2 admin 控制台的用户限制为已使用用户名和密码进行身份验证的管理员。这类似于在其他主要以消费者为中心的商业网站中发现的行为,这些网站限制对网站高架部分的访问,直到输入密码。请记住,每个站点都是不同的,因此不要盲目地将此类规则应用于您的安全站点。对于我们的示例应用程序,我们将专注于保护 H2 数据库控制台。更新 SecurityConfig.java 文件以使用关键字 fullyAuthenticated,这可确保尝试访问 H2 数据库的记住用户被拒绝访问。这显示在以下代码片段中:

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       ...
       http.authorizeRequests()
           .antMatchers("/admin/*")
           .access("hasRole(ADMIN) and isFullyAuthenticated()")
       ...
       http.rememberMe().key("jbcpCalendar")
    }

现有规则保持不变。我们添加了一条规则,要求帐户信息请求具有适当的 ROLE_ADMINGrantedAuthority,并且用户已完全通过身份验证;也就是说,在此经过身份验证的会话期间,他们实际上已经提供了用户名和密码或其他合适的凭据。请注意此处 SpEL 逻辑运算符的语法 - ANDORNOT 用于 SpEL 中的逻辑运算符。 SpEL 设计者考虑到了这一点,因为 && 运算符在 XML 中表示会很尴尬,即使前面的示例使用基于 Java 的配置!

您的代码应如下所示 chapter07.02-日历

继续并使用用户名 [email protected] 和密码 admin1 登录,确保选择记住我功能。访问 H2 数据库控制台,您将看到已授予访问权限。现在,删除 JSESSIONID cookie(或关闭选项卡,然后关闭所有浏览器实例),并确保仍然授予 All Events 访问权限页。现在,导航到 H2 控制台并观察访问被拒绝。

这种方法通过要求用户出示一整套凭据来访问敏感信息,将记住我功能的可用性增强与额外的安全级别相结合。在本章的其余部分,我们将探索使记住我功能更安全的其他方法。

持久记住我

Spring Security 提供了通过利用 RememberMeServices 接口的不同实现来更改验证记住我 cookie 的方法的能力。在本节中,我们将讨论如何使用数据库使用持久的记住我令牌,以及这如何提高应用程序的安全性。

使用基于持久性的记住我功能

此时修改我们的记住我配置以持久保存到数据库是非常简单的。 Spring Security 配置解析器将识别 rememberMe 方法上的新 tokenRepository 方法,并简单地切换 RememberMeServices 的实现类。现在让我们回顾一下完成此操作所需的步骤。

添加 SQL 以创建记住我的模式

我们已将包含预期架构的 SQL 文件放在 resources 文件夹中,与 第 3 章, 自定义身份验证。您可以在以下代码段中查看架构定义:

    //src/main/resources/schema.sql

    ...
    create table persistent_logins (
       username varchar_ignorecase(100) not null,
       series varchar(64) primary key,
       token varchar(64) not null,
       last_used timestamp not null
    );
    ...

使用 remember-me 模式初始化数据源

如上一节所述,Spring Data 将使用 schema.sql 自动初始化嵌入式数据库。但是请注意,对于 JPA,为了创建模式并使用 data.sql 文件为数据库播种,我们必须确保设置 ddl-auto为无,如下代码所示:

    //src/main/resources/application.yml

    spring:
    jpa:
       database-platform: org.hibernate.dialect.H2Dialect
       hibernate:
         ddl-auto: none

配置基于持久性的记住我功能

最后,我们需要对 rememberMe 声明进行一些简短的配置更改,以将其指向我们正在使用的数据源,如以下代码片段所示:

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

   @Autowired
   @SuppressWarnings("SpringJavaAutowiringInspection")
        private DataSource dataSource;
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       ...
       http.rememberMe()
           .key("jbcpCalendar")
           .tokenRepository(persistentTokenRepository)
       ...
    }
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
       JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
       db.setDataSource(dataSource);
       return db;
    }

这就是我们切换到使用基于持久性的记住我身份验证所需要做的一切。继续并启动应用程序并尝试一下。从用户的角度来看,我们没有注意到任何差异,但我们知道支持此功能的实现已经改变。

您的代码应如下所示 chapter07.03-日历

基于持久性的记住我功能如何工作?

基于持久性的记住我服务验证令牌是否存在于数据库中,而不是验证 cookie 中存在的签名。每个持久记忆我 cookie 包含以下内容:

  • Series identifier: This identifies the initial login of a user and remains consistent each time the user is automatically logged in to the original session
  • Token value: A unique value that changes each time a user is authenticated using the remember-me feature

看看下面的图表:

读书笔记《spring-security-third-edition》记住我服务

当 remember-me cookie 被提交时,Spring Security 将使用 o.s.s.web.authentication.rememberme.PersistentTokenRepository 实现来使用提交的系列标识符查找预期的令牌值和过期时间。然后它将 cookie 中的令牌值与预期的令牌值进行比较。如果令牌未过期且两个令牌匹配,则认为用户已通过身份验证。将生成具有相同系列标识符、新令牌值和更新到期日期的新记住我 cookie。

如果在数据库中找到提交的系列令牌,但令牌不匹配,则可以假设有人窃取了 remember-me cookie。在这种情况下,Spring Security 将终止这一系列记住我的令牌,并警告用户他们的登录已被盗用。

可以在数据库中找到持久化的令牌,并使用 H2 控制台查看,如以下屏幕截图所示:

读书笔记《spring-security-third-edition》记住我服务

基于 JPA 的 PersistentTokenRepository

正如我们在前面的章节中看到的,使用 Spring Data 项目进行数据库映射可以大大简化我们的工作。因此,为了保持一致,我们将使用 JdbcTokenRepositoryImpl 的基于 JDBC 的 PersistentTokenRepository 接口重构为基于 JPA 的接口。我们将通过执行以下步骤来做到这一点:

  1. First, let's create a domain object to hold the persistent logins, as shown in the following code snippet:
        //src/main/java/com/packtpub/springsecurity/domain/
        PersistentLogin.java 

        import org.springframework.security.web.authentication.rememberme.
        PersistentRememberMeToken;
        import javax.persistence.*;
        import java.io.Serializable;
        import java.util.Date;
        @Entity
        @Table(name = "persistent_logins")
        public class PersistentLogin implements Serializable {
           @Id
           private String series;
           private String username;
           private String token;
           private Date lastUsed;
           public PersistentLogin(){}
           public PersistentLogin(PersistentRememberMeToken token){
               this.series = token.getSeries();
               this.username = token.getUsername();
               this.token = token.getTokenValue();
               this.lastUsed = token.getDate();
           }
          ...
  1. Next, we need to create a o.s.d.jpa.repository.JpaRepository repository instance, as shown in the following code snippet:
        //src/main/java/com/packtpub/springsecurity/repository/
        RememberMeTokenRepository.java

        import com.packtpub.springsecurity.domain.PersistentLogin;
        import org.springframework.data.jpa.repository.JpaRepository;
        import java.util.List;
        public interface RememberMeTokenRepository extends  
        JpaRepository<PersistentLogin, String> {
            PersistentLogin findBySeries(String series);
            List<PersistentLogin> findByUsername(String username);
        }
  1. Now, we need to create a custom PersistentTokenRepository interface to replace the Jdbc implementation. We have four methods we must override, but the code should look fairly familiar as we will be using JPA for all of the operations:
         //src/main/java/com/packtpub/springsecurity/web/authentication/
         rememberme/JpaPersistentTokenRepository.java:

         ...
         public class JpaPersistentTokenRepository implements 
         PersistentTokenRepository {
               private RememberMeTokenRepository rememberMeTokenRepository;
               public JpaPersistentTokenRepository
               (RememberMeTokenRepository rmtr) {
                  this.rememberMeTokenRepository = rmtr;
           }
           @Override
           public void createNewToken(PersistentRememberMeToken token) {
               PersistentLogin newToken = new PersistentLogin(token);
               this.rememberMeTokenRepository.save(newToken);
           }
          @Override
          public void updateToken(String series, String tokenValue, 
          Date lastUsed) {
               PersistentLogin token = this.rememberMeTokenRepository
               .findBySeries(series);
               if (token != null) {
                   token.setToken(tokenValue);
                   token.setLastUsed(lastUsed);
                   this.rememberMeTokenRepository.save(token);
               }
           }
        @Override
           public PersistentRememberMeToken 
           getTokenForSeries(String seriesId) {
               PersistentLogin token = this.rememberMeTokenRepository
               .findBySeries(seriesId);
               return new PersistentRememberMeToken(token.getUsername(),
               token.getSeries(), token.getToken(), token.getLastUsed());
           }
           @Override
         public void removeUserTokens(String username) {
             List<PersistentLogin> tokens = this.rememberMeTokenRepository
             .findByUsername(username);
              this.rememberMeTokenRepository.delete(tokens);
           }
        }
  1. Now, we need to make a few changes in the SecurityConfig.java file to declare the new PersistentTokenTokenRepository interface, but the rest of the configuration from the last section does not change, as shown in the following code snippet:
            //src/main/java/com/packtpub/springsecurity/configuration/
            SecurityConfig.java

            //@Autowired
            //@SuppressWarnings("SpringJavaAutowiringInspection")
            //private DataSource dataSource;
            @Autowired
            private PersistentTokenRepository persistentTokenRepository;
            ...
            @Bean
            public PersistentTokenRepository persistentTokenRepository(
               RememberMeTokenRepository rmtr) {
               return new JpaPersistentTokenRepository(rmtr);
            }
  1. This is all we need to do to switch JDBC to JPA persistent-based remember-me authentication. Go ahead and start up the application and give it a try. From a user standpoint, we do not notice any differences, but we know that the implementation backing this feature has changed.
您的代码应如下所示 chapter07.04-日历

自定义 RememberMeServices

到目前为止,我们已经使用了一个相当简单的 PersistentTokenRepository 实现。我们使用了 JDBC 支持和 JPA 支持的实现。这提供了对 cookie 持久性的有限控制;如果我们想要更多控制,我们将 PersistentTokenRepository 接口包装在 RememberMeServices 中。 Barry Jaspan 有一篇关于改进的持久登录 Cookie 最佳实践的精彩文章(http://jaspan.com/改进的_persistent_login_cookie_best_practice)。如前所述,Spring Security 有一个稍微修改过的版本,称为 PersistentTokenBasedRememberMeServices,我们可以将自定义的 PersistentTokenRepository 接口包装在我们的记住我服务中并使用它。

在下一节中,我们将使用 PersistentTokenBasedRememberMeServices 包装现有的 PersistentTokenRepository 接口,并使用 rememberMeServices 方法将其连接到我们的记忆中-我声明:

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

    //@Autowired
    //private PersistentTokenRepository persistentTokenRepository;
    @Autowired
    private RememberMeServices rememberMeServices;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
       ...
       http.rememberMe()
           .key("jbcpCalendar")
           .rememberMeServices(rememberMeServices)
       ...
    }
    @Bean
    public RememberMeServices rememberMeServices
    (PersistentTokenRepository ptr){
       PersistentTokenBasedRememberMeServices rememberMeServices = new 
       PersistentTokenBasedRememberMeServices("jbcpCalendar", 
       userDetailsService, ptr);
       rememberMeServices.setAlwaysRemember(true);
       return rememberMeServices;
    }
您的代码应如下所示 chapter07.05-日历

数据库支持的持久令牌是否更安全?

就像 TokenBasedRememberMeServices 一样,持久性令牌可能会受到 cookie 盗窃或其他中间人技术的破坏。使用 SSL,如 附录其他参考资料 中所述,可以规避中间人技术。如果您使用的是 Servlet 3.0 环境(即 Tomcat 7+),Spring Security 会将 cookie 标记为 HttpOnly,这将有助于在发生 XSS 漏洞时防止 cookie 被盗在应用程序中。要了解有关 HttpOnly 属性的更多信息,请参阅本章前面提供的有关 cookie 的外部资源。

使用基于持久性的记住我功能的优点之一是我们可以检测 cookie 是否被泄露。如果出现了正确的系列令牌和不正确的令牌,我们知道任何使用该系列令牌的记住我功能都应该被视为受到损害,我们应该终止与之关联的任何会话。由于验证是有状态的,我们也可以终止特定的记住我功能,而无需更改用户的密码。

清理过期的记住我会话

使用基于持久性的记住我功能的缺点是没有内置支持来清理过期会话。为此,我们需要实现一个清理过期会话的后台进程。我们在本章的示例代码中包含了代码来执行清理。

为简洁起见,我们在下面的代码片段中显示了一个不进行验证或错误处理的版本。您可以在本章的示例代码中查看完整版:

    //src/main/java/com/packtpub/springsecurity/web/authentication/rememberme/
    JpaTokenRepositoryCleaner.java

    public class JpaTokenRepositoryImplCleaner
    implements Runnable {
       private final RememberMeTokenRepository repository;
       private final long tokenValidityInMs;
       public JpaTokenRepositoryImplCleaner(RememberMeTokenRepository 
       repository, long tokenValidityInMs) {
           if (rememberMeTokenRepository == null) {
               throw new IllegalArgumentException("jdbcOperations cannot 
               be null");
           }
           if (tokenValidityInMs < 1) {
               throw new IllegalArgumentException("tokenValidityInMs 
               must be greater than 0. Got " + tokenValidityInMs);
           }
           this. repository = repository;
           this.tokenValidityInMs = tokenValidityInMs;
       }
           public void run() {
           long expiredInMs = System.currentTimeMillis() 
           - tokenValidityInMs;             
              try {
               Iterable<PersistentLogin> expired = 
               rememberMeTokenRepository
               .findByLastUsedAfter(new Date(expiredInMs));
               for(PersistentLogin pl: expired){
                   rememberMeTokenRepository.delete(pl);
               }
           } catch(Throwable t) {...}
       }
    }

本章的示例代码还包括一个简单的 Spring 配置,它将每十分钟执行一次清理程序。如果您不熟悉 Spring 的任务抽象并想学习它,那么您可能想在 https://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html。您可以在以下代码段中找到相关配置。为清楚起见,我们将此调度程序放在 JavaConfig.java 文件中:

    //src/main/java/com/packtpub/springsecurity/configuration/
    JavaConfig.java@Configuration

    @Import({SecurityConfig.class})
    @EnableScheduling
    public class JavaConfig {
        @Autowired
       private RememberMeTokenRepository rememberMeTokenRepository;
       @Scheduled(fixedRate = 10_000)
       public void tokenRepositoryCleaner(){
           Thread trct = new Thread(new JpaTokenRepositoryCleaner(
           rememberMeTokenRepository, 60_000L));
           trct.start();
       }
    }
请记住,此配置不支持集群。因此,如果将其部署到集群,清理程序将为应用程序部署到的每个 JVM 执行一次。

启动应用程序并尝试更新。提供的配置将确保清理程序每十分钟执行一次。您可能希望更改清理任务以更频繁地运行并通过修改 @Scheduled 声明来清理最近使用的记住我标记。然后,您可以创建一些记住我的令牌,并通过在 H2 数据库控制台中查询它们来查看它们是否被删除。

您的代码应如下所示 chapter07.06-日历

记住我的架构

我们已经介绍了 TokenBasedRememberMeServicesPersistentTokenBasedRememberMeServices 的基本架构,但是我们没有描述整体架构。让我们看看所有记住我的片段是如何组合在一起的。

下图说明了验证基于令牌的记住我令牌的过程中涉及的不同组件:

读书笔记《spring-security-third-edition》记住我服务

与任何 Spring Security 过滤器一样,RememberMeAuthenticationFilter 是从 FilterChainProxy 中调用的。 RememberMeAuthenticationFilter 的工作是检查请求,如果感兴趣,则采取行动。 RememberMeAuthenticationFilter 接口将使用 RememberMeServices 实现来确定用户是否已经登录。RememberMeServices 接口通过检查 HTTP 请求来执行此操作一个记住我的 cookie,然后使用我们之前讨论的基于令牌的验证或基于持久性的验证来验证。如果令牌签出,则用户将登录。

记住我和用户生命周期

RememberMeServices 的实现在用户生命周期(经过身份验证的用户会话的生命周期)的几个点被调用。为了帮助您理解记住我功能,了解记住我服务被告知生命周期功能的时间点会很有帮助:

动作

会发生什么?

调用的 RememberMeServices 方法

登录成功

实现设置一个记住我的cookie(如果form参数已经发送)

登录成功

登录失败

实施应该取消 cookie,如果它存在的话

登录失败

用户注销

实施应该取消 cookie,如果它存在的话

注销

logout 方法不存在于 RememberMeServices 接口。相反,每个 RememberMeServices 实现还实现了 LogoutHandler 接口,其中包含 注销方法。通过实施 LogoutHandler 接口,每个 RememberMeServices 实现可以在用户注销时执行必要的清理。

当我们开始创建自定义身份验证处理程序时,了解 RememberMeServices 在哪里以及如何与用户的生命周期联系非常重要,因为我们需要确保任何身份验证处理程序都始终如一地对待 RememberMeServices保留此功能的有用性和安全性。

将记住我功能限制为 IP 地址

让我们使用对记住我架构的理解。一个常见的要求是任何记住我的令牌都应该与创建它的用户的 IP 地址相关联。这为记住我功能增加了额外的安全性。为此,我们只需要实现一个自定义的 PersistentTokenRepository 接口。我们将进行的配置更改将说明如何配置自定义 RememberMeServices。在本节中,我们将查看本章源代码中包含的 IpAwarePersistentTokenRepositoryIpAwarePersistenTokenRepository 接口保证系列标识符在内部与当前用户的IP地址结合,而系列标识符在外部只包含标识符。这意味着无论何时查找或保存令牌,当前 IP 地址都用于查找或保存令牌。在以下代码片段中,您可以看到 IpAwarePersistentTokenRepository 的工作原理。如果您想深入挖掘,我们鼓励您查看本章包含的源代码。

查找 IP 地址的技巧是使用 Spring Security 的 RequestContextHolder。相关代码如下:

需要注意的是,为了使用 RequestContextHolder,你需要确保你已经设置了你的 web.xml 文件使用 RequestContextListener。我们已经为我们的示例代码执行了此设置。但是,当在外部应用程序中使用示例代码时,这可能很有用。参考Javadoc IpAwarePersistentTokenRepository 了解如何设置的详细信息。

看看下面的代码片段:

    //src/main/java/com/packtpub/springsecurity/web/authentication/rememberme/
    IpAwarePersistentTokenRepository.java

    private String ipSeries(String series) {
    ServletRequestAttributes attributes = (ServletRequestAttributes)
    RequestContextHolder.getRequestAttributes();
    return series + attributes.getRequest().getRemoteAddr();
    }

我们可以在此方法的基础上强制保存的令牌在系列标识符中包含 IP 地址,如下所示:

    public void createNewToken(PersistentRememberMeToken token) {
      String ipSeries = ipSeries(token.getSeries());
      PersistentRememberMeToken ipToken = tokenWithSeries(token, ipSeries);
      this.delegateRepository.createNewToken(ipToken);
    }

您可以看到我们首先创建了一个新系列,并将 IP 地址连接到它上面。 tokenWithSeries 方法只是一个帮助器,它创建一个具有所有相同值的新标记,除了一个新系列。然后,我们将带有包含 IP 地址的系列标识符的新令牌提交给 delegateRepsository,这是 PersistentTokenRepository 的原始实现。

每当查找令牌时,我们都要求将当前用户的 IP 地址附加到系列标识符中。这意味着用户无法为具有不同 IP 地址的用户获取令牌:

    public PersistentRememberMeToken getTokenForSeries(String seriesId) {
       String ipSeries = ipSeries(seriesId);
       PersistentRememberMeToken ipToken = delegateRepository.
       getTokenForSeries(ipSeries);
       return tokenWithSeries(ipToken, seriesId);
    }

代码的其余部分非常相似。在内部,我们构造要附加到 IP 地址的系列标识符,而在外部,我们只显示原始系列标识符。通过这样做,我们强制执行只有创建记住我令牌的用户才能使用它的约束。

让我们回顾一下本章的 IpAwarePersistentTokenRepository 示例代码中包含的 Spring 配置。在以下代码片段中,我们首先创建包装新 JpaPersistentTokenRepository 声明的 IpAwarePersistentTokenRepository 声明。然后我们通过实例化一个 OrderedRequestContextFilter 接口来初始化一个 RequestContextFilter 类:

    //src/main/java/com/packtpub/springsecurity/web/configuration/WebMvcConfig.java

    @Bean
    public IpAwarePersistentTokenRepository 
    tokenRepository(RememberMeTokenRepository rmtr) {
       return new IpAwarePersistentTokenRepository(
               new JpaPersistentTokenRepository(rmtr)
       );
    }
    @Bean
    public OrderedRequestContextFilter requestContextFilter() {
       return new OrderedRequestContextFilter();
    }

为了让 Spring Security 使用我们自定义的 RememberMeServices,我们需要更新我们的安全配置以指向它。继续对 SecurityConfig.java 进行以下更新:

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

     @Override
     protected void configure(HttpSecurity http) throws Exception {
       ...
       // remember me configuration
      http.rememberMe()
           .key("jbcpCalendar")
           .rememberMeServices(rememberMeServices);
     }
    @Bean
    public RememberMeServices rememberMeServices
    (PersistentTokenRepository ptr){
       PersistentTokenBasedRememberMeServices rememberMeServices = new 
       PersistentTokenBasedRememberMeServices("jbcpCalendar", 
       userDetailsService, ptr);
       return rememberMeServices;
    }

现在,继续并启动应用程序。您可以使用第二台计算机和 Firebug 等插件来操作您的记住我的 cookie。如果您尝试在另一台计算机上使用来自一台计算机的 remember-me cookie,Spring Security 现在将忽略 remember-me 请求并删除关联的 cookie。

您的代码应如下所示 chapter07.07-日历

请注意,如果用户位于共享或负载平衡的网络基础架构(例如多 WAN 公司环境)后面,则基于 IP 的记住我令牌可能会出现意外行为。然而,在大多数情况下,向记住我功能添加 IP 地址为有用的用户功能提供了额外的、受欢迎的安全层。

自定义 cookie 和 HTTP 参数名称

好奇的用户可能想知道,remember-me 表单字段复选框的预期值是否可以更改为 remember-me,或者 cookie 名称为 remember-me,是否可以更改以掩盖 Spring Security 的使用。可以在两个位置之一进行此更改。请看以下步骤:

  1. First, we can add additional methods to the rememberMe method, as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        http.rememberMe()
               .key("jbcpCalendar")
               .rememberMeParameter("jbcpCalendar-remember-me")
               .rememberMeCookieName("jbcpCalendar-remember-me");
  1. Additionally, now that we've declared our own RememberMeServices implementation as a Spring bean, we can simply define more properties to change the checkbox and cookie names, as follows:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Bean
        public RememberMeServices rememberMeServices
        (PersistentTokenRepository ptr){
           PersistentTokenBasedRememberMeServices rememberMeServices = new 
           PersistentTokenBasedRememberMeServices("jbcpCalendar", 
           userDetailsService, ptr);
           rememberMeServices.setParameter("obscure-remember-me");
           rememberMeServices.setCookieName("obscure-remember-me");
           return rememberMeServices;
        }
  1. Don't forget to change the login.html page to set the name of the checkbox form field and to match the parameter value we declared. Go ahead and make the updates to login.html, as follows:
        //src/main/resources/templates/login.html

        <input type="checkbox" id="remember" name=" obscure-remember-me" value="true"/>
  1. We'd encourage you to experiment here to ensure you understand how these settings are related. Go ahead and start up the application and give it a try.
您的代码应如下所示 chapter07.08-日历

概括

本章解释并演示了 Spring Security 中记住我功能的使用。我们从最基本的设置开始,并学习了如何逐渐使该功能更加安全。具体来说,我们了解了基于令牌的记住我服务以及如何配置它。我们还探讨了基于持久性的记住我服务如何提供额外的安全性、它是如何工作的,以及使用它们时需要考虑的额外注意事项。

我们还介绍了创建自定义记住我的实现,它将记住我的令牌限制为特定的 IP 地址。我们看到了使记住我功能更安全的各种其他方法。

接下来是基于证书的身份验证,我们将讨论如何使用受信任的客户端证书来执行身份验证。