Remember-Me 功能

概述

Remember-Me 是指网站能够在 Session 之间记住登录用户的身份,具体来说就是我成功认证一次之后在一定的时间内我可以不用再输入用户名和密码进行登录了,系统会自动给我登录。这通常是通过服务端发送一个 cookie 给客户端浏览器,下次浏览器再访问服务端时服务端能够自动检测客户端的 cookie,根据 cookie 值触发自动登录操作。Spring Security 为这些操作的发生提供必要的钩子,并且针对于 Remember-Me 功能有两种实现。一种是简单的使用加密来保证基于 cookie 的 token 的安全,另一种是通过数据库或其它持久化存储机制来保存生成的 token。

需要注意的是两种实现都需要一个 UserDetailsService。如果你使用的 AuthenticationProvider 不使用 UserDetailsService,那么记住我将会不起作用,除非在你的 ApplicationContext 中拥有一个 UserDetailsService 类型的 bean。

基于简单加密 token 的方法

当用户选择了记住我成功登录后,Spring Security 将会生成一个 cookie 发送给客户端浏览器。cookie 值由如下方式组成:

base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key))

  • username:登录的用户名。
  • password:登录的密码。
  • expirationTime:token 失效的日期和时间,以毫秒表示。
  • key:用来防止修改 token 的一个 key。

这样用来实现 Remember-Me 功能的 token 只能在指定的时间内有效,且必须保证 token 中所包含的 username、password 和 key 没有被改变才行。需要注意的是,这样做其实是存在安全隐患的,那就是在用户获取到实现记住我功能的 token 后,任何用户都可以在该 token 过期之前通过该 token 进行自动登录。如果用户发现自己的 token 被盗用了,那么他可以通过改变自己的登录密码来立即使其所有的记住我 token 失效。如果希望我们的应用能够更安全一点,可以使用接下来要介绍的持久化 token 方式,或者不使用 Remember-Me 功能,因为 Remember-Me 功能总是有点不安全的。

使用这种方式时,我们只需要在 http 元素下定义一个 remember-me 元素,同时指定其 key 属性即可。key 属性是用来标记存放 token 的 cookie 的,对应上文提到的生成 token 时的那个 key。

   <security:http auto-config="true">
<security:form-login/>
<!-- 定义记住我功能 -->
<security:remember-me key="elim"/>
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>

这里有两个需要注意的地方。第一,如果你的登录页面是自定义的,那么需要在登录页面上新增一个名为 “_spring_security_remember_me” 的 checkbox,这是基于 NameSpace 定义提供的默认名称,如果要自定义可以自己定义 TokenBasedRememberMeServices 或 PersistentTokenBasedRememberMeServices 对应的 bean,然后通过其 parameter 属性进行指定,具体操作请参考后文关于《Remember-Me 相关接口和实现类》部分内容。第二,上述功能需要一个 UserDetailsService,如果在你的 ApplicationContext 中已经拥有一个了,那么 Spring Security 将自动获取;如果没有,那么当然你需要定义一个;如果拥有在 ApplicationContext 中拥有多个 UserDetailsService 定义,那么你需要通过 remember-me 元素的 user-service-ref 属性指定将要使用的那个。如:

   <security:http auto-config="true">
<security:form-login/>
<!-- 定义记住我功能,通过 user-service-ref 指定将要使用的 UserDetailsService-->
<security:remember-me key="elim" user-service-ref="userDetailsService"/>
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http> <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

基于持久化 token 的方法

