Remember-Me 是指網(wǎng)站能夠在 Session 之間記住登錄用戶的身份,具體來說就是我成功認(rèn)證一次之后在一定的時間內(nèi)我可以不用再輸入用戶名和密碼進(jìn)行登錄了,系統(tǒng)會自動給我登錄。這通常是通過服務(wù)端發(fā)送一個 cookie 給客戶端瀏覽器,下次瀏覽器再訪問服務(wù)端時服務(wù)端能夠自動檢測客戶端的 cookie,根據(jù) cookie 值觸發(fā)自動登錄操作。Spring Security 為這些操作的發(fā)生提供必要的鉤子,并且針對于 Remember-Me 功能有兩種實(shí)現(xiàn)。一種是簡單的使用加密來保證基于 cookie 的 token 的安全,另一種是通過數(shù)據(jù)庫或其它持久化存儲機(jī)制來保存生成的 token。
需要注意的是兩種實(shí)現(xiàn)都需要一個 UserDetailsService。如果你使用的 AuthenticationProvider 不使用 UserDetailsService,那么記住我將會不起作用,除非在你的 ApplicationContext 中擁有一個 UserDetailsService 類型的 bean。
當(dāng)用戶選擇了記住我成功登錄后,Spring Security 將會生成一個 cookie 發(fā)送給客戶端瀏覽器。cookie 值由如下方式組成:
base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key))
這樣用來實(shí)現(xiàn) Remember-Me 功能的 token 只能在指定的時間內(nèi)有效,且必須保證 token 中所包含的 username、password 和 key 沒有被改變才行。需要注意的是,這樣做其實(shí)是存在安全隱患的,那就是在用戶獲取到實(shí)現(xiàn)記住我功能的 token 后,任何用戶都可以在該 token 過期之前通過該 token 進(jìn)行自動登錄。如果用戶發(fā)現(xiàn)自己的 token 被盜用了,那么他可以通過改變自己的登錄密碼來立即使其所有的記住我 token 失效。如果希望我們的應(yīng)用能夠更安全一點(diǎn),可以使用接下來要介紹的持久化 token 方式,或者不使用 Remember-Me 功能,因?yàn)?Remember-Me 功能總是有點(diǎn)不安全的。
使用這種方式時,我們只需要在 http 元素下定義一個 remember-me 元素,同時指定其 key 屬性即可。key 屬性是用來標(biāo)記存放 token 的 cookie 的,對應(yīng)上文提到的生成 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 定義提供的默認(rèn)名稱,如果要自定義可以自己定義 TokenBasedRememberMeServices 或 PersistentTokenBasedRememberMeServices 對應(yīng)的 bean,然后通過其 parameter 屬性進(jìn)行指定,具體操作請參考后文關(guān)于《Remember-Me 相關(guān)接口和實(shí)現(xiàn)類》部分內(nèi)容。第二,上述功能需要一個 UserDetailsService,如果在你的 ApplicationContext 中已經(jīng)擁有一個了,那么 Spring Security 將自動獲取;如果沒有,那么當(dāng)然你需要定義一個;如果擁有在 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 的方法在實(shí)現(xiàn) Remember-Me 功能上大體相同,都是在用戶選擇了 “記住我” 成功登錄后,將生成的 token 存入 cookie 中并發(fā)送到客戶端瀏覽器,待到下次用戶訪問系統(tǒng)時,系統(tǒng)將直接從客戶端 cookie 中讀取 token 進(jìn)行認(rèn)證。所不同的是基于簡單加密 token 的方法,一旦用戶登錄成功后,生成的 token 將在客戶端保存一段時間,如果用戶不點(diǎn)擊退出登錄,或者不修改密碼,那么在 cookie 失效之前,他都可以使用該 token 進(jìn)行登錄,哪怕該 token 被別人盜用了,用戶與盜用者都同樣可以進(jìn)行登錄。而基于持久化 token 的方法采用這樣的實(shí)現(xiàn)邏輯:
從以上邏輯我們可以看出持久化 token 的方法比簡單加密 token 的方法更安全,因?yàn)橐坏┠愕?cookie 被人盜用了,你只要再利用原有的 cookie 試圖自動登錄一次,原有的 token 將失效導(dǎo)致盜用者不能再使用原來盜用的 cookie 進(jìn)行登錄了,同時用戶可以發(fā)現(xiàn)自己的 cookie 有被盜用的可能性。但因?yàn)?cookie 被盜用后盜用者還可以在用戶下一次登錄前順利的進(jìn)行登錄,所以如果你的應(yīng)用對安全性要求比較高就不要使用 Remember-Me 功能了。
使用持久化 token 方法時需要我們的數(shù)據(jù)庫中擁有如下表及其表結(jié)構(gòu)。
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 屬性指定對應(yīng)的數(shù)據(jù)源,同時別忘了它也同樣需要 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>
在上述介紹中,我們實(shí)現(xiàn) Remember-Me 功能是通過 Spring Security 為了簡化 Remember-Me 而提供的 NameSpace 進(jìn)行定義的。而底層實(shí)際上還是通過 RememberMeServices、UsernamePasswordAuthenticationFilter 和 RememberMeAuthenticationFilter 的協(xié)作來完成的。RememberMeServices 是 Spring Security 為 Remember-Me 提供的一個服務(wù)接口,其定義如下。
publicinterface RememberMeServices {
/**
* 自動登錄。在實(shí)現(xiàn)這個方法的時候應(yīng)該判斷用戶提供的 Remember-Me cookie 是否有效,如果無效,應(yīng)當(dāng)直接忽略。
* 如果認(rèn)證成功應(yīng)當(dāng)返回一個 AuthenticationToken,推薦返回 RememberMeAuthenticationToken;
* 如果認(rèn)證不成功應(yīng)當(dāng)返回 null。
*/
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
/**
* 在用戶登錄失敗時調(diào)用。實(shí)現(xiàn)者應(yīng)當(dāng)做一些類似于刪除 cookie 之類的處理。
*/
void loginFail(HttpServletRequest request, HttpServletResponse response);
/**
* 在用戶成功登錄后調(diào)用。實(shí)現(xiàn)者可以在這里判斷用戶是否選擇了 “Remember-Me” 登錄,然后做相應(yīng)的處理。
*/
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);
}
UsernamePasswordAuthenticationFilter 擁有一個 RememberMeServices 的引用,默認(rèn)是一個空實(shí)現(xiàn)的 NullRememberMeServices,而實(shí)際當(dāng)我們通過 remember-me 定義啟用 Remember-Me 時,它會是一個具體的實(shí)現(xiàn)。用戶的請求會先通過 UsernamePasswordAuthenticationFilter,如認(rèn)證成功會調(diào)用 RememberMeServices 的 loginSuccess() 方法,否則調(diào)用 RememberMeServices 的 loginFail() 方法。UsernamePasswordAuthenticationFilter 是不會調(diào)用 RememberMeServices 的 autoLogin() 方法進(jìn)行自動登錄的。之后運(yùn)行到 RememberMeAuthenticationFilter 時如果檢測到還沒有登錄,那么 RememberMeAuthenticationFilter 會嘗試著調(diào)用所包含的 RememberMeServices 的 autoLogin() 方法進(jìn)行自動登錄。關(guān)于 RememberMeServices Spring Security 已經(jīng)為我們提供了兩種實(shí)現(xiàn),分別對應(yīng)于前文提到的基于簡單加密 token 和基于持久化 token 的方法。
TokenBasedRememberMeServices 對應(yīng)于前文介紹的使用 namespace 時基于簡單加密 token 的實(shí)現(xiàn)。TokenBasedRememberMeServices 會在用戶選擇了記住我成功登錄后,生成一個包含 token 信息的 cookie 發(fā)送到客戶端;如果用戶登錄失敗則會刪除客戶端保存的實(shí)現(xiàn) Remember-Me 的 cookie。需要自動登錄時,它會判斷 cookie 中所包含的關(guān)于 Remember-Me 的信息是否與系統(tǒng)一致,一致則返回一個 RememberMeAuthenticationToken 供 RememberMeAuthenticationProvider 處理,不一致則會刪除客戶端的 Remember-Me cookie。TokenBasedRememberMeServices 還實(shí)現(xiàn)了 Spring Security 的 LogoutHandler 接口,所以它可以在用戶退出登錄時立即清除 Remember-Me cookie。
如果把使用 namespace 定義 Remember-Me 改為直接定義 RememberMeServices 和對應(yīng)的 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>
<!-- 用于認(rèn)證的 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 中包含的用戶名對應(yīng)的參數(shù)名 -->
<property name="usernameParameter" value="username"/>
<property name="passwordParameter" value="password"/>
<!-- 指定登錄的提交地址 -->
<property name="filterProcessesUrl" value="/login.do"/>
</bean>
<!-- Remember-Me 對應(yīng)的 Filter -->
<bean id="rememberMeFilter"
class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices" />
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<!-- RememberMeServices 的實(shí)現(xiàn) -->
<bean id="rememberMeServices"
class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="userDetailsService" />
<property name="key" value="elim" />
<!-- 指定 request 中包含的用戶是否選擇了記住我的參數(shù)名 -->
<property name="parameter" value="rememberMe"/>
</bean>
<!-- key 值需與對應(yīng)的 RememberMeServices 保持一致 -->
<bean id="rememberMeAuthenticationProvider"
class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="elim" />
</bean>
需要注意的是 RememberMeAuthenticationProvider 在認(rèn)證 RememberMeAuthenticationToken 的時候是比較它們擁有的 key 是否相等,而 RememberMeAuthenticationToken 的 key 是 TokenBasedRememberMeServices 提供的,所以在使用時需要保證 RememberMeAuthenticationProvider 和 TokenBasedRememberMeServices 的 key 屬性值保持一致。需要配置 UsernamePasswordAuthenticationFilter 的 rememberMeServices 為我們定義好的 TokenBasedRememberMeServices,把 RememberMeAuthenticationProvider 加入 AuthenticationManager 的 providers 列表,并添加 RememberMeAuthenticationFilter 和 UsernamePasswordAuthenticationFilter 到 FilterChainProxy。
PersistentTokenBasedRememberMeServices 是 RememberMeServices 基于前文提到的持久化 token 的方式實(shí)現(xiàn)的。具體實(shí)現(xiàn)邏輯跟前文介紹的以 NameSpace 的方式使用基于持久化 token 的 Remember-Me 是一樣的,這里就不再贅述了。此外,如果單獨(dú)使用,其使用方式和上文描述的 TokenBasedRememberMeServices 是一樣的,這里也不再贅述了。
需要注意的是 PersistentTokenBasedRememberMeServices 是需要將 token 進(jìn)行持久化的,所以我們必須為其指定存儲 token 的 PersistentTokenRepository。Spring Security 對此有兩種實(shí)現(xiàn),InMemoryTokenRepositoryImpl 和 JdbcTokenRepositoryImpl。前者是將 token 存放在內(nèi)存中的,通常用于測試,而后者是將 token 存放在數(shù)據(jù)庫中。PersistentTokenBasedRememberMeServices 默認(rèn)使用的是前者,我們可以通過其 tokenRepository 屬性來指定使用的 PersistentTokenRepository。
使用 JdbcTokenRepositoryImpl 時我們可以使用在前文提到的默認(rèn)表結(jié)構(gòu)。如果需要使用自定義的表,那么我們可以對 JdbcTokenRepositoryImpl 進(jìn)行重寫。定義 JdbcTokenRepositoryImpl 時需要指定一個數(shù)據(jù)源 dataSource,同時可以通過設(shè)置參數(shù) createTableOnStartup 的值來控制是否要在系統(tǒng)啟動時創(chuàng)建對應(yīng)的存入 token 的表,默認(rèn)創(chuàng)建語句為 “create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)”,但是如果自動創(chuàng)建時對應(yīng)的表已經(jīng)存在于數(shù)據(jù)庫中,則會拋出異常。createTableOnStartup 屬性默認(rèn)為 false。
直接顯示地使用 PersistentTokenBasedRememberMeServices 和上文提到的直接顯示地使用 TokenBasedRememberMeServices 的方式是一樣的,我們只需要將上文提到的配置中 RememberMeServices 實(shí)現(xiàn)類 TokenBasedRememberMeServices 換成 PersistentTokenBasedRememberMeServices 即可。
<!-- RememberMeServices 的實(shí)現(xiàn) -->
<bean id="rememberMeServices"
class="org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices">
<property name="userDetailsService" ref="userDetailsService" />
<property name="key" value="elim" />
<!-- 指定 request 中包含的用戶是否選擇了記住我的參數(shù)名 -->
<property name="parameter" value="rememberMe"/>
<!-- 指定 PersistentTokenRepository -->
<property name="tokenRepository">
<bean class="org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl">
<!-- 數(shù)據(jù)源 -->
<property name="dataSource" ref="dataSource"/>
<!-- 是否在系統(tǒng)啟動時創(chuàng)建持久化 token 的數(shù)據(jù)庫表 -->
<property name="createTableOnStartup" value="false"/>
</bean>
</property>
</bean>
更多建議: