Spring Security Custom Logout Handler – Spring Security自定义注销处理程序

最后修改: 2020年 4月 26日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The Spring Security framework provides very flexible and powerful support for authentication. Together with user identification, we’ll typically want to handle user logout events and, in some cases, add some custom logout behavior. One such use case could be for invalidating a user cache or closing authenticated sessions.

Spring Security框架为认证提供了非常灵活和强大的支持。与用户识别一起,我们通常希望处理用户注销事件,并在某些情况下,添加一些自定义注销行为。一个这样的用例可能是为了使用户缓存无效或关闭已认证的会话。

For this very purpose, Spring provides the LogoutHandler interface, and in this tutorial, we’ll take a look at how to implement our own custom logout handler.

为了这个目的,Spring提供了LogoutHandler接口,在本教程中,我们将看看如何实现我们自己的自定义注销处理程序。

2. Handling Logout Requests

2.处理注销请求

Every web application that logs users in must log them out someday. Spring Security handlers usually control the logout process. Basically, we have two ways of handling logout. As we’re going to see, one of them is implementing the LogoutHandler interface.

每个登录用户的Web应用程序都必须在某一天将其注销。Spring Security处理程序通常控制注销过程。基本上,我们有两种处理注销的方式。正如我们将要看到的,其中一种是实现LogoutHandler接口。

2.1. LogoutHandler Interface

2.1.LogoutHandler 接口

The LogoutHandler interface has the following definition:

LogoutHandler接口有以下定义。

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

It is possible to add as many logout handlers as we need to our application. The one requirement for the implementation is that no exceptions are thrown. This is because handler actions must not break the application state on logout.

我们可以根据需要在我们的应用程序中添加尽可能多的注销处理程序。对实现的一个要求是不抛出异常。这是因为处理程序的动作不能在注销时破坏应用程序的状态。

For example, one of the handlers may do some cache cleanup, and its method must complete successfully. In the tutorial example, we’ll show exactly this use case.

例如,其中一个处理程序可能会做一些缓存清理,其方法必须成功完成。在教程的例子中,我们将确切地展示这种用例。

2.2. LogoutSuccessHandler Interface

2.2.LogoutSuccessHandler接口

On the other hand, we can use exceptions to control the user logout strategy. For this, we have the LogoutSuccessHandler interface and the onLogoutSuccess method. This method may raise an exception to set user redirection to an appropriate destination.

另一方面,我们可以使用异常来控制用户的注销策略。为此,我们有LogoutSuccessHandler接口和onLogoutSuccess方法。这个方法可能会引发一个异常,以将用户重定向到一个适当的目的地。

Furthermore, it’s not possible to add multiple handlers when using a LogoutSuccessHandler type, so there is only one possible implementation for the application. Generally speaking, it turns out that it’s the last point of the logout strategy.

此外,当使用LogoutSuccessHandler类型时,不可能添加多个处理程序,所以对应用程序来说只有一个可能的实现。一般来说,事实证明,这是注销策略的最后一点。

3. LogoutHandler Interface in Practice

3.LogoutHandler接口的实践

Now, let’s create a simple web application to demonstrate the logout handling process. We’ll implement some simple caching logic to retrieve user data to avoid unnecessary hits on the database.

现在,让我们创建一个简单的Web应用程序来演示注销处理过程。我们将实现一些简单的缓存逻辑来检索用户数据,以避免对数据库的不必要的点击。

Let’s start with the application.properties file, which contains the database connection properties for our sample application:

让我们从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 Application Setup

3.1.网络应用程序设置

Next, we’ll add a simple User entity that we’ll use for login purposes and data retrieval. As we can see, the User class maps to the users table in our database:

接下来,我们将添加一个简单的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
}

For the caching purposes of our application, we’ll implement a cache service that uses a ConcurrentHashMap internally to store users:

为了我们应用程序的缓存目的,我们将实现一个缓存服务,在内部使用一个ConcurrentHashMap来存储用户。

@Service
public class UserCache {
    @PersistenceContext
    private EntityManager entityManager;

    private final ConcurrentMap<String, User> store = new ConcurrentHashMap<>(256);
}

Using this service, we can retrieve a user by user name (login) from the database and store it internally in our map:

使用这个服务,我们可以通过用户名(login)从数据库中检索到一个用户,并在我们的地图中内部存储。

public User getByUserName(String userName) {
    return store.computeIfAbsent(userName, k -> 
      entityManager.createQuery("from User where login=:login", User.class)
        .setParameter("login", k)
        .getSingleResult());
}

