1. 程式人生 > >Spring Security教程(七):RememberMe功能

Spring Security教程(七):RememberMe功能

在之前的教程中一筆帶過式的講了下RememberMe記住密碼的功能,那篇的Remember功能是最簡易的配置,其功能和安全性都不強。這裡就配置下security中RememberMe的各種方式。

一、概述

RememberMe 是指使用者在網站上能夠在 Session 之間記住登入使用者的身份的憑證,通俗的來說就是使用者登陸成功認證一次之後在制定的一定時間內可以不用再輸入使用者名稱和密碼進行自動登入。這個過程中通過服務端傳送一個 cookie 給客戶端瀏覽器儲存,下次瀏覽器再訪問服務端時服務端能夠自動檢測客戶端的 cookie,根據 cookie 值觸發自動登入操作。Spring Security中的 Remember-Me 功能通常有兩種實現方式。一種是簡單的使用加密來保證基於 cookie 的 token 的安全,另一種是通過資料庫或其它持久化儲存機制來儲存生成的 token


兩種方式的區別:第一中方式不安全,就是說在使用者獲取到實現記住我功能的 token 後,任何使用者都可以在該 token 過期之前通過該 token 進行自動登入。如果使用者發現自己的 token 被盜用了,那麼他可以通過改變自己的登入密碼來立即使其所有的記住我 token 失效。如果希望我們的應用能夠更安全,那就使用第二種方式。第二種方式也是詳細要講解的。

二、基於簡單加密的方式

需要特別注意的是,這兩種方式在配置的時候都要提供一個UserDetailsService,這個東西其實就是之前配置的jdbc-user-service標籤的一個實現類,配置程式碼如下:
<
beans:bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> <beans:property name="usersByUsernameQuery" value="select username,password,status as enabled from user where username = ?" /> <beans:property name
="authoritiesByUsernameQuery" value="select user.username,role.name from user,role,user_role where user.id=user_role.user_id and user_role.role_id=role.id and user.username=?" /> <beans:property name="dataSource" ref="dataSource" /> </beans:bean>

說明:

  1. dataSource就是連線資料庫的資料來源;
  2. usersByUsernameQuery就是配置jdbc-user-service時候的users-by-username-query,這個是根據使用者名稱來查詢使用者的sql語句;
  3. 同理authoritiesByUsernameQuery就是對應的authorities-by-username-query,這個用來根據使用者名稱查詢對應的許可權。

下面來配置rememberMe的過濾器:

<!-- Remember-Me 對應的 Filter -->
    <beans:bean id="rememberMeFilter"
        class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
        <beans:property name="rememberMeServices" ref="rememberMeServices" />
        <beans:property name="authenticationManager" ref="authenticationManager" />
    </beans:bean>

這個過濾器僅僅這樣配置是不會起作用的,還要把它加入的Security的FilterChain中去,用<custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>即可,另外這個過濾器要提供一個rememberMeServices和一個使用者認證的authenticationManager,後面這個其實就是authentication-manager所配置的東西,而前面這個需要另外配置,配置方式如下:

<bean id="rememberMeServices"
   class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
      <property name="userDetailsService" ref="userDetailsService" />
      <property name="key" value="zmc" />
      <!-- 指定 request 中包含的使用者是否選擇了記住我的引數名 -->
      <property name="parameter" value="rememberMe"/>
   </bean>

這個配置中的userDetailsService就是上面配置的,直接引用上面的即可;key就是token中的key,這個key可以用來方式token被修改,另外這個key要和後面配置的rememberMeAuthenticationProvider的key要一樣;parameter就是登陸介面的點選記住密碼的checkbox的name值,這個一定要一直,要不然沒有效果的。
除此之外還要配置一個使用者記住密碼做認證的authenticationManager

<bean id="rememberMeAuthenticationProvider"
   class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
      <property name="key" value="zmc" />
   </bean>

同時還要將其新增到authentication-manager標籤中去

<authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="userDetailsService">
        </authentication-provider>
        <!-- 記住密碼 -->
        <authentication-provider ref="rememberMeAuthenticationProvider"></authentication-provider>
    </authentication-manager>
