Spring Security 定製註銷處理器

Spring Security
Remote
0
06:00 AM · Nov 30 ,2025

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 接口,來實現自定義註銷處理程序,從而從用户緩存中清除用户。

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

發佈 評論

Some HTML is okay.