知識庫 / Spring / Spring Security RSS 訂閱

Spring Security 定製註銷處理器

Spring Security
HongKong
6
01:00 PM · Dec 06 ,2025

1. 概述

Spring Security 框架提供非常靈活且強大的身份驗證支持。 結合用户識別,我們通常需要處理用户註銷事件,並在某些情況下添加自定義註銷行為。 諸如失效用户緩存或關閉已認證會話這樣的用例就是一個例子。

為了實現這個目的,Spring 提供了 <em >LogoutHandler</em> 接口,並在本教程中,我們將探討如何實現自定義的註銷處理程序。

2. 處理登出請求

任何需要用户登錄的 Web 應用程序最終都需要提供登出功能。Spring Security 處理程序通常控制登出過程。 基本上,我們有以下兩種處理登出請求的方式。 如我們即將看到的,其中一種方式是通過實現 LogoutHandler 接口。

2.1. <em >LogoutHandler</em > 接口

<em >LogoutHandler</em > 接口具有以下定義:

public interface LogoutHandler {
    void logout(HttpServletRequest request, HttpServletResponse response,Authentication authentication);
}

我們可以為我們的應用程序添加所需的任意數量的註銷處理程序。 唯一的要求是,在實現過程中不能拋出任何異常。 這是因為處理程序的操作不能在註銷時破壞應用程序狀態。

例如,一個處理程序可以執行緩存清理,並且它的方法必須成功完成。 在教程示例中,我們將展示這個用例。

2.2. <em>LogoutSuccessHandler</em> 接口

我們可以利用異常來控制用户註銷策略。為此,我們有 <em>LogoutSuccessHandler</em> 接口<em>onLogoutSuccess</em> 方法。 此方法可以引發異常,將用户重定向到適當的目的地。

此外,使用 <em>LogoutSuccessHandler</em> 類型 時,<b 沒有辦法添加多個處理程序,因此應用程序中只有一個可能的實現。一般來説,這實際上是註銷策略的最後一步。

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);
}

使用這項服務,我們可以從數據庫中根據用户名(登錄名)檢索用户,並在內部地圖中存儲它們:

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 身份驗證進行身份驗證:

@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();
    }
}

需要注意的是,上述配置中最重要的部分是 addLogoutHandler 方法。我們在退出處理的末尾傳遞並 觸發我們的 CustomLogoutHandler。 剩餘的設置用於調整 HTTP Basic Auth。

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);
}

讓我們分解步驟,以瞭解我們已經做了什麼:

  1. 首先,我們檢查緩存是否為空
  2. 然後,我們通過 withBasicAuth 方法對用户進行身份驗證
  3. 現在我們可以驗證從用户數據和語言值中檢索到的數據
  4. 因此,我們可以驗證用户現在必須緩存
  5. 再次,我們通過命中語言端點並使用會話 cookie 來驗證用户數據
  6. 最後,我們驗證註銷用户
在我們的第二個測試中,我們將驗證用户註銷時緩存將被清理。 這就是我們的註銷處理程序將被調用時。
@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 的 <em >LogoutHandler</em> 接口,來實現自定義註銷處理程序,從而從用户緩存中清除用户。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.