最後面將rememberMeServices新增到myUsernamePasswordAuthenticationFilter中去。 最終的配置檔案如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                        http://www.springframework.org/schema/context
                        http://www.springframework.org/schema/context/spring-context-3.1.xsd
                        http://www.springframework.org/schema/tx
                        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
                        http://www.springframework.org/schema/security
                        http://www.springframework.org/schema/security/spring-security.xsd">
    
    <http pattern="/login.jsp" security="none"></http>
    <http auto-config="false" entry-point-ref="loginUrlAuthenticationEntryPoint">
        <!-- <form-login login-page="/login.jsp" default-target-url="/index.jsp" 
            authentication-failure-url="/login.jsp?error=true" /> -->
        <logout invalidate-session="true" logout-success-url="/login.jsp"
            logout-url="/j_spring_security_logout" />
        <custom-filter ref="myUsernamePasswordAuthenticationFilter"
            position="FORM_LOGIN_FILTER" />
        <!--替換預設REMEMBER_ME_FILTER-->    
        <custom-filter ref="rememberMeFilter" position="REMEMBER_ME_FILTER"/>
        <!-- 通過配置custom-filter來增加過濾器,before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity預設的過濾器之前執行。 -->
        <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR" />
    </http>
    <beans:bean id="loginUrlAuthenticationEntryPoint"
        class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
        <beans:property name="loginFormUrl" value="/login.jsp" />
    </beans:bean>
    <!-- 資料來源 -->
    <beans:bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
        destroy-method="close">
        <!-- 此為c3p0在spring中直接配置datasource c3p0是一個開源的JDBC連線池 -->
        <beans:property name="driverClass" value="com.mysql.jdbc.Driver" />
        <beans:property name="jdbcUrl"
            value="jdbc:mysql://localhost:3306/springsecuritydemo?useUnicode=true&characterEncoding=UTF-8" />
        <beans:property name="user" value="root" />
        <beans:property name="password" value="" />
        <beans:property name="maxPoolSize" value="50"></beans:property>
        <beans:property name="minPoolSize" value="10"></beans:property>
        <beans:property name="initialPoolSize" value="10"></beans:property>
        <beans:property name="maxIdleTime" value="25000"></beans:property>
        <beans:property name="acquireIncrement" value="1"></beans:property>
        <beans:property name="acquireRetryAttempts" value="30"></beans:property>
        <beans:property name="acquireRetryDelay" value="1000"></beans:property>
        <beans:property name="testConnectionOnCheckin" value="true"></beans:property>
        <beans:property name="idleConnectionTestPeriod" value="18000"></beans:property>
        <beans:property name="checkoutTimeout" value="5000"></beans:property>
        <beans:property name="automaticTestTable" value="t_c3p0"></beans:property>
    </beans:bean>
    
    <beans:bean id="builder" class="com.zmc.demo.JdbcRequestMapBulider">
        <beans:property name="dataSource" ref="dataSource" />
        <beans:property name="resourceQuery"
            value="select re.res_string,r.name from role r,resc re,resc_role rr where 
        r.id=rr.role_id and re.id=rr.resc_id" />
    </beans:bean>
 
    <beans:bean id="myUsernamePasswordAuthenticationFilter"
        class="com.zmc.demo.MyUsernamePasswordAuthenticationFilter
        ">
        <beans:property name="filterProcessesUrl" value="/j_spring_security_check" />
        <beans:property name="authenticationManager" ref="authenticationManager" />
        <beans:property name="authenticationSuccessHandler"
            ref="loginLogAuthenticationSuccessHandler" />
        <beans:property name="authenticationFailureHandler"
            ref="simpleUrlAuthenticationFailureHandler" />
        <beans:property name="rememberMeServices" ref="rememberMeServices" />
    </beans:bean>
 
    <beans:bean id="loginLogAuthenticationSuccessHandler"
        class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
        <beans:property name="targetUrlParameter" value="/index.jsp" />
    </beans:bean>
 
    <beans:bean id="simpleUrlAuthenticationFailureHandler"
        class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
        <beans:property name="defaultFailureUrl" value="/login.jsp" />
    </beans:bean>
 
 
    <!-- 認證過濾器 -->
    <beans:bean id="filterSecurityInterceptor"
        class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
        <!-- 使用者擁有的許可權 -->
        <beans:property name="accessDecisionManager" ref="accessDecisionManager" />
        <!-- 使用者是否擁有所請求資源的許可權 -->
        <beans:property name="authenticationManager" ref="authenticationManager" />
        <!-- 資源與許可權對應關係 -->
        <beans:property name="securityMetadataSource" ref="securityMetadataSource" />
    </beans:bean>
 
    <!-- acl領域模型 -->
    <beans:bean class="com.zmc.demo.MyAccessDecisionManager" id="accessDecisionManager">
    </beans:bean>
    <!-- -->
    <authentication-manager alias="authenticationManager">
        <authentication-provider user-service-ref="userDetailsService
        </authentication-provider>
        <!-- 記住密碼 -->
        <authentication-provider ref="rememberMeAuthenticationProvider"></authentication-provider>
    </authentication-manager>
    <!-- 配置userDetailsService -->
    <beans:bean id="userDetailsService"
        class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
        <beans:property name="usersByUsernameQuery"
            value="select username,password,status as enabled from user where username = ?" />
        <beans:property name="authoritiesByUsernameQuery"
            value="select user.username,role.name from user,role,user_role 
                                       where user.id=user_role.user_id and 
                                       user_role.role_id=role.id and user.username=?" />
        <beans:property name="dataSource" ref="dataSource" />
    </beans:bean>
    <!-- Remember-Me 對應的 Filter -->
    <beans:bean id="rememberMeFilter"
        class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
        <beans:property name="rememberMeServices" ref="rememberMeServices" />
        <beans:property name="authenticationManager" ref="authenticationManager" />
    </beans:bean>
    <!-- rememberService -->
    <!-- RememberMeServices 的實現 -->
    <beans:bean id="rememberMeServices"
        class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
        <beans:property name="userDetailsService" ref="userDetailsService" />
        <beans:property name="key" value="zmc" />
        <!-- 指定 request 中包含的使用者是否選擇了記住我的引數名 -->
        <beans:property name="parameter" value="rememberMe" />
      </beans:property>
    </beans:bean>
    <!-- 記住密碼 -->
    <!-- key 值需與對應的 RememberMeServices 保持一致 -->
    <beans:bean id="rememberMeAuthenticationProvider"
        class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
        <beans:property name="key" value="zmc" />
    </beans:bean>
    <beans:bean id="securityMetadataSource"
        class="com.zmc.demo.MyFilterInvocationSecurityMetadataSource">
        <beans:property name="builder" ref="builder"></beans:property>
    </beans:bean>
    