持久化 token 的方法跟简单加密 token 的方法在实现 Remember-Me 功能上大体相同,都是在用户选择了 “记住我” 成功登录后,将生成的 token 存入 cookie 中并发送到客户端浏览器,待到下次用户访问系统时,系统将直接从客户端 cookie 中读取 token 进行认证。所不同的是基于简单加密 token 的方法,一旦用户登录成功后,生成的 token 将在客户端保存一段时间,如果用户不点击退出登录,或者不修改密码,那么在 cookie 失效之前,他都可以使用该 token 进行登录,哪怕该 token 被别人盗用了,用户与盗用者都同样可以进行登录。而基于持久化 token 的方法采用这样的实现逻辑:

  1. 用户选择了 “记住我” 成功登录后,将会把 username、随机产生的序列号、生成的 token 存入一个数据库表中,同时将它们的组合生成一个 cookie 发送给客户端浏览器。
  2. 当下一次没有登录的用户访问系统时,首先检查 cookie,如果对应 cookie 中包含的 username、序列号和 token 与数据库中保存的一致,则表示其通过验证,系统将重新生成一个新的 token 替换数据库中对应组合的旧 token,序列号保持不变,同时删除旧的 cookie,重新生成包含新生成的 token,就的序列号和 username 的 cookie 发送给客户端。
  3. 如果检查 cookie 时,cookie 中包含的 username 和序列号跟数据库中保存的匹配,但是 token 不匹配。这种情况极有可能是因为你的 cookie 被人盗用了,由于盗用者使用你原本通过认证的 cookie 进行登录了导致旧的 token 失效,而产生了新的 token。这个时候 Spring Security 就可以发现 cookie 被盗用的情况,它将删除数据库中与当前用户相关的所有 token 记录,这样盗用者使用原有的 cookie 将不能再登录,同时提醒用户其帐号有被盗用的可能性。
  4. 如果对应 cookie 不存在,或者包含的 username 和序列号与数据库中保存的不一致,那么将会引导用户到登录页面。

从以上逻辑我们可以看出持久化 token 的方法比简单加密 token 的方法更安全,因为一旦你的 cookie 被人盗用了,你只要再利用原有的 cookie 试图自动登录一次,原有的 token 将失效导致盗用者不能再使用原来盗用的 cookie 进行登录了,同时用户可以发现自己的 cookie 有被盗用的可能性。但因为 cookie 被盗用后盗用者还可以在用户下一次登录前顺利的进行登录,所以如果你的应用对安全性要求比较高就不要使用 Remember-Me 功能了。

使用持久化 token 方法时需要我们的数据库中拥有如下表及其表结构。

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

然后还是通过 remember-me 元素来使用,只是这个时候我们需要其 data-source-ref 属性指定对应的数据源,同时别忘了它也同样需要 ApplicationContext 中拥有 UserDetailsService,如果拥有多个,请使用 user-service-ref 属性指定 remember-me 使用的是哪一个。

<security:http auto-config="true">
<security:form-login/>
<!-- 定义记住我功能 -->
<security:remember-me data-source-ref="dataSource"/>
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>

Remember-Me 相关接口和实现类

在上述介绍中,我们实现 Remember-Me 功能是通过 Spring Security 为了简化 Remember-Me 而提供的 NameSpace 进行定义的。而底层实际上还是通过 RememberMeServices、UsernamePasswordAuthenticationFilter 和 RememberMeAuthenticationFilter 的协作来完成的。RememberMeServices 是 Spring Security 为 Remember-Me 提供的一个服务接口,其定义如下。

publicinterface RememberMeServices {
/**
* 自动登录。在实现这个方法的时候应该判断用户提供的 Remember-Me cookie 是否有效,如果无效,应当直接忽略。
* 如果认证成功应当返回一个 AuthenticationToken,推荐返回 RememberMeAuthenticationToken;
* 如果认证不成功应当返回 null。
*/
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
/**
* 在用户登录失败时调用。实现者应当做一些类似于删除 cookie 之类的处理。
*/
void loginFail(HttpServletRequest request, HttpServletResponse response);
/**
* 在用户成功登录后调用。实现者可以在这里判断用户是否选择了 “Remember-Me” 登录,然后做相应的处理。
*/
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}

