記住我 認證

記住我(remember-me)或持久化登入(persistent-login)認證是指網站能夠在不同會話之間記住使用者的身份。這通常透過向瀏覽器傳送一個 cookie 來實現,該 cookie 在未來的會話中被檢測到並觸發自動登入。Spring Security 為這些操作提供了必要的鉤子,並提供了兩種具體的記住我實現。一種使用雜湊來保障基於 cookie 的令牌的安全,另一種使用資料庫或其他持久化儲存機制來儲存生成的令牌。

注意,這兩種實現都需要一個 UserDetailsService。如果您使用的認證提供者不使用 UserDetailsService(例如,LDAP 提供者),那麼它將無法工作,除非您的應用程式上下文中有另一個 UserDetailsService bean。

簡單的基於雜湊令牌的方法

這種方法使用雜湊來實現一個實用的記住我策略。本質上,在成功進行互動式認證後,會向瀏覽器傳送一個 cookie,該 cookie 的構成如下

base64(username + ":" + expirationTime + ":" + algorithmName + ":"
algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token
algorithmName:     The algorithm used to generate and to verify the remember-me token signature

記住我令牌僅在指定的期限內有效,並且僅當用戶名、密碼和金鑰未更改時才有效。值得注意的是,這存在一個潛在的安全問題,即捕獲的記住我令牌可以在任何使用者代理中使用,直到令牌過期。這與摘要認證存在相同的問題。如果使用者意識到令牌已被捕獲,他們可以輕鬆更改密碼並立即使所有已頒發的記住我令牌失效。如果需要更強的安全性,您應該使用下一節描述的方法。或者,完全不應使用記住我服務。

如果您熟悉名稱空間配置章節中討論的主題,您可以透過新增 <remember-me> 元素來啟用記住我認證

<http>
...
<remember-me key="myAppKey"/>
</http>

UserDetailsService 通常會自動選擇。如果您的應用程式上下文中有多個,則需要使用 user-service-ref 屬性指定應使用哪一個,其值是您的 UserDetailsService bean 的名稱。

持久化令牌方法

這種方法基於文章 Improved Persistent Login Cookie Best Practice,並做了一些小修改 [1]。要在名稱空間配置中使用這種方法,您需要提供一個數據源引用

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

資料庫應包含一個 persistent_logins 表,使用以下 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)

記住我介面和實現

記住我在 UsernamePasswordAuthenticationFilter 中使用,並透過 AbstractAuthenticationProcessingFilter 超類中的鉤子實現。它也在 BasicAuthenticationFilter 中使用。這些鉤子會在適當的時候呼叫具體的 RememberMeServices 實現。以下清單顯示了該介面

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
	Authentication successfulAuthentication);

請參閱 RememberMeServices 的 Javadoc,以獲取關於這些方法功能的更詳細討論,但請注意,在此階段,AbstractAuthenticationProcessingFilter 僅呼叫 loginFail()loginSuccess() 方法。每當 SecurityContextHolder 不包含 Authentication 時,RememberMeAuthenticationFilter 都會呼叫 autoLogin() 方法。因此,此介面為底層記住我實現提供了足夠的認證相關事件通知,並在候選 web 請求可能包含 cookie 並希望被記住時委託給實現。這種設計允許使用任意數量的記住我實現策略。

我們之前看到,Spring Security 提供了兩種實現。我們將依次介紹它們。

TokenBasedRememberMeServices

此實現支援簡單的基於雜湊令牌的方法中描述的更簡單的方法。TokenBasedRememberMeServices 生成一個 RememberMeAuthenticationToken,由 RememberMeAuthenticationProvider 處理。認證提供者和 TokenBasedRememberMeServices 之間共享一個 key。此外,TokenBasedRememberMeServices 需要一個 UserDetailsService,從中可以檢索使用者名稱和密碼用於簽名比較,並生成包含正確 GrantedAuthority 例項的 RememberMeAuthenticationTokenTokenBasedRememberMeServices 也實現了 Spring Security 的 LogoutHandler 介面,因此可以與 LogoutFilter 一起使用,以自動清除 cookie。

預設情況下,此實現使用 SHA-256 演算法對令牌簽名進行編碼。為了驗證令牌簽名,會解析並使用從 algorithmName 中檢索到的演算法。如果不存在 algorithmName,將使用預設匹配演算法,即 SHA-256。您可以指定不同的演算法用於簽名編碼和簽名匹配,這使得使用者可以安全地升級到不同的編碼演算法,同時在不存在 algorithmName 的情況下仍然能夠驗證舊的簽名。為此,您可以將自定義的 TokenBasedRememberMeServices 指定為一個 Bean 並在配置中使用它。

  • Java

  • XML

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
	http
			.authorizeHttpRequests((authorize) -> authorize
					.anyRequest().authenticated()
			)
			.rememberMe((remember) -> remember
				.rememberMeServices(rememberMeServices)
			);
	return http.build();
}

@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
	RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
	TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
	rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
	return rememberMe;
}
<http>
  <remember-me services-ref="rememberMeServices"/>
</http>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
    <property name="userDetailsService" ref="myUserDetailsService"/>
    <property name="key" value="springRocks"/>
    <property name="matchingAlgorithm" value="MD5"/>
    <property name="encodingAlgorithm" value="SHA256"/>
</bean>

在應用程式上下文中需要以下 bean 來啟用記住我服務

  • Java

  • XML

@Bean
RememberMeAuthenticationFilter rememberMeFilter() {
    RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
    rememberMeFilter.setRememberMeServices(rememberMeServices());
    rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
    return rememberMeFilter;
}

@Bean
TokenBasedRememberMeServices rememberMeServices() {
    TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
    rememberMeServices.setUserDetailsService(myUserDetailsService);
    rememberMeServices.setKey("springRocks");
    return rememberMeServices;
}

@Bean
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
    RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
    rememberMeAuthenticationProvider.setKey("springRocks");
    return rememberMeAuthenticationProvider;
}
<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

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

記住將您的 RememberMeServices 實現新增到 UsernamePasswordAuthenticationFilter.setRememberMeServices() 屬性中,將 RememberMeAuthenticationProvider 包含在您的 AuthenticationManager.setProviders() 列表中,並將 RememberMeAuthenticationFilter 新增到您的 FilterChainProxy 中(通常緊跟在 UsernamePasswordAuthenticationFilter 之後)。

PersistentTokenBasedRememberMeServices

您可以像使用 TokenBasedRememberMeServices 一樣使用此類,但它還需要配置一個 PersistentTokenRepository 來儲存令牌。

  • InMemoryTokenRepositoryImpl,僅用於測試。

  • JdbcTokenRepositoryImpl,將令牌儲存在資料庫中。

有關資料庫模式,請參閱持久化令牌方法


1. 本質上,cookie 中不包含使用者名稱,以防止不必要地暴露有效的登入名。在這篇文章的評論區有關於此的討論。