</beans:beans>

因為這個例子是在之前的例子上進行修改的,所以其它的沒講到的一些配置在之前部落格都有詳細的講解,請參考之前的部落格。

二、基於持久化的方式配置

在講這配置之前先對上面rememberService中的實現類TokenBasedRememberMeServices進行簡單的講解,該類主要是基於簡單加密 token 的一個實現類。TokenBasedRememberMeServices 會在使用者選擇了記住我成功登入後,生成一個包含 token 資訊的 cookie 傳送到客戶端;如果使用者登入失敗則會刪除客戶端儲存的實現 Remember-Me 的 cookie。需要自動登入時,它會判斷 cookie 中所包含的關於 Remember-Me 的資訊是否與系統一致,一致則返回一個 RememberMeAuthenticationToken 供 RememberMeAuthenticationProvider 處理,不一致則會刪除客戶端的 Remember-Me cookie。TokenBasedRememberMeServices 還實現了 Spring Security 的 LogoutHandler 介面,所以它可以在使用者退出登入時立即清除 Remember-Me cookie。

而基礎持久化方式配置的實質就是這個類不同,基於持久化方式配置的所用的實現類為:PersistentTokenBasedRememberMeServices,一看名字就知道其作用,就是將token進行持久化儲存起來,要儲存資料相應的就是為其制定儲存的地方,這個儲存的地方就是用PersistentTokenRepository來指定的,Spring Security 對此有兩種實現,InMemoryTokenRepositoryImpl 和 JdbcTokenRepositoryImpl。前者是將 token 存放在記憶體中的,通常用於測試,而後者是將 token 存放在資料庫中。PersistentTokenBasedRememberMeServices 預設使用的是前者,我們可以通過其 tokenRepository 屬性來指定使用的 PersistentTokenRepository。這例子用JdbcTokenRepositoryImpl來進行持久化儲存,顯然要往資料庫儲存資料,肯定要有一張表,這個表security也有提供,sql語句為:create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)。在資料庫中建立該表即可。

建立後的表為:

所以持久化方式配置只需要將第一種方式的TokenBasedRememberMeServices進行修改就可以,用以下程式碼提換就可以了:

<beans:bean id="rememberMeServices"
        class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
        <beans:property name="userDetailsService" ref="userDetailsService" />
        <beans:property name="key" value="zmc" />
        <!-- 指定 request 中包含的使用者是否選擇了記住我的引數名 -->
        <beans:property name="parameter" value="rememberMe" />
        <beans:property name="tokenRepository">
         <beans:bean class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
            <!-- 資料來源 -->
            <beans:property name="dataSource" ref="dataSource"/>
            <!-- 是否在啟動時建立持久化 token 的資料庫表 若為true,但資料有這個表時,會啟動失敗,提示表已存在 -->
            <beans:property name="createTableOnStartup" value="false"/>
         </beans:bean>
      </beans:property>
    </beans:bean>

三、兩種配置的效果

從上面的圖可以看出,但沒點選2周不用登陸的時候,登陸後再退出,再訪問資源的時候就要求重新登陸。同時資料庫也不會新增資料。

 

從上圖可以看到,當勾選2周不用登陸在登陸後,就算退出登陸後,再訪問資源也可以不用直接訪問,同時,資料庫也會將登陸資訊儲存起來,比較兩次資料還可以發現,除了使用者名稱沒變,其他的資料都會因為第二次訪問而進行更新。