UsernamePasswordAuthenticationFilter 拥有一个 RememberMeServices 的引用,默认是一个空实现的 NullRememberMeServices,而实际当我们通过 remember-me 定义启用 Remember-Me 时,它会是一个具体的实现。用户的请求会先通过 UsernamePasswordAuthenticationFilter,如认证成功会调用 RememberMeServices 的 loginSuccess() 方法,否则调用 RememberMeServices 的 loginFail() 方法。UsernamePasswordAuthenticationFilter 是不会调用 RememberMeServices 的 autoLogin() 方法进行自动登录的。之后运行到 RememberMeAuthenticationFilter 时如果检测到还没有登录,那么 RememberMeAuthenticationFilter 会尝试着调用所包含的 RememberMeServices 的 autoLogin() 方法进行自动登录。关于 RememberMeServices Spring Security 已经为我们提供了两种实现,分别对应于前文提到的基于简单加密 token 和基于持久化 token 的方法。

TokenBasedRememberMeServices

TokenBasedRememberMeServices 对应于前文介绍的使用 namespace 时基于简单加密 token 的实现。TokenBasedRememberMeServices 会在用户选择了记住我成功登录后,生成一个包含 token 信息的 cookie 发送到客户端;如果用户登录失败则会删除客户端保存的实现 Remember-Me 的 cookie。需要自动登录时,它会判断 cookie 中所包含的关于 Remember-Me 的信息是否与系统一致,一致则返回一个 RememberMeAuthenticationToken 供 RememberMeAuthenticationProvider 处理,不一致则会删除客户端的 Remember-Me cookie。TokenBasedRememberMeServices 还实现了 Spring Security 的 LogoutHandler 接口,所以它可以在用户退出登录时立即清除 Remember-Me cookie。

如果把使用 namespace 定义 Remember-Me 改为直接定义 RememberMeServices 和对应的 Filter 来使用的话,那么我们可以如下定义。

   <security:http>
<security:form-login login-page="/login.jsp"/>
<security:intercept-url pattern="/login*.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!-- 把 usernamePasswordAuthenticationFilter 加入 FilterChain -->
<security:custom-filter ref="usernamePasswordAuthenticationFilter" before="FORM_LOGIN_FILTER"/>
<security:custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>
</security:http>
<!-- 用于认证的 AuthenticationManager -->
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
user-service-ref="userDetailsService"/>
<security:authentication-provider ref="rememberMeAuthenticationProvider"/>
</security:authentication-manager> <bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean> <bean id="usernamePasswordAuthenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="authenticationManager"/>
<!-- 指定 request 中包含的用户名对应的参数名 -->
<property name="usernameParameter" value="username"/>
<property name="passwordParameter" value="password"/>
<!-- 指定登录的提交地址 -->
<property name="filterProcessesUrl" value="/login.do"/>
</bean>
<!-- Remember-Me 对应的 Filter -->
<bean id="rememberMeFilter"
class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices" />
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<!-- RememberMeServices 的实现 -->
<bean id="rememberMeServices"
class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="userDetailsService" />
<property name="key" value="elim" />
<!-- 指定 request 中包含的用户是否选择了记住我的参数名 -->
<property name="parameter" value="rememberMe"/>
</bean>
<!-- key 值需与对应的 RememberMeServices 保持一致 -->
<bean id="rememberMeAuthenticationProvider"
class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="elim" />
</bean>

需要注意的是 RememberMeAuthenticationProvider 在认证 RememberMeAuthenticationToken 的时候是比较它们拥有的 key 是否相等,而 RememberMeAuthenticationToken 的 key 是 TokenBasedRememberMeServices 提供的,所以在使用时需要保证 RememberMeAuthenticationProvider 和 TokenBasedRememberMeServices 的 key 属性值保持一致。需要配置 UsernamePasswordAuthenticationFilter 的 rememberMeServices 为我们定义好的 TokenBasedRememberMeServices,把 RememberMeAuthenticationProvider 加入 AuthenticationManager 的 providers 列表,并添加 RememberMeAuthenticationFilter 和 UsernamePasswordAuthenticationFilter 到 FilterChainProxy。

