目錄
1.1 概述
1.2 基於簡單加密token的方法
1.3 基於持久化token的方法
1.4 Remember-Me相關接口和實現類
1.4.1 TokenBasedRememberMeServices
1.4.2 PersistentTokenBasedRememberMeServices
1.1 概述
Remember-Me是指網站能夠在Session之間記住登錄用戶的身份,具體來說就是我成功認證一次之后在一定的時間內我可以不用再輸入用戶名和密碼進行登錄了,系統會自動給我登錄。這通常是通過服務端發送一個cookie給客戶端瀏覽器,下次瀏覽器再訪問服務端時服務端能夠自動檢測客戶端的cookie,根據cookie值觸發自動登錄操作。Spring Security為這些操作的發生提供必要的鈎子,並且針對於Remember-Me功能有兩種實現。一種是簡單的使用加密來保證基於cookie的token的安全,另一種是通過數據庫或其它持久化存儲機制來保存生成的token。
需要注意的是兩種實現都需要一個UserDetailsService。如果你使用的AuthenticationProvider不使用UserDetailsService,那么記住我將會不起作用,除非在你的ApplicationContext中擁有一個UserDetailsService類型的bean。
1.2 基於簡單加密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>
1.3 基於持久化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>
1.4 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的方法。
1.4.1 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。
1.4.2 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">
<beanclass="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
<!-- 數據源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 是否在系統啟動時創建持久化token的數據庫表 -->
<property name="createTableOnStartup" value="false"/>
</bean>
</property>
</bean>
(注:本文是基於Spring Security3.1.6所寫)