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);
}
讓我們分解步驟,以瞭解我們已經做了什麼:
- 首先,我們檢查緩存是否為空
- 然後,我們通過 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 的 <em >LogoutHandler</em> 接口,來實現自定義註銷處理程序,從而從用户緩存中清除用户。