PersistentTokenBasedRememberMeServices

PersistentTokenBasedRememberMeServices 是 RememberMeServices 基于前文提到的持久化 token 的方式实现的。具体实现逻辑跟前文介绍的以 NameSpace 的方式使用基于持久化 token 的 Remember-Me 是一样的,这里就不再赘述了。此外,如果单独使用,其使用方式和上文描述的 TokenBasedRememberMeServices 是一样的,这里也不再赘述了。

需要注意的是 PersistentTokenBasedRememberMeServices 是需要将 token 进行持久化的,所以我们必须为其指定存储 token 的 PersistentTokenRepository。Spring Security 对此有两种实现,InMemoryTokenRepositoryImpl 和 JdbcTokenRepositoryImpl。前者是将 token 存放在内存中的,通常用于测试,而后者是将 token 存放在数据库中。PersistentTokenBasedRememberMeServices 默认使用的是前者,我们可以通过其 tokenRepository 属性来指定使用的 PersistentTokenRepository。

使用 JdbcTokenRepositoryImpl 时我们可以使用在前文提到的默认表结构。如果需要使用自定义的表,那么我们可以对 JdbcTokenRepositoryImpl 进行重写。定义 JdbcTokenRepositoryImpl 时需要指定一个数据源 dataSource,同时可以通过设置参数 createTableOnStartup 的值来控制是否要在系统启动时创建对应的存入 token 的表,默认创建语句为 “create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)”,但是如果自动创建时对应的表已经存在于数据库中,则会抛出异常。createTableOnStartup 属性默认为 false。

直接显示地使用 PersistentTokenBasedRememberMeServices 和上文提到的直接显示地使用 TokenBasedRememberMeServices 的方式是一样的,我们只需要将上文提到的配置中 RememberMeServices 实现类 TokenBasedRememberMeServices 换成 PersistentTokenBasedRememberMeServices 即可。

   <!-- RememberMeServices 的实现 -->
<bean id="rememberMeServices"
class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
<property name="userDetailsService" ref="userDetailsService" />
<property name="key" value="elim" />
<!-- 指定 request 中包含的用户是否选择了记住我的参数名 -->
<property name="parameter" value="rememberMe"/>
<!-- 指定 PersistentTokenRepository -->
<property name="tokenRepository">
<bean class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 是否在系统启动时创建持久化 token 的数据库表 -->
<property name="createTableOnStartup" value="false"/>
</bean>
</property>
</bean>