Furthermore, it is possible to evict the user from the store. As we’ll see later, this will be the main action that we’ll invoke from our logout handler:

此外,还可以将用户从商店中驱逐出去。正如我们稍后所见,这将是我们将从注销处理程序中调用的主要动作。

public void evictUser(String userName) {
    store.remove(userName);
}

To retrieve user data and language information we’ll use a standard Spring Controller:

为了检索用户数据和语言信息,我们将使用一个标准的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 Security Configuration

3.2.网络安全配置

There are two simple actions we’ll focus on in the application — login and logout. First, we need to set up our MVC configuration class to allow users to authenticate using Basic HTTP Auth:

在应用程序中,有两个简单的动作我们要重点关注–登录和注销。首先,我们需要设置我们的MVC配置类,允许用户使用Basic HTTP Auth进行认证。

@Configuration
@EnableWebSecurity
public class MvcConfiguration {

    @Autowired
    private CustomLogoutHandler logoutHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.httpBasic()
            .and()
                .authorizeRequests()
                    .antMatchers(HttpMethod.GET, "/user/**")
                    .hasRole("USER")
            .and()
                .logout()
                    .logoutUrl("/user/logout")
                    .addLogoutHandler(logoutHandler)
                    .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
                    .permitAll()
            .and()
                .csrf()
                    .disable()
                .formLogin()
                    .disable();
        return http.build();
    }

    // further configuration
}

The important part to note from the above configuration is the addLogoutHandler method. We pass and trigger our CustomLogoutHandler at the end of logout processing. The remaining settings fine-tune the HTTP Basic Auth.

从上述配置中需要注意的重要部分是addLogoutHandler方法。我们在注销处理结束时传递并触发我们的CustomLogoutHandler。其余的设置对HTTP Basic Auth进行了微调。

3.3. Custom Logout Handler

3.3.自定义注销处理程序

Finally, and most importantly, we’ll write our custom logout handler that handles the necessary user cache cleanup:

最后,也是最重要的,我们将编写我们的自定义注销处理程序,处理必要的用户缓存清理。

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

As we can see, we override the logout method and simply evict the given user from the user cache.

我们可以看到,我们覆盖了logout方法,并简单地从用户缓存中驱逐了给定的用户。

4. Integration Testing

4.集成测试

Let’s now test the functionality. To begin with, we need to verify that the cache works as intended — that is, it loads authorized users into its internal store:

现在让我们来测试一下这个功能。首先,我们需要验证缓存是否按预期工作–也就是说,它将授权用户加载到其内部存储中

@Test
public void whenLogin_thenUseUserCache() {
    assertThat(userCache.size()).isEqualTo(0);

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

Let’s decompose the steps to understand what we’ve done::

让我们分解一下步骤,了解我们做了什么:。

  • First, we check that the cache is empty
  • Next, we authenticate a user via the withBasicAuth method
  • Now we can verify the user data and language value retrieved
  • Consequently, we can verify that the user must now be in the cache
  • Again, we check the user data by hitting the language endpoint and using a session cookie
  • Finally, we verify logging out the user

In our second test, we’ll verify that the user cache is cleaned when we logout. This is the moment when our logout handler will be invoked:

在我们的第二个测试中,我们将验证当我们注销时,用户的缓存被清理了。这就是我们的注销处理程序将被调用的时刻。

@Test
public void whenLogout_thenCacheIsEmpty() {
    assertThat(userCache.size()).isEqualTo(0);

    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()).isEqualTo(0);

    response = restTemplate.exchange(getLanguageUrl(), HttpMethod.GET, 
      new HttpEntity<String>(requestHeaders), String.class);
    assertThat(response.getStatusCode()
        .value()).isEqualTo(401);
}

Again, step by step:

同样,一步一步来。

  • As before, we begin by checking that the cache is empty
  • Then we authenticate a user and check the user is in the cache
  • Next, we perform a logout and check that the user has been removed from the cache
  • Finally, an attempt to hit the language endpoint results with 401 HTTP unauthorized response code

5. Conclusion

5.总结

It this tutorial, we learned how to implement a custom logout handler for evicting users from a user cache using Spring’s LogoutHandler interface.

在本教程中,我们学习了如何使用Spring的LogoutHandler接口实现自定义注销处理程序,以便从用户缓存中驱逐用户。

As always, the full source code of the article is available over on GitHub.

一如既往,该文章的完整源代码可在GitHub上获得