1. 概述
Spring Security 框架提供非常靈活和強大的身份驗證支持。 結合用户識別,我們通常需要處理用户註銷事件,以及在某些情況下添加自定義註銷行為。 諸如失效用户緩存或關閉認證會話等用例就是其中之一。
為了實現這個目的,Spring 提供了 LogoutHandler 接口,並且在本教程中,我們將探討如何實現自定義的註銷處理程序。
2. 處理註銷請求
任何需要記錄用户登錄的 Web 應用程序最終都需要提供註銷功能。 Spring Security 處理程序通常控制註銷過程。 基本上,我們有兩類處理註銷的方法。 如我們即將看到的,一種方法是實現 LogoutHandler 接口。
2.1. LogoutHandler 接口
LogoutHandler 接口的定義如下:
public interface LogoutHandler {
void logout(HttpServletRequest request, HttpServletResponse response,Authentication authentication);
}
我們可以將所需的註銷處理程序數量增加到任何程度。 唯一的要求是實現中不能拋出任何異常。 這是因為處理程序操作不能在註銷時破壞應用程序狀態。
例如,一個處理程序可能會執行緩存清理,並且其方法必須成功完成。 在教程示例中,我們將演示確切的用例。
2.2. LogoutSuccessHandler 接口
另一方面,我們可以使用異常來控制用户註銷策略。 為此,我們有 LogoutSuccessHandler 接口和 onLogoutSuccess 方法。 此方法可以引發異常以將用户重定向到適當的目的地。
此外,當使用 LogoutSuccessHandler 類型時,不能添加多個處理程序,因此只有一個可能的實現適用於應用程序。 基本上,這確實是註銷策略的最後一點。
3. LogoutHandler 接口在實踐中的應用
現在,讓我們創建一個簡單的 Web 應用程序,以演示登出處理過程。 我們將實現一些簡單的緩存邏輯,以避免與數據庫的無必要訪問。
讓我們從 application.properties 文件開始,其中包含我們樣本應用程序的數據庫連接屬性:
spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=test
spring.datasource.password=test
spring.jpa.hibernate.ddl-auto=create
3.1. Web 應用程序設置
接下來,我們將添加一個簡單的 User 實體,我們將在登錄目的和數據檢索中使用它。 如你所見,User 類映射到數據庫中的 users 表:
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(unique = true)
private String login;
private String password;
private String role;
private String language;
// standard setters and getters
}
為了我們的應用程序的緩存目的,我們將實現一個使用 ConcurrentHashMap 內部存儲用户的緩存服務:
@Service
public class UserCache {
@PersistenceContext
private EntityManager entityManager;
private final ConcurrentMap<String, User> store = new ConcurrentHashMap<>(256);
}
使用這個服務,我們可以從數據庫中檢索用户名(login)並將其存儲在我們的映射中:
public User getByUserName(String userName) {
return store.computeIfAbsent(userName, k ->
entityManager.createQuery("from User where login=:login", User.class)
.setParameter("login", k)
.getSingleResult());
}
此外,還可以從存儲中清除用户。 如你所見,這將是我們在登出處理的末尾觸發的主要操作:
public void evictUser(String userName) {
store.remove(userName);
}
要檢索用户數據和語言信息,我們將使用標準的 Spring Controller:
@Controller
@RequestMapping(path = "/user")
public class UserController {
private final UserCache userCache;
public UserController(UserCache userCache) {
this.userCache = userCache;
}
@GetMapping(path = "/language")
@ResponseBody
public String getLanguage() {
String userName = UserUtils.getAuthenticatedUserName();
User user = userCache.getByUserName(userName);
return user.getLanguage();
}
}
3.2. Web 安全配置
在我們的應用程序中,我們關注兩個簡單的操作——登錄和登出。 首先,我們需要配置我們的 MVC 配置類,以允許用户使用 Basic HTTP Auth 進行身份驗證:
@Configuration
@EnableWebSecurity
public class MvcConfiguration {
@Autowired
private DataSource dataSource;
@Autowired
private CustomLogoutHandler logoutHandler;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults())
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
authorizationManagerRequestMatcherRegistry.requestMatchers(HttpMethod.GET, "/user/**").hasRole("USER"))
.logout(httpSecurityLogoutConfigurer ->
httpSecurityLogoutConfigurer.logoutUrl("/user/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)).permitAll())
.securityContext(httpSecuritySecurityContextConfigurer -> httpSecuritySecurityContextConfigurer.requireExplicitSave(false))
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable);
return http.build();
}
}
需要注意的是,從上面的配置中,我們傳遞並觸發 CustomLogoutHandler 在登出處理的末尾。
3.3. 自定義登出處理程序
最後,也是最重要的是,我們將編寫自定義登出處理程序,以處理必要的緩存清理:
@Service
public class CustomLogoutHandler implements LogoutHandler {
private final UserCache userCache;
public CustomLogoutHandler(UserCache userCache) {
this.userCache = userCache;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) {
String userName = UserUtils.getAuthenticatedUserName();
userCache.evictUser(userName);
}
}
如你所見,我們覆蓋 logout 方法,並簡單地從用户緩存中清除給定的用户。
4. 集成測試
現在我們測試功能。首先,我們需要驗證緩存是否按預期工作——即,它將授權用户加載到其內部存儲中:
@Test
public void whenLogin_thenUseUserCache() {
assertThat(userCache.size()).isZero();
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
.getForEntity(getLanguageUrl(), String.class);
assertThat(response.getBody()).contains("english");
assertThat(userCache.size()).isEqualTo(1);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", response.getHeaders()
.getFirst(HttpHeaders.SET_COOKIE));
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET,
new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getBody()).contains("english");
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET,
new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(200);
}
讓我們分解步驟以瞭解我們所做的事情:
- 首先,我們檢查緩存是否為空
- 然後,我們通過 withBasicAuth 方法對用户進行身份驗證
- 現在我們可以驗證從緩存中檢索到的用户數據和語言值
- 因此,我們可以驗證用户現在必須位於緩存中
- 再次,我們通過命中語言端點並使用會話 cookie 檢查用户數據<
- 最後,我們驗證註銷用户
在我們的第二個測試中,我們將驗證用户註銷時緩存被清理。 此時將調用我們的註銷處理程序:
@Test
public void whenLogout_thenCacheIsEmpty() {
assertThat(userCache.size()).isZero();
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "pass")
.getForEntity(getLanguageUrl(), String.class);
assertThat(response.getBody()).contains("english");
assertThat(userCache.size()).isEqualTo(1);
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Cookie", response.getHeaders()
.getFirst(HttpHeaders.SET_COOKIE));
response = restTemplate.exchange(getLogoutUrl(), HttpMethod.GET,
new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(200);
assertThat(userCache.size()).isZero();
response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET,
new HttpEntity<String>(requestHeaders), String.class);
assertThat(response.getStatusCode()
.value()).isEqualTo(401);
}
再次,一步一步:
- 如前所述,我們首先檢查緩存是否為空
- 然後,我們對用户進行身份驗證並檢查用户是否位於緩存中
- 接下來,我們執行註銷並檢查用户是否從緩存中刪除
- 最後,嘗試命中語言端點結果與 401 HTTP 未授權響應代碼
5. 結論
在本教程中,我們學習瞭如何使用 Spring 的 LogoutHandler 接口,來實現自定義註銷處理程序,從而從用户緩存中清除用户。