Spring Security 入门(1-3-5)Spring Security - remember me!的更多相关文章

  1. Spring Security 入门(1-6-1)Spring Security - 配置文件解析和访问请求处理

    1.在pom.xml中添加maven坐标 <dependency> <groupId>org.springframework.security</groupId> ...

  2. Spring Security 入门(1-7)Spring Security - Session管理

    参考链接:https://xueliang.org/article/detail/20170302232815082 session 管理 Spring Security 通过 http 元素下的子元 ...

  3. Spring Security 入门(1-6-2)Spring Security - 内置的filter顺序、自定义filter、http元素和对应的filterChain

    Spring Security 的底层是通过一系列的 Filter 来管理的,每个 Filter 都有其自身的功能,而且各个 Filter 在功能上还有关联关系,所以它们的顺序也是非常重要的. 1.S ...

  4. Spring Security 入门(1-3-2)Spring Security - http元素 - intercept-url配置

    http元素下可以配置登录页面,也可以配置 url 拦截. 1.直接配置拦截url和对应的访问权限 <security:http use-expressions="false" ...

  5. Spring Security 入门(1-4-2)Spring Security - 认证过程之AuthenticationProvider的扩展补充说明

    1.用户信息从数据库获取 通常我们的用户信息都不会向第一节示例中那样简单的写在配置文件中,而是从其它存储位置获取,比如数据库.根据之前的介绍我们知道用户信息是通过 UserDetailsService ...

  6. Spring Security 入门(1-3-1)Spring Security - http元素 - 默认登录和登录定制

    登录表单配置 - http 元素下的 form-login 元素是用来定义表单登录信息的.当我们什么属性都不指定的时候 Spring Security 会为我们生成一个默认的登录页面. 如果不想使用默 ...

  7. Spring Security 入门(1-2)Spring Security - 从 配置例子例子 开始我们的学习历程

    1.Spring Security 的配置文件 我们需要为 Spring Security 专门建立一个 Spring 的配置文件,该文件就专门用来作为 Spring Security 的配置. &l ...

  8. Spring Security入门(2-3)Spring Security 的运行原理 4 - 自定义登录方法和页面

    参考链接,多谢作者: http://blog.csdn.net/lee353086/article/details/52586916 http元素下的form-login元素是用来定义表单登录信息的. ...

  9. Spring Security 入门(3-11)Spring Security 的使用-自定义登录验证和回调地址

    配置文件 security-ns.xml <?xml version="1.0" encoding="UTF-8"?> <beans xmln ...

随机推荐

  1. thoughtworks面试题分析与解答

    题目描述 A squad of robotic rovers are to be landed by NASA on a plateau on Mars. This plateau, which is ...

  2. 大数据(1):基于sogou.500w.utf8数据的MapReduce程序设计

    环境:centos7+hadoop2.5.2 1.使用ECLIPS具打包运行WORDCOUNT实例,统计莎士比亚文集各单词计数(文件SHAKESPEARE.TXT). ①WorldCount.java ...

  3. Vue2.0+Node.js+MongoDB全栈打造商城系统 免费下载

    <ignore_js_op> 课程目录||--第01章 课程介绍|    01-01 课程-导学.mp4|    01-02 前端框架回顾.mp4|    01-03 vue概况以及核心思 ...

  4. 在js中实现新窗口打开页面

    我们都知道可以在html代码中使用<a href="xxxx" target="_blank"></a>这种方式来打开一个新的窗口打开一 ...

  5. Kafka OffsetMonitor:监控消费者和延迟的队列

    一个小应用程序来监视kafka消费者的进度和它们的延迟的队列. KafkaOffsetMonitor是用来实时监控Kafka集群中的consumer以及在队列中的位置(偏移量). 你可以查看当前的消费 ...

  6. java各种概念 Core Java总结

    Base: OOA是什么?OOD是什么?OOP是什么?{ oo(object-oriented):基于对象概念,以对象为中心,以类和继承为构造机制,来认识,理解,刻画客观世界和设计,构建相应的软件系统 ...

  7. php和apache工作原理?

    1.CGI和FastCGI是apache处理php脚本的其中两种工作模式,还有ISAPI,SAPI等 2.而php-fpm并不是一种工作模式,而是一个PHP在FastCGI模式运行下的进程管理器,全称 ...

  8. Kettle 初始配置数据量类型资源库

    PS:有段时间不使用Kettle了,但总遇到小伙伴问起,写一篇记录下. 文档使用版本:KETTLE 7.0 Kettle资源库可分为文件与数据库,文件型只需要配置好存放路径就行,这边介绍的是配置数据库 ...

  9. JavaScript(第六天)【函数】

    函数是定义一次但却可以调用或执行任意多次的一段JS代码.函数有时会有参数,即函数被调用时指定了值的局部变量.函数常常使用这些参数来计算一个返回值,这个值也成为函数调用表达式的值. 一.函数声明   函 ...

  10. Tornado 协程

    同步异步I/O客户端 from tornado.httpclient import HTTPClient,AsyncHTTPClient def ssync_visit(): http_client ...